1"""Implementation of caching solutions.
3- :py:obj:`searx.cache.ExpireCache` and its :py:obj:`searx.cache.ExpireCacheCfg`
8from __future__
import annotations
10__all__ = [
"ExpireCacheCfg",
"ExpireCacheStats",
"ExpireCache",
"ExpireCacheSQLite"]
27from searx
import sqlitedb
28from searx
import logger
29from searx
import get_setting
31log = logger.getChild(
"cache")
35 """Configuration of a :py:obj:`ExpireCache` cache."""
38 """Name of the cache."""
41 """URL of the SQLite DB, the path to the database file. If unset a default
42 DB will be created in `/tmp/sxng_cache_{self.name}.db`"""
44 MAX_VALUE_LEN: int = 1024 * 10
45 """Max length of a *serialized* value."""
47 MAXHOLD_TIME: int = 60 * 60 * 24 * 7
48 """Hold time (default in sec.), after which a value is removed from the cache."""
50 MAINTENANCE_PERIOD: int = 60 * 60
51 """Maintenance period in seconds / when :py:obj:`MAINTENANCE_MODE` is set to
54 MAINTENANCE_MODE: typing.Literal[
"auto",
"off"] =
"auto"
55 """Type of maintenance mode
58 Maintenance is carried out automatically as part of the maintenance
59 intervals (:py:obj:`MAINTENANCE_PERIOD`); no external process is required.
62 Maintenance is switched off and must be carried out by an external process
67 """Password used by :py:obj:`ExpireCache.secret_hash`.
69 The default password is taken from :ref:`secret_key <server.secret_key>`.
70 When the password is changed, the hashed keys in the cache can no longer be
71 used, which is why all values in the cache are deleted when the password is
78 self.
db_url = tempfile.gettempdir() + os.sep + f
"sxng_cache_{ExpireCache.normalize_name(self.name)}.db"
83 """Dataclass which provides information on the status of the cache."""
85 cached_items: dict[str, list[tuple[str, typing.Any, int]]]
86 """Values in the cache mapped by context name.
92 ("foo key": "foo value", <expire>),
93 ("bar key": "bar value", <expire>),
108 lines.append(f
"[{ctx_name:20s}] empty")
111 for key, value, expire
in kv_list:
112 valid_until = datetime.datetime.fromtimestamp(expire).strftime(
"%Y-%m-%d %H:%M:%S")
114 lines.append(f
"[{ctx_name:20s}] {valid_until} {key:12}" f
" --> ({type(value).__name__}) {value} ")
116 lines.append(f
"Number of contexts: {c_ctx}")
117 lines.append(f
"number of key/value pairs: {c_kv}")
118 return "\n".join(lines)
122 """Abstract base class for the implementation of a key/value cache
127 hash_token =
"hash_token"
130 def set(self, key: str, value: typing.Any, expire: int |
None, ctx: str |
None =
None) -> bool:
131 """Set *key* to *value*. To set a timeout on key use argument
132 ``expire`` (in sec.). If expire is unset the default is taken from
133 :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`. After the timeout has expired,
134 the key will automatically be deleted.
136 The ``ctx`` argument specifies the context of the ``key``. A key is
137 only unique in its context.
139 The concrete implementations of this abstraction determine how the
140 context is mapped in the connected database. In SQL databases, for
141 example, the context is a DB table or in a Key/Value DB it could be
142 a prefix for the key.
144 If the context is not specified (the default is ``None``) then a
145 default context should be used, e.g. a default table for SQL databases
146 or a default prefix in a Key/Value DB.
150 def get(self, key: str, default=
None, ctx: str |
None =
None) -> typing.Any:
151 """Return *value* of *key*. If key is unset, ``None`` is returned."""
154 def maintenance(self, force: bool =
False, truncate: bool =
False) -> bool:
155 """Performs maintenance on the cache.
158 Maintenance should be carried out even if the maintenance interval has
159 not yet been reached.
162 Truncate the entire cache, which is necessary, for example, if the
163 password has changed.
167 def state(self) -> ExpireCacheStats:
168 """Returns a :py:obj:`ExpireCacheStats`, which provides information
169 about the status of the cache."""
173 """Factory to build a caching instance.
177 Currently, only the SQLite adapter is available, but other database
178 types could be implemented in the future, e.g. a Valkey (Redis)
185 """Returns a normalized name that can be used as a file name or as a SQL
186 table name (is used, for example, to normalize the context name)."""
188 _valid =
"-_." + string.ascii_letters + string.digits
189 return "".join([c
for c
in name
if c
in _valid])
192 dump: bytes = pickle.dumps(value)
196 obj = pickle.loads(value)
200 """Creates a hash of the argument ``name``. The hash value is formed
201 from the ``name`` combined with the :py:obj:`password
202 <ExpireCacheCfg.password>`. Can be used, for example, to make the
203 ``key`` stored in the DB unreadable for third parties."""
205 if isinstance(name, str):
206 name = bytes(name, encoding=
'utf-8')
207 m = hmac.new(name + self.cfg.password, digestmod=
'sha256')
212 """Cache that manages key/value pairs in a SQLite DB. The DB model in the
213 SQLite DB is implemented in abstract class :py:obj:`SQLiteAppl
214 <searx.sqlitedb.SQLiteAppl>`.
216 The following configurations are required / supported:
218 - :py:obj:`ExpireCacheCfg.db_url`
219 - :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`
220 - :py:obj:`ExpireCacheCfg.MAINTENANCE_PERIOD`
221 - :py:obj:`ExpireCacheCfg.MAINTENANCE_MODE`
227 DDL_CREATE_TABLES = {}
229 CACHE_TABLE_PREFIX =
"CACHE-TABLE"
232 """An instance of the SQLite expire cache is build up from a
233 :py:obj:`config <ExpireCacheCfg>`."""
236 if cfg.db_url ==
":memory:":
237 log.critical(
"don't use SQLite DB in :memory: in production!!")
240 def init(self, conn: sqlite3.Connection) -> bool:
241 ret_val = super().
init(conn)
245 new = hashlib.sha256(self.
cfg.password).hexdigest()
249 log.warning(
"[%s] hash token changed: truncate all cache tables", self.
cfg.name)
255 def maintenance(self, force: bool =
False, truncate: bool =
False) -> bool:
270 expire = int(time.time())
274 res = conn.execute(f
"DELETE FROM {table} WHERE expire < ?", (expire,))
275 log.debug(
"deleted %s keys from table %s (expire date reached)", res.rowcount, table)
280 conn.execute(
"PRAGMA wal_checkpoint(TRUNCATE)")
286 """Create DB ``table`` if it has not yet been created, no recreates are
287 initiated if the table already exists.
293 log.info(
"key/value table '%s' NOT exists in DB -> create DB table ..", table)
294 sql_table =
"\n".join(
296 f
"CREATE TABLE IF NOT EXISTS {table} (",
299 f
" expire INTEGER DEFAULT (strftime('%s', 'now') + {self.cfg.MAXHOLD_TIME}),",
300 "PRIMARY KEY (key))",
303 sql_index = f
"CREATE INDEX IF NOT EXISTS index_expire_{table} ON {table}(expire);"
305 conn.execute(sql_table)
306 conn.execute(sql_index)
309 self.
properties.
set(f
"{self.CACHE_TABLE_PREFIX}-{table}", table)
314 """List of key/value tables already created in the DB."""
315 sql = f
"SELECT value FROM properties WHERE name LIKE '{self.CACHE_TABLE_PREFIX}%%'"
316 rows = self.
DB.execute(sql).fetchall()
or []
317 return [r[0]
for r
in rows]
320 log.debug(
"truncate table: %s",
",".join(table_names))
322 for table
in table_names:
323 conn.execute(f
"DELETE FROM {table}")
329 """Returns (unix epoch) time of the next maintenance."""
331 return self.
cfg.MAINTENANCE_PERIOD + self.
properties.m_time(
"LAST_MAINTENANCE", int(time.time()))
335 def set(self, key: str, value: typing.Any, expire: int |
None, ctx: str |
None =
None) -> bool:
336 """Set key/value in DB table given by argument ``ctx``. If expire is
337 unset the default is taken from :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`.
338 If ``ctx`` argument is ``None`` (the default), a table name is
339 generated from the :py:obj:`ExpireCacheCfg.name`. If DB table does not
340 exists, it will be created (on demand) by :py:obj:`self.create_table
341 <ExpireCacheSQLite.create_table>`.
347 if len(value) > self.
cfg.MAX_VALUE_LEN:
348 log.warning(
"ExpireCache.set(): %s.key='%s' - value too big to cache (len: %s) ", table, value, len(value))
352 expire = self.
cfg.MAXHOLD_TIME
353 expire = int(time.time()) + expire
361 f
"INSERT INTO {table_name} (key, value, expire) VALUES (?, ?, ?)"
363 f
"UPDATE SET value=?, expire=?"
368 self.
DB.execute(sql, (key, value, expire, value, expire))
371 conn.execute(sql, (key, value, expire, value, expire))
376 def get(self, key: str, default=
None, ctx: str |
None =
None) -> typing.Any:
377 """Get value of ``key`` from table given by argument ``ctx``. If
378 ``ctx`` argument is ``None`` (the default), a table name is generated
379 from the :py:obj:`ExpireCacheCfg.name`. If ``key`` not exists (in
380 table), the ``default`` value is returned.
392 sql = f
"SELECT value FROM {table} WHERE key = ?"
393 row = self.
DB.execute(sql, (key,)).fetchone()
399 def state(self) -> ExpireCacheStats:
402 cached_items[table] = []
403 for row
in self.
DB.execute(f
"SELECT key, value, expire FROM {table}"):
404 cached_items[table].append((row[0], self.
deserialize(row[1]), row[2]))
truncate_tables(self, list[str] table_names)
bool create_table(self, str table)
bool init(self, sqlite3.Connection conn)
__init__(self, ExpireCacheCfg cfg)
typing.Any get(self, str key, default=None, str|None ctx=None)
bool maintenance(self, bool force=False, bool truncate=False)
bool set(self, str key, typing.Any value, int|None expire, str|None ctx=None)
ExpireCacheStats state(self)
bytes serialize(self, typing.Any value)
ExpireCache build_cache(ExpireCacheCfg cfg)
typing.Any get(self, str key, default=None, str|None ctx=None)
ExpireCacheCfg hash_token
str secret_hash(self, str|bytes name)
str normalize_name(str name)
ExpireCacheStats state(self)
bool maintenance(self, bool force=False, bool truncate=False)
typing.Any deserialize(self, bytes value)
bool set(self, str key, typing.Any value, int|None expire, str|None ctx=None)
sqlite3.Connection connect(self)
get_setting(name, default=_unset)