2"""A collection of convenient functions and redis/lua scripts.
4This code was partial inspired by the `Bullet-Proofing Lua Scripts in RedisPy`_
7.. _Bullet-Proofing Lua Scripts in RedisPy:
8 https://redis.com/blog/bullet-proofing-lua-scripts-in-redispy/
14from searx
import get_setting
16LUA_SCRIPT_STORAGE = {}
17"""A global dictionary to cache client's ``Script`` objects, used by
18:py:obj:`lua_script_storage`"""
22 """Returns a redis :py:obj:`Script
23 <redis.commands.core.CoreCommands.register_script>` instance.
25 Due to performance reason the ``Script`` object is instantiated only once
26 for a client (``client.register_script(..)``) and is cached in
27 :py:obj:`LUA_SCRIPT_STORAGE`.
33 client_id = id(client)
35 if LUA_SCRIPT_STORAGE.get(client_id)
is None:
36 LUA_SCRIPT_STORAGE[client_id] = {}
38 if LUA_SCRIPT_STORAGE[client_id].get(script)
is None:
39 LUA_SCRIPT_STORAGE[client_id][script] = client.register_script(script)
41 return LUA_SCRIPT_STORAGE[client_id][script]
45local prefix = tostring(ARGV[1])
46for i, name in ipairs(redis.call('KEYS', prefix .. '*')) do
47 redis.call('EXPIRE', name, 0)
53 """Purge all keys with ``prefix`` from database.
55 Queries all keys in the database by the given prefix and set expire time to
56 zero. The default prefix will drop all keys which has been set by SearXNG
57 (drops SearXNG schema entirely from database).
59 The implementation is the lua script from string :py:obj:`PURGE_BY_PREFIX`.
60 The lua script uses EXPIRE_ instead of DEL_: if there are a lot keys to
61 delete and/or their values are big, `DEL` could take more time and blocks
62 the command loop while `EXPIRE` turns back immediate.
64 :param prefix: prefix of the key to delete (default: ``SearXNG_``)
67 .. _EXPIRE: https://redis.io/commands/expire/
68 .. _DEL: https://redis.io/commands/del/
71 script = lua_script_storage(client, PURGE_BY_PREFIX)
76 """Creates a hash of the ``name``.
78 Combines argument ``name`` with the ``secret_key`` from :ref:`settings
79 server`. This function can be used to get a more anonymized name of a Redis
82 :param name: the name to create a secret hash for
85 m = hmac.new(bytes(name, encoding=
'utf-8'), digestmod=
'sha256')
86 m.update(bytes(get_setting(
'server.secret_key'), encoding=
'utf-8'))
91local limit = tonumber(ARGV[1])
92local expire = tonumber(ARGV[2])
95local c = redis.call('GET', c_name)
98 c = redis.call('INCR', c_name)
100 redis.call('EXPIRE', c_name, expire)
104 if limit == 0 or c < limit then
105 c = redis.call('INCR', c_name)
113 """Increment a counter and return the new value.
115 If counter with redis key ``SearXNG_counter_<name>`` does not exists it is
116 created with initial value 1 returned. The replacement ``<name>`` is a
117 *secret hash* of the value from argument ``name`` (see
118 :py:func:`secret_hash`).
120 The implementation of the redis counter is the lua script from string
121 :py:obj:`INCR_COUNTER`.
123 :param name: name of the counter
126 :param expire: live-time of the counter in seconds (default ``None`` means
128 :type expire: int / see EXPIRE_
130 :param limit: limit where the counter stops to increment (default ``None``)
131 :type limit: int / limit is 2^64 see INCR_
133 :return: value of the incremented counter
136 .. _EXPIRE: https://redis.io/commands/expire/
137 .. _INCR: https://redis.io/commands/incr/
139 A simple demo of a counter with expire time and limit::
141 >>> for i in range(6):
142 ... i, incr_counter(client, "foo", 3, 5) # max 3, duration 5 sec
143 ... time.sleep(1) # from the third call on max has been reached
153 script = lua_script_storage(client, INCR_COUNTER)
154 name =
"SearXNG_counter_" + secret_hash(name)
155 c = script(args=[limit, expire], keys=[name])
160 """Drop counter with redis key ``SearXNG_counter_<name>``
162 The replacement ``<name>`` is a *secret hash* of the value from argument
163 ``name`` (see :py:func:`incr_counter` and :py:func:`incr_sliding_window`).
165 name =
"SearXNG_counter_" + secret_hash(name)
169INCR_SLIDING_WINDOW =
"""
170local expire = tonumber(ARGV[1])
172local current_time = redis.call('TIME')
174redis.call('ZREMRANGEBYSCORE', name, 0, current_time[1] - expire)
175redis.call('ZADD', name, current_time[1], current_time[1] .. current_time[2])
176local result = redis.call('ZCOUNT', name, 0, current_time[1] + 1)
177redis.call('EXPIRE', name, expire)
183 """Increment a sliding-window counter and return the new value.
185 If counter with redis key ``SearXNG_counter_<name>`` does not exists it is
186 created with initial value 1 returned. The replacement ``<name>`` is a
187 *secret hash* of the value from argument ``name`` (see
188 :py:func:`secret_hash`).
190 :param name: name of the counter
193 :param duration: live-time of the sliding window in seconds
196 :return: value of the incremented counter
199 The implementation of the redis counter is the lua script from string
200 :py:obj:`INCR_SLIDING_WINDOW`. The lua script uses `sorted sets in Redis`_
201 to implement a sliding window for the redis key ``SearXNG_counter_<name>``
202 (ZADD_). The current TIME_ is used to score the items in the sorted set and
203 the time window is moved by removing items with a score lower current time
204 minus *duration* time (ZREMRANGEBYSCORE_).
206 The EXPIRE_ time (the duration of the sliding window) is refreshed on each
207 call (increment) and if there is no call in this duration, the sorted
208 set expires from the redis DB.
210 The return value is the amount of items in the sorted set (ZCOUNT_), what
211 means the number of calls in the sliding window.
213 .. _Sorted sets in Redis:
214 https://redis.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/1-2-5-sorted-sets-in-redis/
215 .. _TIME: https://redis.io/commands/time/
216 .. _ZADD: https://redis.io/commands/zadd/
217 .. _EXPIRE: https://redis.io/commands/expire/
218 .. _ZREMRANGEBYSCORE: https://redis.io/commands/zremrangebyscore/
219 .. _ZCOUNT: https://redis.io/commands/zcount/
221 A simple demo of the sliding window::
223 >>> for i in range(5):
224 ... incr_sliding_window(client, "foo", 3) # duration 3 sec
225 ... time.sleep(1) # from the third call (second) on the window is moved
232 >>> time.sleep(3) # wait until expire
233 >>> incr_sliding_window(client, "foo", 3)
237 script = lua_script_storage(client, INCR_SLIDING_WINDOW)
238 name =
"SearXNG_counter_" + secret_hash(name)
239 c = script(args=[duration], keys=[name])
purge_by_prefix(client, str prefix="SearXNG_")
drop_counter(client, name)
incr_sliding_window(client, str name, int duration)
lua_script_storage(client, script)
incr_counter(client, str name, int limit=0, int expire=0)