1"""Implementation of caching solutions.
3- :py:obj:`searx.cache.ExpireCache` and its :py:obj:`searx.cache.ExpireCacheCfg`
8__all__ = [
"ExpireCacheCfg",
"ExpireCacheStats",
"ExpireCache",
"ExpireCacheSQLite"]
11from collections.abc
import Iterator
26from searx
import sqlitedb
27from searx
import logger
28from searx
import get_setting
30log = logger.getChild(
"cache")
34 """Configuration of a :py:obj:`ExpireCache` cache."""
37 """Name of the cache."""
40 """URL of the SQLite DB, the path to the database file. If unset a default
41 DB will be created in `/tmp/sxng_cache_{self.name}.db`"""
43 MAX_VALUE_LEN: int = 1024 * 10
44 """Max length of a *serialized* value."""
46 MAXHOLD_TIME: int = 60 * 60 * 24 * 7
47 """Hold time (default in sec.), after which a value is removed from the cache."""
49 MAINTENANCE_PERIOD: int = 60 * 60
50 """Maintenance period in seconds / when :py:obj:`MAINTENANCE_MODE` is set to
53 MAINTENANCE_MODE: typing.Literal[
"auto",
"off"] =
"auto"
54 """Type of maintenance mode
57 Maintenance is carried out automatically as part of the maintenance
58 intervals (:py:obj:`MAINTENANCE_PERIOD`); no external process is required.
61 Maintenance is switched off and must be carried out by an external process
66 """Password used by :py:obj:`ExpireCache.secret_hash`.
68 The default password is taken from :ref:`secret_key <server.secret_key>`.
69 When the password is changed, the hashed keys in the cache can no longer be
70 used, which is why all values in the cache are deleted when the password is
77 self.
db_url = tempfile.gettempdir() + os.sep + f
"sxng_cache_{ExpireCache.normalize_name(self.name)}.db"
82 """Dataclass which provides information on the status of the cache."""
84 cached_items: dict[str, list[tuple[str, typing.Any, int]]]
85 """Values in the cache mapped by context name.
91 ("foo key": "foo value", <expire>),
92 ("bar key": "bar value", <expire>),
102 lines: list[str] = []
107 lines.append(f
"[{ctx_name:20s}] empty")
110 for key, value, expire
in kv_list:
111 valid_until = datetime.datetime.fromtimestamp(expire).strftime(
"%Y-%m-%d %H:%M:%S")
113 lines.append(f
"[{ctx_name:20s}] {valid_until} {key:12}" f
" --> ({type(value).__name__}) {value} ")
115 lines.append(f
"Number of contexts: {c_ctx}")
116 lines.append(f
"number of key/value pairs: {c_kv}")
117 return "\n".join(lines)
121 """Abstract base class for the implementation of a key/value cache
126 hash_token: str =
"hash_token"
129 def set(self, key: str, value: typing.Any, expire: int |
None, ctx: str |
None =
None) -> bool:
130 """Set *key* to *value*. To set a timeout on key use argument
131 ``expire`` (in sec.). If expire is unset the default is taken from
132 :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`. After the timeout has expired,
133 the key will automatically be deleted.
135 The ``ctx`` argument specifies the context of the ``key``. A key is
136 only unique in its context.
138 The concrete implementations of this abstraction determine how the
139 context is mapped in the connected database. In SQL databases, for
140 example, the context is a DB table or in a Key/Value DB it could be
141 a prefix for the key.
143 If the context is not specified (the default is ``None``) then a
144 default context should be used, e.g. a default table for SQL databases
145 or a default prefix in a Key/Value DB.
149 def get(self, key: str, default: typing.Any =
None, ctx: str |
None =
None) -> typing.Any:
150 """Return *value* of *key*. If key is unset, ``None`` is returned."""
153 def maintenance(self, force: bool =
False, truncate: bool =
False) -> bool:
154 """Performs maintenance on the cache.
157 Maintenance should be carried out even if the maintenance interval has
158 not yet been reached.
161 Truncate the entire cache, which is necessary, for example, if the
162 password has changed.
166 def state(self) -> ExpireCacheStats:
167 """Returns a :py:obj:`ExpireCacheStats`, which provides information
168 about the status of the cache."""
172 """Factory to build a caching instance.
176 Currently, only the SQLite adapter is available, but other database
177 types could be implemented in the future, e.g. a Valkey (Redis)
184 """Returns a normalized name that can be used as a file name or as a SQL
185 table name (is used, for example, to normalize the context name)."""
187 _valid =
"-_." + string.ascii_letters + string.digits
188 return "".join([c
for c
in name
if c
in _valid])
191 dump: bytes = pickle.dumps(value)
195 obj = pickle.loads(value)
199 """Creates a hash of the argument ``name``. The hash value is formed
200 from the ``name`` combined with the :py:obj:`password
201 <ExpireCacheCfg.password>`. Can be used, for example, to make the
202 ``key`` stored in the DB unreadable for third parties."""
204 if isinstance(name, str):
205 name = bytes(name, encoding=
'utf-8')
206 m = hmac.new(name + self.cfg.password, digestmod=
'sha256')
211 """Cache that manages key/value pairs in a SQLite DB. The DB model in the
212 SQLite DB is implemented in abstract class :py:obj:`SQLiteAppl
213 <searx.sqlitedb.SQLiteAppl>`.
215 The following configurations are required / supported:
217 - :py:obj:`ExpireCacheCfg.db_url`
218 - :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`
219 - :py:obj:`ExpireCacheCfg.MAINTENANCE_PERIOD`
220 - :py:obj:`ExpireCacheCfg.MAINTENANCE_MODE`
226 DDL_CREATE_TABLES: dict[str, str] = {}
228 CACHE_TABLE_PREFIX: str =
"CACHE-TABLE"
231 """An instance of the SQLite expire cache is build up from a
232 :py:obj:`config <ExpireCacheCfg>`."""
234 self.
cfg: ExpireCacheCfg = cfg
235 if cfg.db_url ==
":memory:":
236 log.critical(
"don't use SQLite DB in :memory: in production!!")
239 def init(self, conn: sqlite3.Connection) -> bool:
240 ret_val = super().
init(conn)
244 new = hashlib.sha256(self.
cfg.password).hexdigest()
248 log.warning(
"[%s] hash token changed: truncate all cache tables", self.
cfg.name)
254 def maintenance(self, force: bool =
False, truncate: bool =
False) -> bool:
269 expire = int(time.time())
273 res = conn.execute(f
"DELETE FROM {table} WHERE expire < ?", (expire,))
274 log.debug(
"deleted %s keys from table %s (expire date reached)", res.rowcount, table)
279 conn.execute(
"PRAGMA wal_checkpoint(TRUNCATE)")
285 """Create DB ``table`` if it has not yet been created, no recreates are
286 initiated if the table already exists.
292 log.info(
"key/value table '%s' NOT exists in DB -> create DB table ..", table)
293 sql_table =
"\n".join(
295 f
"CREATE TABLE IF NOT EXISTS {table} (",
298 f
" expire INTEGER DEFAULT (strftime('%s', 'now') + {self.cfg.MAXHOLD_TIME}),",
299 "PRIMARY KEY (key))",
302 sql_index = f
"CREATE INDEX IF NOT EXISTS index_expire_{table} ON {table}(expire);"
304 conn.execute(sql_table)
305 conn.execute(sql_index)
308 self.
properties.
set(f
"{self.CACHE_TABLE_PREFIX}-{table}", table)
313 """List of key/value tables already created in the DB."""
314 sql = f
"SELECT value FROM properties WHERE name LIKE '{self.CACHE_TABLE_PREFIX}%%'"
315 rows = self.
DB.execute(sql).fetchall()
or []
316 return [r[0]
for r
in rows]
319 log.debug(
"truncate table: %s",
",".join(table_names))
321 for table
in table_names:
322 conn.execute(f
"DELETE FROM {table}")
328 """Returns (unix epoch) time of the next maintenance."""
330 return self.
cfg.MAINTENANCE_PERIOD + self.
properties.m_time(
"LAST_MAINTENANCE", int(time.time()))
334 def set(self, key: str, value: typing.Any, expire: int |
None, ctx: str |
None =
None) -> bool:
335 """Set key/value in DB table given by argument ``ctx``. If expire is
336 unset the default is taken from :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`.
337 If ``ctx`` argument is ``None`` (the default), a table name is
338 generated from the :py:obj:`ExpireCacheCfg.name`. If DB table does not
339 exists, it will be created (on demand) by :py:obj:`self.create_table
340 <ExpireCacheSQLite.create_table>`.
346 if len(value) > self.
cfg.MAX_VALUE_LEN:
347 log.warning(
"ExpireCache.set(): %s.key='%s' - value too big to cache (len: %s) ", table, value, len(value))
351 expire = self.
cfg.MAXHOLD_TIME
352 expire = int(time.time()) + expire
360 f
"INSERT INTO {table_name} (key, value, expire) VALUES (?, ?, ?)"
362 f
"UPDATE SET value=?, expire=?"
367 self.
DB.execute(sql, (key, value, expire, value, expire))
370 conn.execute(sql, (key, value, expire, value, expire))
375 def get(self, key: str, default: typing.Any =
None, ctx: str |
None =
None) -> typing.Any:
376 """Get value of ``key`` from table given by argument ``ctx``. If
377 ``ctx`` argument is ``None`` (the default), a table name is generated
378 from the :py:obj:`ExpireCacheCfg.name`. If ``key`` not exists (in
379 table), the ``default`` value is returned.
391 sql = f
"SELECT value FROM {table} WHERE key = ?"
392 row = self.
DB.execute(sql, (key,)).fetchone()
398 def pairs(self, ctx: str) -> Iterator[tuple[str, typing.Any]]:
399 """Iterate over key/value pairs from table given by argument ``ctx``.
400 If ``ctx`` argument is ``None`` (the default), a table name is
401 generated from the :py:obj:`ExpireCacheCfg.name`."""
409 for row
in self.
DB.execute(f
"SELECT key, value FROM {table}"):
412 def state(self) -> ExpireCacheStats:
413 cached_items: dict[str, list[tuple[str, typing.Any, int]]] = {}
415 cached_items[table] = []
416 for row
in self.
DB.execute(f
"SELECT key, value, expire FROM {table}"):
417 cached_items[table].append((row[0], self.
deserialize(row[1]), row[2]))
typing.Any get(self, str key, typing.Any default=None, str|None ctx=None)
truncate_tables(self, list[str] table_names)
bool create_table(self, str table)
bool init(self, sqlite3.Connection conn)
__init__(self, ExpireCacheCfg cfg)
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)
Iterator[tuple[str, typing.Any]] pairs(self, str ctx)
bytes serialize(self, typing.Any value)
"ExpireCacheSQLite" build_cache(ExpireCacheCfg cfg)
typing.Any get(self, str key, typing.Any default=None, str|None ctx=None)
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)
SQLiteProperties properties
t.Any get_setting(str name, t.Any default=_unset)