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"]
13from collections.abc
import Iterator
28from searx
import sqlitedb
29from searx
import logger
30from searx
import get_setting
32log = logger.getChild(
"cache")
36 """Configuration of a :py:obj:`ExpireCache` cache."""
39 """Name of the cache."""
42 """URL of the SQLite DB, the path to the database file. If unset a default
43 DB will be created in `/tmp/sxng_cache_{self.name}.db`"""
45 MAX_VALUE_LEN: int = 1024 * 10
46 """Max length of a *serialized* value."""
48 MAXHOLD_TIME: int = 60 * 60 * 24 * 7
49 """Hold time (default in sec.), after which a value is removed from the cache."""
51 MAINTENANCE_PERIOD: int = 60 * 60
52 """Maintenance period in seconds / when :py:obj:`MAINTENANCE_MODE` is set to
55 MAINTENANCE_MODE: typing.Literal[
"auto",
"off"] =
"auto"
56 """Type of maintenance mode
59 Maintenance is carried out automatically as part of the maintenance
60 intervals (:py:obj:`MAINTENANCE_PERIOD`); no external process is required.
63 Maintenance is switched off and must be carried out by an external process
68 """Password used by :py:obj:`ExpireCache.secret_hash`.
70 The default password is taken from :ref:`secret_key <server.secret_key>`.
71 When the password is changed, the hashed keys in the cache can no longer be
72 used, which is why all values in the cache are deleted when the password is
79 self.
db_url = tempfile.gettempdir() + os.sep + f
"sxng_cache_{ExpireCache.normalize_name(self.name)}.db"
84 """Dataclass which provides information on the status of the cache."""
86 cached_items: dict[str, list[tuple[str, typing.Any, int]]]
87 """Values in the cache mapped by context name.
93 ("foo key": "foo value", <expire>),
94 ("bar key": "bar value", <expire>),
109 lines.append(f
"[{ctx_name:20s}] empty")
112 for key, value, expire
in kv_list:
113 valid_until = datetime.datetime.fromtimestamp(expire).strftime(
"%Y-%m-%d %H:%M:%S")
115 lines.append(f
"[{ctx_name:20s}] {valid_until} {key:12}" f
" --> ({type(value).__name__}) {value} ")
117 lines.append(f
"Number of contexts: {c_ctx}")
118 lines.append(f
"number of key/value pairs: {c_kv}")
119 return "\n".join(lines)
123 """Abstract base class for the implementation of a key/value cache
128 hash_token =
"hash_token"
131 def set(self, key: str, value: typing.Any, expire: int |
None, ctx: str |
None =
None) -> bool:
132 """Set *key* to *value*. To set a timeout on key use argument
133 ``expire`` (in sec.). If expire is unset the default is taken from
134 :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`. After the timeout has expired,
135 the key will automatically be deleted.
137 The ``ctx`` argument specifies the context of the ``key``. A key is
138 only unique in its context.
140 The concrete implementations of this abstraction determine how the
141 context is mapped in the connected database. In SQL databases, for
142 example, the context is a DB table or in a Key/Value DB it could be
143 a prefix for the key.
145 If the context is not specified (the default is ``None``) then a
146 default context should be used, e.g. a default table for SQL databases
147 or a default prefix in a Key/Value DB.
151 def get(self, key: str, default=
None, ctx: str |
None =
None) -> typing.Any:
152 """Return *value* of *key*. If key is unset, ``None`` is returned."""
155 def maintenance(self, force: bool =
False, truncate: bool =
False) -> bool:
156 """Performs maintenance on the cache.
159 Maintenance should be carried out even if the maintenance interval has
160 not yet been reached.
163 Truncate the entire cache, which is necessary, for example, if the
164 password has changed.
168 def state(self) -> ExpireCacheStats:
169 """Returns a :py:obj:`ExpireCacheStats`, which provides information
170 about the status of the cache."""
174 """Factory to build a caching instance.
178 Currently, only the SQLite adapter is available, but other database
179 types could be implemented in the future, e.g. a Valkey (Redis)
186 """Returns a normalized name that can be used as a file name or as a SQL
187 table name (is used, for example, to normalize the context name)."""
189 _valid =
"-_." + string.ascii_letters + string.digits
190 return "".join([c
for c
in name
if c
in _valid])
193 dump: bytes = pickle.dumps(value)
197 obj = pickle.loads(value)
201 """Creates a hash of the argument ``name``. The hash value is formed
202 from the ``name`` combined with the :py:obj:`password
203 <ExpireCacheCfg.password>`. Can be used, for example, to make the
204 ``key`` stored in the DB unreadable for third parties."""
206 if isinstance(name, str):
207 name = bytes(name, encoding=
'utf-8')
208 m = hmac.new(name + self.cfg.password, digestmod=
'sha256')
213 """Cache that manages key/value pairs in a SQLite DB. The DB model in the
214 SQLite DB is implemented in abstract class :py:obj:`SQLiteAppl
215 <searx.sqlitedb.SQLiteAppl>`.
217 The following configurations are required / supported:
219 - :py:obj:`ExpireCacheCfg.db_url`
220 - :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`
221 - :py:obj:`ExpireCacheCfg.MAINTENANCE_PERIOD`
222 - :py:obj:`ExpireCacheCfg.MAINTENANCE_MODE`
228 DDL_CREATE_TABLES = {}
230 CACHE_TABLE_PREFIX =
"CACHE-TABLE"
233 """An instance of the SQLite expire cache is build up from a
234 :py:obj:`config <ExpireCacheCfg>`."""
237 if cfg.db_url ==
":memory:":
238 log.critical(
"don't use SQLite DB in :memory: in production!!")
241 def init(self, conn: sqlite3.Connection) -> bool:
242 ret_val = super().
init(conn)
246 new = hashlib.sha256(self.
cfg.password).hexdigest()
250 log.warning(
"[%s] hash token changed: truncate all cache tables", self.
cfg.name)
256 def maintenance(self, force: bool =
False, truncate: bool =
False) -> bool:
271 expire = int(time.time())
275 res = conn.execute(f
"DELETE FROM {table} WHERE expire < ?", (expire,))
276 log.debug(
"deleted %s keys from table %s (expire date reached)", res.rowcount, table)
281 conn.execute(
"PRAGMA wal_checkpoint(TRUNCATE)")
287 """Create DB ``table`` if it has not yet been created, no recreates are
288 initiated if the table already exists.
294 log.info(
"key/value table '%s' NOT exists in DB -> create DB table ..", table)
295 sql_table =
"\n".join(
297 f
"CREATE TABLE IF NOT EXISTS {table} (",
300 f
" expire INTEGER DEFAULT (strftime('%s', 'now') + {self.cfg.MAXHOLD_TIME}),",
301 "PRIMARY KEY (key))",
304 sql_index = f
"CREATE INDEX IF NOT EXISTS index_expire_{table} ON {table}(expire);"
306 conn.execute(sql_table)
307 conn.execute(sql_index)
310 self.
properties.
set(f
"{self.CACHE_TABLE_PREFIX}-{table}", table)
315 """List of key/value tables already created in the DB."""
316 sql = f
"SELECT value FROM properties WHERE name LIKE '{self.CACHE_TABLE_PREFIX}%%'"
317 rows = self.
DB.execute(sql).fetchall()
or []
318 return [r[0]
for r
in rows]
321 log.debug(
"truncate table: %s",
",".join(table_names))
323 for table
in table_names:
324 conn.execute(f
"DELETE FROM {table}")
330 """Returns (unix epoch) time of the next maintenance."""
332 return self.
cfg.MAINTENANCE_PERIOD + self.
properties.m_time(
"LAST_MAINTENANCE", int(time.time()))
336 def set(self, key: str, value: typing.Any, expire: int |
None, ctx: str |
None =
None) -> bool:
337 """Set key/value in DB table given by argument ``ctx``. If expire is
338 unset the default is taken from :py:obj:`ExpireCacheCfg.MAXHOLD_TIME`.
339 If ``ctx`` argument is ``None`` (the default), a table name is
340 generated from the :py:obj:`ExpireCacheCfg.name`. If DB table does not
341 exists, it will be created (on demand) by :py:obj:`self.create_table
342 <ExpireCacheSQLite.create_table>`.
348 if len(value) > self.
cfg.MAX_VALUE_LEN:
349 log.warning(
"ExpireCache.set(): %s.key='%s' - value too big to cache (len: %s) ", table, value, len(value))
353 expire = self.
cfg.MAXHOLD_TIME
354 expire = int(time.time()) + expire
362 f
"INSERT INTO {table_name} (key, value, expire) VALUES (?, ?, ?)"
364 f
"UPDATE SET value=?, expire=?"
369 self.
DB.execute(sql, (key, value, expire, value, expire))
372 conn.execute(sql, (key, value, expire, value, expire))
377 def get(self, key: str, default=
None, ctx: str |
None =
None) -> typing.Any:
378 """Get value of ``key`` from table given by argument ``ctx``. If
379 ``ctx`` argument is ``None`` (the default), a table name is generated
380 from the :py:obj:`ExpireCacheCfg.name`. If ``key`` not exists (in
381 table), the ``default`` value is returned.
393 sql = f
"SELECT value FROM {table} WHERE key = ?"
394 row = self.
DB.execute(sql, (key,)).fetchone()
400 def pairs(self, ctx: str) -> Iterator[tuple[str, typing.Any]]:
401 """Iterate over key/value pairs from table given by argument ``ctx``.
402 If ``ctx`` argument is ``None`` (the default), a table name is
403 generated from the :py:obj:`ExpireCacheCfg.name`."""
411 for row
in self.
DB.execute(f
"SELECT key, value FROM {table}"):
414 def state(self) -> ExpireCacheStats:
417 cached_items[table] = []
418 for row
in self.
DB.execute(f
"SELECT key, value, expire FROM {table}"):
419 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)
Iterator[tuple[str, typing.Any]] pairs(self, str ctx)
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)