.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
redislib.py
Go to the documentation of this file.
1# SPDX-License-Identifier: AGPL-3.0-or-later
2"""A collection of convenient functions and redis/lua scripts.
3
4This code was partial inspired by the `Bullet-Proofing Lua Scripts in RedisPy`_
5article.
6
7.. _Bullet-Proofing Lua Scripts in RedisPy:
8 https://redis.com/blog/bullet-proofing-lua-scripts-in-redispy/
9
10"""
11
12import hmac
13
14from searx import get_setting
15
16LUA_SCRIPT_STORAGE = {}
17"""A global dictionary to cache client's ``Script`` objects, used by
18:py:obj:`lua_script_storage`"""
19
20
21def lua_script_storage(client, script):
22 """Returns a redis :py:obj:`Script
23 <redis.commands.core.CoreCommands.register_script>` instance.
24
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`.
28
29 """
30
31 # redis connection can be closed, lets use the id() of the redis connector
32 # as key in the script-storage:
33 client_id = id(client)
34
35 if LUA_SCRIPT_STORAGE.get(client_id) is None:
36 LUA_SCRIPT_STORAGE[client_id] = {}
37
38 if LUA_SCRIPT_STORAGE[client_id].get(script) is None:
39 LUA_SCRIPT_STORAGE[client_id][script] = client.register_script(script)
40
41 return LUA_SCRIPT_STORAGE[client_id][script]
42
43
44PURGE_BY_PREFIX = """
45local prefix = tostring(ARGV[1])
46for i, name in ipairs(redis.call('KEYS', prefix .. '*')) do
47 redis.call('EXPIRE', name, 0)
48end
49"""
50
51
52def purge_by_prefix(client, prefix: str = "SearXNG_"):
53 """Purge all keys with ``prefix`` from database.
54
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).
58
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.
63
64 :param prefix: prefix of the key to delete (default: ``SearXNG_``)
65 :type name: str
66
67 .. _EXPIRE: https://redis.io/commands/expire/
68 .. _DEL: https://redis.io/commands/del/
69
70 """
71 script = lua_script_storage(client, PURGE_BY_PREFIX)
72 script(args=[prefix])
73
74
75def secret_hash(name: str):
76 """Creates a hash of the ``name``.
77
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
80 KEY.
81
82 :param name: the name to create a secret hash for
83 :type name: str
84 """
85 m = hmac.new(bytes(name, encoding='utf-8'), digestmod='sha256')
86 m.update(bytes(get_setting('server.secret_key'), encoding='utf-8'))
87 return m.hexdigest()
88
89
90INCR_COUNTER = """
91local limit = tonumber(ARGV[1])
92local expire = tonumber(ARGV[2])
93local c_name = KEYS[1]
94
95local c = redis.call('GET', c_name)
96
97if not c then
98 c = redis.call('INCR', c_name)
99 if expire > 0 then
100 redis.call('EXPIRE', c_name, expire)
101 end
102else
103 c = tonumber(c)
104 if limit == 0 or c < limit then
105 c = redis.call('INCR', c_name)
106 end
107end
108return c
109"""
110
111
112def incr_counter(client, name: str, limit: int = 0, expire: int = 0):
113 """Increment a counter and return the new value.
114
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`).
119
120 The implementation of the redis counter is the lua script from string
121 :py:obj:`INCR_COUNTER`.
122
123 :param name: name of the counter
124 :type name: str
125
126 :param expire: live-time of the counter in seconds (default ``None`` means
127 infinite).
128 :type expire: int / see EXPIRE_
129
130 :param limit: limit where the counter stops to increment (default ``None``)
131 :type limit: int / limit is 2^64 see INCR_
132
133 :return: value of the incremented counter
134 :type return: int
135
136 .. _EXPIRE: https://redis.io/commands/expire/
137 .. _INCR: https://redis.io/commands/incr/
138
139 A simple demo of a counter with expire time and limit::
140
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
144 ...
145 (0, 1)
146 (1, 2)
147 (2, 3)
148 (3, 3)
149 (4, 3)
150 (5, 1)
151
152 """
153 script = lua_script_storage(client, INCR_COUNTER)
154 name = "SearXNG_counter_" + secret_hash(name)
155 c = script(args=[limit, expire], keys=[name])
156 return c
157
158
159def drop_counter(client, name):
160 """Drop counter with redis key ``SearXNG_counter_<name>``
161
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`).
164 """
165 name = "SearXNG_counter_" + secret_hash(name)
166 client.delete(name)
167
168
169INCR_SLIDING_WINDOW = """
170local expire = tonumber(ARGV[1])
171local name = KEYS[1]
172local current_time = redis.call('TIME')
173
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)
178return result
179"""
180
181
182def incr_sliding_window(client, name: str, duration: int):
183 """Increment a sliding-window counter and return the new value.
184
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`).
189
190 :param name: name of the counter
191 :type name: str
192
193 :param duration: live-time of the sliding window in seconds
194 :typeduration: int
195
196 :return: value of the incremented counter
197 :type return: int
198
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_).
205
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.
209
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.
212
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/
220
221 A simple demo of the sliding window::
222
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
226 ...
227 1
228 2
229 3
230 3
231 3
232 >>> time.sleep(3) # wait until expire
233 >>> incr_sliding_window(client, "foo", 3)
234 1
235
236 """
237 script = lua_script_storage(client, INCR_SLIDING_WINDOW)
238 name = "SearXNG_counter_" + secret_hash(name)
239 c = script(args=[duration], keys=[name])
240 return c
purge_by_prefix(client, str prefix="SearXNG_")
Definition redislib.py:52
drop_counter(client, name)
Definition redislib.py:159
secret_hash(str name)
Definition redislib.py:75
incr_sliding_window(client, str name, int duration)
Definition redislib.py:182
lua_script_storage(client, script)
Definition redislib.py:21
incr_counter(client, str name, int limit=0, int expire=0)
Definition redislib.py:112