[mod] migrate from Redis to Valkey (#4795)

This patch migrates from `redis==5.2.1` [1] to `valkey==6.1.0` [2].

The migration to valkey is necessary because the company behind Redis has decided
to abandon the open source license. After experiencing a drop in user numbers,
they now want to run it under a dual license again. But this move demonstrates
once again how unreliable the company is and how it treats open source
developers.

To review first, read the docs::

    $ make docs.live

Follow the instructions to remove redis:

- http://0.0.0.0:8000/admin/settings/settings_redis.html

Config and install a local valkey DB:

- http://0.0.0.0:8000/admin/settings/settings_valkey.html

[1] https://pypi.org/project/redis/
[2] https://pypi.org/project/valkey/

Co-authored-by: HLFH <gaspard@dhautefeuille.eu>
Co-authored-by: Markus Heiser <markus.heiser@darmarit.de>
This commit is contained in:
Gaspard d'Hautefeuille
2025-07-09 07:55:37 +02:00
committed by GitHub
parent bd593d0bad
commit f798ddd492
43 changed files with 468 additions and 724 deletions
+4 -4
View File
@@ -12,11 +12,11 @@ from ._helpers import too_many_requests
__all__ = ['dump_request', 'get_network', 'get_real_ip', 'too_many_requests']
redis_client = None
valkey_client = None
cfg = None
def init(_cfg, _redis_client):
global redis_client, cfg # pylint: disable=global-statement
redis_client = _redis_client
def init(_cfg, _valkey_client):
global valkey_client, cfg # pylint: disable=global-statement
valkey_client = _valkey_client
cfg = _cfg
+12 -12
View File
@@ -6,8 +6,8 @@ Method ``ip_limit``
The ``ip_limit`` method counts request from an IP in *sliding windows*. If
there are to many requests in a sliding window, the request is evaluated as a
bot request. This method requires a redis DB and needs a HTTP X-Forwarded-For_
header. To take privacy only the hash value of an IP is stored in the redis DB
bot request. This method requires a valkey DB and needs a HTTP X-Forwarded-For_
header. To take privacy only the hash value of an IP is stored in the valkey DB
and at least for a maximum of 10 minutes.
The :py:obj:`.link_token` method can be used to investigate whether a request is
@@ -46,8 +46,8 @@ import flask
import werkzeug
from searx.extended_types import SXNG_Request
from searx import redisdb
from searx.redislib import incr_sliding_window, drop_counter
from searx import valkeydb
from searx.valkeylib import incr_sliding_window, drop_counter
from . import link_token
from . import config
@@ -97,14 +97,14 @@ def filter_request(
) -> werkzeug.Response | None:
# pylint: disable=too-many-return-statements
redis_client = redisdb.client()
valkey_client = valkeydb.client()
if network.is_link_local and not cfg['botdetection.ip_limit.filter_link_local']:
logger.debug("network %s is link-local -> not monitored by ip_limit method", network.compressed)
return None
if request.args.get('format', 'html') != 'html':
c = incr_sliding_window(redis_client, 'ip_limit.API_WINDOW:' + network.compressed, API_WINDOW)
c = incr_sliding_window(valkey_client, 'ip_limit.API_WINDOW:' + network.compressed, API_WINDOW)
if c > API_MAX:
return too_many_requests(network, "too many request in API_WINDOW")
@@ -114,12 +114,12 @@ def filter_request(
if not suspicious:
# this IP is no longer suspicious: release ip again / delete the counter of this IP
drop_counter(redis_client, 'ip_limit.SUSPICIOUS_IP_WINDOW' + network.compressed)
drop_counter(valkey_client, 'ip_limit.SUSPICIOUS_IP_WINDOW' + network.compressed)
return None
# this IP is suspicious: count requests from this IP
c = incr_sliding_window(
redis_client, 'ip_limit.SUSPICIOUS_IP_WINDOW' + network.compressed, SUSPICIOUS_IP_WINDOW
valkey_client, 'ip_limit.SUSPICIOUS_IP_WINDOW' + network.compressed, SUSPICIOUS_IP_WINDOW
)
if c > SUSPICIOUS_IP_MAX:
logger.error("BLOCK: too many request from %s in SUSPICIOUS_IP_WINDOW (redirect to /)", network)
@@ -127,22 +127,22 @@ def filter_request(
response.headers["Cache-Control"] = "no-store, max-age=0"
return response
c = incr_sliding_window(redis_client, 'ip_limit.BURST_WINDOW' + network.compressed, BURST_WINDOW)
c = incr_sliding_window(valkey_client, 'ip_limit.BURST_WINDOW' + network.compressed, BURST_WINDOW)
if c > BURST_MAX_SUSPICIOUS:
return too_many_requests(network, "too many request in BURST_WINDOW (BURST_MAX_SUSPICIOUS)")
c = incr_sliding_window(redis_client, 'ip_limit.LONG_WINDOW' + network.compressed, LONG_WINDOW)
c = incr_sliding_window(valkey_client, 'ip_limit.LONG_WINDOW' + network.compressed, LONG_WINDOW)
if c > LONG_MAX_SUSPICIOUS:
return too_many_requests(network, "too many request in LONG_WINDOW (LONG_MAX_SUSPICIOUS)")
return None
# vanilla limiter without extensions counts BURST_MAX and LONG_MAX
c = incr_sliding_window(redis_client, 'ip_limit.BURST_WINDOW' + network.compressed, BURST_WINDOW)
c = incr_sliding_window(valkey_client, 'ip_limit.BURST_WINDOW' + network.compressed, BURST_WINDOW)
if c > BURST_MAX:
return too_many_requests(network, "too many request in BURST_WINDOW (BURST_MAX)")
c = incr_sliding_window(redis_client, 'ip_limit.LONG_WINDOW' + network.compressed, LONG_WINDOW)
c = incr_sliding_window(valkey_client, 'ip_limit.LONG_WINDOW' + network.compressed, LONG_WINDOW)
if c > LONG_MAX:
return too_many_requests(network, "too many request in LONG_WINDOW (LONG_MAX)")
+16 -16
View File
@@ -10,7 +10,7 @@ a ping by request a static URL.
.. note::
This method requires a redis DB and needs a HTTP X-Forwarded-For_ header.
This method requires a valkey DB and needs a HTTP X-Forwarded-For_ header.
To get in use of this method a flask URL route needs to be added:
@@ -45,8 +45,8 @@ import string
import random
from searx import logger
from searx import redisdb
from searx.redislib import secret_hash
from searx import valkeydb
from searx.valkeylib import secret_hash
from searx.extended_types import SXNG_Request
from ._helpers import (
@@ -76,17 +76,17 @@ def is_suspicious(network: IPv4Network | IPv6Network, request: SXNG_Request, ren
:py:obj:`PING_LIVE_TIME`.
"""
redis_client = redisdb.client()
if not redis_client:
valkey_client = valkeydb.client()
if not valkey_client:
return False
ping_key = get_ping_key(network, request)
if not redis_client.get(ping_key):
if not valkey_client.get(ping_key):
logger.info("missing ping (IP: %s) / request: %s", network.compressed, ping_key)
return True
if renew:
redis_client.set(ping_key, 1, ex=PING_LIVE_TIME)
valkey_client.set(ping_key, 1, ex=PING_LIVE_TIME)
logger.debug("found ping for (client) network %s -> %s", network.compressed, ping_key)
return False
@@ -98,9 +98,9 @@ def ping(request: SXNG_Request, token: str):
The expire time of this ping-key is :py:obj:`PING_LIVE_TIME`.
"""
from . import redis_client, cfg # pylint: disable=import-outside-toplevel, cyclic-import
from . import valkey_client, cfg # pylint: disable=import-outside-toplevel, cyclic-import
if not redis_client:
if not valkey_client:
return
if not token_is_valid(token):
return
@@ -110,7 +110,7 @@ def ping(request: SXNG_Request, token: str):
ping_key = get_ping_key(network, request)
logger.debug("store ping_key for (client) network %s (IP %s) -> %s", network.compressed, real_ip, ping_key)
redis_client.set(ping_key, 1, ex=PING_LIVE_TIME)
valkey_client.set(ping_key, 1, ex=PING_LIVE_TIME)
def get_ping_key(network: IPv4Network | IPv6Network, request: SXNG_Request) -> str:
@@ -134,21 +134,21 @@ def token_is_valid(token) -> bool:
def get_token() -> str:
"""Returns current token. If there is no currently active token a new token
is generated randomly and stored in the redis DB.
is generated randomly and stored in the valkey DB.
- :py:obj:`TOKEN_LIVE_TIME`
- :py:obj:`TOKEN_KEY`
"""
redis_client = redisdb.client()
if not redis_client:
# This function is also called when limiter is inactive / no redis DB
valkey_client = valkeydb.client()
if not valkey_client:
# This function is also called when limiter is inactive / no valkey DB
# (see render function in webapp.py)
return '12345678'
token = redis_client.get(TOKEN_KEY)
token = valkey_client.get(TOKEN_KEY)
if token:
token = token.decode('UTF-8')
else:
token = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))
redis_client.set(TOKEN_KEY, token, ex=TOKEN_LIVE_TIME)
valkey_client.set(TOKEN_KEY, token, ex=TOKEN_LIVE_TIME)
return token
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Redis is an open source (BSD licensed), in-memory data structure (key value
based) store. Before configuring the ``redis_server`` engine, you must install
the dependency redis_.
"""Valkey is an open source (BSD licensed), in-memory data structure (key value
based) store. Before configuring the ``valkey_server`` engine, you must install
the dependency valkey_.
Configuration
=============
@@ -17,11 +17,11 @@ Below is an example configuration:
.. code:: yaml
# Required dependency: redis
# Required dependency: valkey
- name: myredis
- name: myvalkey
shortcut : rds
engine: redis_server
engine: valkey_server
exact_match_only: false
host: '127.0.0.1'
port: 6379
@@ -34,13 +34,13 @@ Implementations
"""
import redis # pylint: disable=import-error
import valkey # pylint: disable=import-error
from searx.result_types import EngineResults
engine_type = 'offline'
# redis connection variables
# valkey connection variables
host = '127.0.0.1'
port = 6379
password = ''
@@ -50,12 +50,12 @@ db = 0
paging = False
exact_match_only = True
_redis_client = None
_valkey_client = None
def init(_engine_settings):
global _redis_client # pylint: disable=global-statement
_redis_client = redis.StrictRedis(
global _valkey_client # pylint: disable=global-statement
_valkey_client = valkey.StrictValkey(
host=host,
port=port,
db=db,
@@ -72,28 +72,28 @@ def search(query, _params) -> EngineResults:
res.add(res.types.KeyValue(kvmap=kvmap))
return res
kvmap: dict[str, str] = _redis_client.hgetall(query)
kvmap: dict[str, str] = _valkey_client.hgetall(query)
if kvmap:
res.add(res.types.KeyValue(kvmap=kvmap))
elif " " in query:
qset, rest = query.split(" ", 1)
for row in _redis_client.hscan_iter(qset, match='*{}*'.format(rest)):
for row in _valkey_client.hscan_iter(qset, match='*{}*'.format(rest)):
res.add(res.types.KeyValue(kvmap={row[0]: row[1]}))
return res
def search_keys(query) -> list[dict]:
ret = []
for key in _redis_client.scan_iter(match='*{}*'.format(query)):
key_type = _redis_client.type(key)
for key in _valkey_client.scan_iter(match='*{}*'.format(query)):
key_type = _valkey_client.type(key)
res = None
if key_type == 'hash':
res = _redis_client.hgetall(key)
res = _valkey_client.hgetall(key)
elif key_type == 'list':
res = dict(enumerate(_redis_client.lrange(key, 0, -1)))
res = dict(enumerate(_valkey_client.lrange(key, 0, -1)))
if res:
res['redis_key'] = key
res['valkey_key'] = key
ret.append(res)
return ret
+11 -11
View File
@@ -17,7 +17,7 @@ from the :ref:`botdetection`:
the time.
- Detection & dynamically :ref:`botdetection rate limit` of bots based on the
behavior of the requests. For dynamically changeable IP lists a Redis
behavior of the requests. For dynamically changeable IP lists a Valkey
database is needed.
The prerequisite for IP based methods is the correct determination of the IP of
@@ -50,13 +50,13 @@ To enable the limiter activate:
...
limiter: true # rate limit the number of request on the instance, block some bots
and set the redis-url connection. Check the value, it depends on your redis DB
(see :ref:`settings redis`), by example:
and set the valkey-url connection. Check the value, it depends on your valkey DB
(see :ref:`settings valkey`), by example:
.. code:: yaml
redis:
url: unix:///usr/local/searxng-redis/run/redis.sock?db=0
valkey:
url: valkey://localhost:6379/0
Configure Limiter
@@ -102,7 +102,7 @@ import werkzeug
from searx import (
logger,
redisdb,
valkeydb,
)
from searx import botdetection
from searx.extended_types import SXNG_Request, sxng_request
@@ -217,7 +217,7 @@ def pre_request():
def is_installed():
"""Returns ``True`` if limiter is active and a redis DB is available."""
"""Returns ``True`` if limiter is active and a valkey DB is available."""
return _INSTALLED
@@ -229,15 +229,15 @@ def initialize(app: flask.Flask, settings):
# (e.g. the self_info plugin uses the botdetection to get client IP)
cfg = get_cfg()
redis_client = redisdb.client()
botdetection.init(cfg, redis_client)
valkey_client = valkeydb.client()
botdetection.init(cfg, valkey_client)
if not (settings['server']['limiter'] or settings['server']['public_instance']):
return
if not redis_client:
if not valkey_client:
logger.error(
"The limiter requires Redis, please consult the documentation: "
"The limiter requires Valkey, please consult the documentation: "
"https://docs.searxng.org/admin/searx.limiter.html"
)
if settings['server']['public_instance']:
-69
View File
@@ -1,69 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementation of the redis client (redis-py_).
.. _redis-py: https://github.com/redis/redis-py
This implementation uses the :ref:`settings redis` setup from ``settings.yml``.
A redis DB connect can be tested by::
>>> from searx import redisdb
>>> redisdb.initialize()
True
>>> db = redisdb.client()
>>> db.set("foo", "bar")
True
>>> db.get("foo")
b'bar'
>>>
"""
import os
import pwd
import logging
import redis
from searx import get_setting
OLD_REDIS_URL_DEFAULT_URL = 'unix:///usr/local/searxng-redis/run/redis.sock?db=0'
"""This was the default Redis URL in settings.yml."""
_CLIENT = None
logger = logging.getLogger(__name__)
def client() -> redis.Redis:
return _CLIENT
def initialize():
global _CLIENT # pylint: disable=global-statement
redis_url = get_setting('redis.url')
if not redis_url:
return False
try:
# create a client, but no connection is done
_CLIENT = redis.Redis.from_url(redis_url)
# log the parameters as seen by the redis lib, without the password
kwargs = _CLIENT.get_connection_kwargs().copy()
kwargs.pop('password', None)
kwargs = ' '.join([f'{k}={v!r}' for k, v in kwargs.items()])
logger.info("connecting to Redis %s", kwargs)
# check the connection
_CLIENT.ping()
# no error: the redis connection is working
logger.info("connected to Redis")
return True
except redis.exceptions.RedisError as e:
_CLIENT = None
_pw = pwd.getpwuid(os.getuid())
logger.exception("[%s (%s)] can't connect redis DB ...", _pw.pw_name, _pw.pw_uid)
if redis_url == OLD_REDIS_URL_DEFAULT_URL and isinstance(e, redis.exceptions.ConnectionError):
logger.info(
"You can safely ignore the above Redis error if you don't use Redis. "
"You can remove this error by setting redis.url to false in your settings.yml."
)
return False
+17 -17
View File
@@ -8,18 +8,18 @@ import os
import signal
from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
import redis.exceptions
import valkey.exceptions
from searx import logger, settings, sxng_debug
from searx.redisdb import client as get_redis_client
from searx.valkeydb import client as get_valkey_client
from searx.exceptions import SearxSettingsException
from searx.search.processors import PROCESSORS
from searx.search.checker import Checker
from searx.search.checker.scheduler import scheduler_function
REDIS_RESULT_KEY = 'SearXNG_checker_result'
REDIS_LOCK_KEY = 'SearXNG_checker_lock'
VALKEY_RESULT_KEY = 'SearXNG_checker_result'
VALKEY_LOCK_KEY = 'SearXNG_checker_lock'
CheckerResult = Union['CheckerOk', 'CheckerErr', 'CheckerOther']
@@ -77,23 +77,23 @@ def _get_interval(every: Any, error_msg: str) -> Tuple[int, int]:
def get_result() -> CheckerResult:
client = get_redis_client()
client = get_valkey_client()
if client is None:
# without Redis, the checker is disabled
# without Valkey, the checker is disabled
return {'status': 'disabled'}
serialized_result: Optional[bytes] = client.get(REDIS_RESULT_KEY)
serialized_result: Optional[bytes] = client.get(VALKEY_RESULT_KEY)
if serialized_result is None:
# the Redis key does not exist
# the Valkey key does not exist
return {'status': 'unknown'}
return json.loads(serialized_result)
def _set_result(result: CheckerResult):
client = get_redis_client()
client = get_valkey_client()
if client is None:
# without Redis, the function does nothing
# without Valkey, the function does nothing
return
client.set(REDIS_RESULT_KEY, json.dumps(result))
client.set(VALKEY_RESULT_KEY, json.dumps(result))
def _timestamp():
@@ -102,9 +102,9 @@ def _timestamp():
def run():
try:
# use a Redis lock to make sure there is no checker running at the same time
# use a Valkey lock to make sure there is no checker running at the same time
# (this should not happen, this is a safety measure)
with get_redis_client().lock(REDIS_LOCK_KEY, blocking_timeout=60, timeout=3600):
with get_valkey_client().lock(VALKEY_LOCK_KEY, blocking_timeout=60, timeout=3600):
logger.info('Starting checker')
result: CheckerOk = {'status': 'ok', 'engines': {}, 'timestamp': _timestamp()}
for name, processor in PROCESSORS.items():
@@ -118,7 +118,7 @@ def run():
_set_result(result)
logger.info('Check done')
except redis.exceptions.LockError:
except valkey.exceptions.LockError:
_set_result({'status': 'error', 'timestamp': _timestamp()})
logger.exception('Error while running the checker')
except Exception: # pylint: disable=broad-except
@@ -149,9 +149,9 @@ def initialize():
logger.info('Checker scheduler is disabled')
return
# make sure there is a Redis connection
if get_redis_client() is None:
logger.error('The checker requires Redis')
# make sure there is a Valkey connection
if get_valkey_client() is None:
logger.error('The checker requires Valkey')
return
# start the background scheduler
+6 -6
View File
@@ -2,9 +2,9 @@
--
-- This script is not a string in scheduler.py, so editors can provide syntax highlighting.
-- The Redis KEY is defined here and not in Python on purpose:
-- The Valkey KEY is defined here and not in Python on purpose:
-- only this LUA script can read and update this key to avoid lock and concurrency issues.
local redis_key = 'SearXNG_checker_next_call_ts'
local valkey_key = 'SearXNG_checker_next_call_ts'
local now = redis.call('TIME')[1]
local start_after_from = ARGV[1]
@@ -12,14 +12,14 @@ local start_after_to = ARGV[2]
local every_from = ARGV[3]
local every_to = ARGV[4]
local next_call_ts = redis.call('GET', redis_key)
local next_call_ts = redis.call('GET', valkey_key)
if (next_call_ts == false or next_call_ts == nil) then
-- the scheduler has never run on this Redis instance, so:
-- the scheduler has never run on this Valkey instance, so:
-- 1/ the scheduler does not run now
-- 2/ the next call is a random time between start_after_from and start_after_to
local initial_delay = math.random(start_after_from, start_after_to)
redis.call('SET', redis_key, now + initial_delay)
redis.call('SET', valkey_key, now + initial_delay)
return { false, initial_delay }
end
@@ -31,6 +31,6 @@ if call_now then
-- the checker runs now, define the timestamp of the next call:
-- this is a random delay between every_from and every_to
local periodic_delay = math.random(every_from, every_to)
next_call_ts = redis.call('INCRBY', redis_key, periodic_delay)
next_call_ts = redis.call('INCRBY', valkey_key, periodic_delay)
end
return { call_now, next_call_ts - now }
+10 -10
View File
@@ -1,11 +1,11 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring
"""Lame scheduler which use Redis as a source of truth:
* the Redis key SearXNG_checker_next_call_ts contains the next time the embedded checker should run.
* to avoid lock, a unique Redis script reads and updates the Redis key SearXNG_checker_next_call_ts.
* this Redis script returns a list of two elements:
"""Lame scheduler which use Valkey as a source of truth:
* the Valkey key SearXNG_checker_next_call_ts contains the next time the embedded checker should run.
* to avoid lock, a unique Valkey script reads and updates the Valkey key SearXNG_checker_next_call_ts.
* this Valkey script returns a list of two elements:
* the first one is a boolean. If True, the embedded checker must run now in this worker.
* the second element is the delay in second to wait before the next call to the Redis script.
* the second element is the delay in second to wait before the next call to the Valkey script.
This scheduler is not generic on purpose: if more feature are required, a dedicate scheduler must be used
(= a better scheduler should not use the web workers)
@@ -16,8 +16,8 @@ import time
from pathlib import Path
from typing import Callable
from searx.redisdb import client as get_redis_client
from searx.redislib import lua_script_storage
from searx.valkeydb import client as get_valkey_client
from searx.valkeylib import lua_script_storage
logger = logging.getLogger('searx.search.checker')
@@ -29,7 +29,7 @@ def scheduler_function(start_after_from: int, start_after_to: int, every_from: i
"""Run the checker periodically. The function never returns.
Parameters:
* start_after_from and start_after_to: when to call "callback" for the first on the Redis instance
* start_after_from and start_after_to: when to call "callback" for the first on the Valkey instance
* every_from and every_to: after the first call, how often to call "callback"
There is no issue:
@@ -38,11 +38,11 @@ def scheduler_function(start_after_from: int, start_after_to: int, every_from: i
"""
scheduler_now_script = SCHEDULER_LUA.open().read()
while True:
# ask the Redis script what to do
# ask the Valkey script what to do
# the script says
# * if the checker must run now.
# * how to long to way before calling the script again (it can be call earlier, but not later).
script = lua_script_storage(get_redis_client(), scheduler_now_script)
script = lua_script_storage(get_valkey_client(), scheduler_now_script)
call_now, wait_time = script(args=[start_after_from, start_after_to, every_from, every_to])
# does the worker run the checker now?
+7 -6
View File
@@ -110,9 +110,10 @@ server:
X-Robots-Tag: noindex, nofollow
Referrer-Policy: no-referrer
redis:
# URL to connect redis database. Is overwritten by ${SEARXNG_REDIS_URL}.
# https://docs.searxng.org/admin/settings/settings_redis.html#settings-redis
valkey:
# URL to connect valkey database. Is overwritten by ${SEARXNG_VALKEY_URL}.
# https://docs.searxng.org/admin/settings/settings_valkey.html#settings-valkey
# url: valkey://localhost:6379/0
url: false
ui:
@@ -1809,10 +1810,10 @@ engines:
shortcut: rt
disabled: true
# Required dependency: redis
# - name: myredis
# Required dependency: valkey
# - name: myvalkey
# shortcut : rds
# engine: redis_server
# engine: valkey_server
# exact_match_only: false
# host: '127.0.0.1'
# port: 6379
+4
View File
@@ -185,9 +185,13 @@ SCHEMA = {
'method': SettingsValue(('POST', 'GET'), 'POST', 'SEARXNG_METHOD'),
'default_http_headers': SettingsValue(dict, {}),
},
# redis is deprecated ..
'redis': {
'url': SettingsValue((None, False, str), False, 'SEARXNG_REDIS_URL'),
},
'valkey': {
'url': SettingsValue((None, False, str), False, 'SEARXNG_VALKEY_URL'),
},
'ui': {
'static_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'static')),
'static_use_hash': SettingsValue(bool, False, 'SEARXNG_STATIC_USE_HASH'),
+65
View File
@@ -0,0 +1,65 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementation of the valkey client (valkey-py_).
.. _valkey-py: https://github.com/valkey-io/valkey-py
This implementation uses the :ref:`settings valkey` setup from ``settings.yml``.
A valkey DB connect can be tested by::
>>> from searx import valkeydb
>>> valkeydb.initialize()
True
>>> db = valkeydb.client()
>>> db.set("foo", "bar")
True
>>> db.get("foo")
b'bar'
>>>
"""
import os
import pwd
import logging
import warnings
import valkey
from searx import get_setting
_CLIENT = None
logger = logging.getLogger(__name__)
def client() -> valkey.Valkey:
return _CLIENT
def initialize():
global _CLIENT # pylint: disable=global-statement
if get_setting('redis.url'):
warnings.warn("setting redis.url is deprecated, use valkey.url", DeprecationWarning)
valkey_url = get_setting('valkey.url') or get_setting('redis.url')
if not valkey_url:
return False
try:
# create a client, but no connection is done
_CLIENT = valkey.Valkey.from_url(valkey_url)
# log the parameters as seen by the valkey lib, without the password
kwargs = _CLIENT.get_connection_kwargs().copy()
kwargs.pop('password', None)
kwargs = ' '.join([f'{k}={v!r}' for k, v in kwargs.items()])
logger.info("connecting to Valkey %s", kwargs)
# check the connection
_CLIENT.ping()
# no error: the valkey connection is working
logger.info("connected to Valkey")
return True
except valkey.exceptions.ValkeyError:
_CLIENT = None
_pw = pwd.getpwuid(os.getuid())
logger.exception("[%s (%s)] can't connect valkey DB ...", _pw.pw_name, _pw.pw_uid)
return False
+26 -26
View File
@@ -1,10 +1,10 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""A collection of convenient functions and redis/lua scripts.
"""A collection of convenient functions and valkey/lua scripts.
This code was partial inspired by the `Bullet-Proofing Lua Scripts in RedisPy`_
This code was partial inspired by the `Bullet-Proofing Lua Scripts in ValkeyPy`_
article.
.. _Bullet-Proofing Lua Scripts in RedisPy:
.. _Bullet-Proofing Lua Scripts in ValkeyPy:
https://redis.com/blog/bullet-proofing-lua-scripts-in-redispy/
"""
@@ -19,8 +19,8 @@ LUA_SCRIPT_STORAGE = {}
def lua_script_storage(client, script):
"""Returns a redis :py:obj:`Script
<redis.commands.core.CoreCommands.register_script>` instance.
"""Returns a valkey :py:obj:`Script
<valkey.commands.core.CoreCommands.register_script>` instance.
Due to performance reason the ``Script`` object is instantiated only once
for a client (``client.register_script(..)``) and is cached in
@@ -28,7 +28,7 @@ def lua_script_storage(client, script):
"""
# redis connection can be closed, lets use the id() of the redis connector
# valkey connection can be closed, lets use the id() of the valkey connector
# as key in the script-storage:
client_id = id(client)
@@ -64,8 +64,8 @@ def purge_by_prefix(client, prefix: str = "SearXNG_"):
:param prefix: prefix of the key to delete (default: ``SearXNG_``)
:type name: str
.. _EXPIRE: https://redis.io/commands/expire/
.. _DEL: https://redis.io/commands/del/
.. _EXPIRE: https://valkey.io/commands/expire/
.. _DEL: https://valkey.io/commands/del/
"""
script = lua_script_storage(client, PURGE_BY_PREFIX)
@@ -76,7 +76,7 @@ def secret_hash(name: str):
"""Creates a hash of the ``name``.
Combines argument ``name`` with the ``secret_key`` from :ref:`settings
server`. This function can be used to get a more anonymized name of a Redis
server`. This function can be used to get a more anonymized name of a Valkey
KEY.
:param name: the name to create a secret hash for
@@ -112,12 +112,12 @@ return c
def incr_counter(client, name: str, limit: int = 0, expire: int = 0):
"""Increment a counter and return the new value.
If counter with redis key ``SearXNG_counter_<name>`` does not exists it is
If counter with valkey key ``SearXNG_counter_<name>`` does not exists it is
created with initial value 1 returned. The replacement ``<name>`` is a
*secret hash* of the value from argument ``name`` (see
:py:func:`secret_hash`).
The implementation of the redis counter is the lua script from string
The implementation of the valkey counter is the lua script from string
:py:obj:`INCR_COUNTER`.
:param name: name of the counter
@@ -133,8 +133,8 @@ def incr_counter(client, name: str, limit: int = 0, expire: int = 0):
:return: value of the incremented counter
:type return: int
.. _EXPIRE: https://redis.io/commands/expire/
.. _INCR: https://redis.io/commands/incr/
.. _EXPIRE: https://valkey.io/commands/expire/
.. _INCR: https://valkey.io/commands/incr/
A simple demo of a counter with expire time and limit::
@@ -157,7 +157,7 @@ def incr_counter(client, name: str, limit: int = 0, expire: int = 0):
def drop_counter(client, name):
"""Drop counter with redis key ``SearXNG_counter_<name>``
"""Drop counter with valkey key ``SearXNG_counter_<name>``
The replacement ``<name>`` is a *secret hash* of the value from argument
``name`` (see :py:func:`incr_counter` and :py:func:`incr_sliding_window`).
@@ -182,7 +182,7 @@ return result
def incr_sliding_window(client, name: str, duration: int):
"""Increment a sliding-window counter and return the new value.
If counter with redis key ``SearXNG_counter_<name>`` does not exists it is
If counter with valkey key ``SearXNG_counter_<name>`` does not exists it is
created with initial value 1 returned. The replacement ``<name>`` is a
*secret hash* of the value from argument ``name`` (see
:py:func:`secret_hash`).
@@ -196,27 +196,27 @@ def incr_sliding_window(client, name: str, duration: int):
:return: value of the incremented counter
:type return: int
The implementation of the redis counter is the lua script from string
:py:obj:`INCR_SLIDING_WINDOW`. The lua script uses `sorted sets in Redis`_
to implement a sliding window for the redis key ``SearXNG_counter_<name>``
The implementation of the valkey counter is the lua script from string
:py:obj:`INCR_SLIDING_WINDOW`. The lua script uses `sorted sets in Valkey`_
to implement a sliding window for the valkey key ``SearXNG_counter_<name>``
(ZADD_). The current TIME_ is used to score the items in the sorted set and
the time window is moved by removing items with a score lower current time
minus *duration* time (ZREMRANGEBYSCORE_).
The EXPIRE_ time (the duration of the sliding window) is refreshed on each
call (increment) and if there is no call in this duration, the sorted
set expires from the redis DB.
set expires from the valkey DB.
The return value is the amount of items in the sorted set (ZCOUNT_), what
means the number of calls in the sliding window.
.. _Sorted sets in Redis:
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/
.. _TIME: https://redis.io/commands/time/
.. _ZADD: https://redis.io/commands/zadd/
.. _EXPIRE: https://redis.io/commands/expire/
.. _ZREMRANGEBYSCORE: https://redis.io/commands/zremrangebyscore/
.. _ZCOUNT: https://redis.io/commands/zcount/
.. _Sorted sets in Valkey:
https://valkey.com/ebook/part-1-getting-started/chapter-1-getting-to-know-valkey/1-2-what-valkey-data-structures-look-like/1-2-5-sorted-sets-in-valkey/
.. _TIME: https://valkey.io/commands/time/
.. _ZADD: https://valkey.io/commands/zadd/
.. _EXPIRE: https://valkey.io/commands/expire/
.. _ZREMRANGEBYSCORE: https://valkey.io/commands/zremrangebyscore/
.. _ZCOUNT: https://valkey.io/commands/zcount/
A simple demo of the sliding window::
+2 -2
View File
@@ -118,7 +118,7 @@ from searx.locales import (
from searx.autocomplete import search_autocomplete, backends as autocomplete_backends
from searx import favicons
from searx.redisdb import initialize as redis_initialize
from searx.valkeydb import initialize as valkey_initialize
from searx.sxng_locales import sxng_locales
import searx.search
from searx.network import stream as http_stream, set_context_network_name
@@ -1397,7 +1397,7 @@ def init():
return
locales_initialize()
redis_initialize()
valkey_initialize()
searx.plugins.initialize(app)
metrics: bool = get_setting("general.enable_metrics") # type: ignore