cache.storage.redis

On this page

RedisStorage

Python
class RedisStorage(BaseStorage)

Overview

Redis-backed storage with connection pooling, used when CacheConfig.storage_backend == StorageBackend.REDIS. Distinctively, this class is self-healing at construction time: if Redis cannot be reached during __init__, it silently constructs an internal SQLiteStorage and routes every subsequent call to it instead, rather than raising.

Constructor

Python
RedisStorage(config: Any, fallback_db: str = "./zerda_fallback.db") -> None
ParameterTypeDefaultMeaning
configRedisConfig-like object (duck-typed, annotated as Any)— requiredMust expose host, port, db, password, ssl, socket_timeout, socket_connect_timeout, max_connections, key_prefix
fallback_dbstr"./zerda_fallback.db"Path used to construct the fallback SQLiteStorage if Redis is unreachable

Construction attempts to:

  1. import redis (deferred import — the redis package is optional at the package level).
  2. Build a redis.ConnectionPool from the fields on config, with decode_responses=True.
  3. ping() the server to confirm connectivity.
  4. On success: store self._redis, self._prefix = config.key_prefix, self._fallback = None.
  5. On any exception (import failure, connection refusal, auth failure, timeout, etc.): log a warning, set self._redis = None, and construct self._fallback = SQLiteStorage(fallback_db). self._prefix becomes "" in this mode.

[!WARNING] Fallback mode is permanent for the lifetime of the instance There is no retry/reconnect logic. Once __init__ falls back to SQLite, that RedisStorage instance will route to SQLite for its entire lifetime — even if Redis becomes reachable again later — since _is_redis_ok() simply checks whether self._redis is not None, which is fixed at construction time.

Key namespacing

Non-fallback keys are prefixed with config.key_prefix (default "zerda:") via the private _fk(key) helper, so multiple RedisStorage instances (or other Redis consumers) can share a Redis database without key collisions. The prefix is not applied when running in fallback mode (SQLite doesn't need it, and self._prefix is "" there).

Methods

All nine BaseStorage methods follow the same pattern: check _is_redis_ok(); if False, delegate to self._fallback; otherwise perform the Redis operation inside a try/except that logs and returns a safe default (None, False, [], or 0) on any exception.

get(key: str) -> Optional[IntelligentCacheEntry]

GET the prefixed key, JSON-decode, and reconstruct via IntelligentCacheEntry.from_storage_dict().

set(key: str, entry: IntelligentCacheEntry, ttl_s: Optional[float] = None) -> None

JSON-serializes the entry via to_storage_dict(). Uses SETEX if ttl_s is truthy (same 0-is-falsy caveat as the other backends), otherwise plain SET. Additionally performs SADD {prefix}tenant:{tenant_id} {key} to maintain a per-tenant index set — this is Redis-specific bookkeeping that MemoryStorage/SQLiteStorage don't need (they can scan directly).

delete(key: str) -> bool

DEL the prefixed key; returns whether at least one key was removed. Does not remove the key from the tenant index set (see Known Issues for the consequence).

exists(key: str) -> bool

EXISTS on the prefixed key.

keys_by_tenant(tenant_id: str) -> List[str]

Reads the {prefix}tenant:{tenant_id} set. For each member, re-checks EXISTS on the actual key — members whose underlying key no longer exists (e.g. expired via TTL, or deleted without index cleanup) are actively SREM'd from the set as a corrective side effect, then excluded from the returned list. This makes keys_by_tenant self-healing for the index, at the cost of one extra EXISTS/SREM round-trip per stale member.

all_keys() -> List[str]

KEYS {prefix}* (a full key-space scan — see Performance below), excluding keys whose suffix starts with tenant: (the index sets themselves).

total_size_bytes() -> int

Returns Redis's own used_memory from INFO memory — this is the entire Redis instance's memory usage, not just this application's keys, if the Redis instance is shared with other consumers.

flush_tenant(tenant_id: str) -> int

Reads the tenant's key set, DELs every underlying key in one call, then DELs the index set itself. Returns the count of keys deleted (from the pre-deletion set contents, not Redis's actual DEL return value for that batch).

close() -> None

Disconnects the connection pool (if in Redis mode) and closes the fallback SQLiteStorage (if one was created) — note that in fallback mode, self._fallback is truthy so its close() is always called, while self._pool.disconnect() is only attempted when _is_redis_ok() was ever True.

Thread Safety

Delegates to the redis-py client's own thread safety (connection pooling is designed for concurrent use) or to SQLiteStorage's thread-local connections in fallback mode.

Side Effects

  • Network requests: every operation in Redis mode is a round-trip to the Redis server.
  • Logging: logger.info on successful connection; logger.warning on fallback; logger.error on any per-operation exception.
  • File operations: creates the fallback SQLite file only if Redis was unreachable at construction time.

Performance Considerations

  • all_keys() uses the blocking KEYS command, which scans the entire Redis keyspace matching the prefix pattern. On a large, shared Redis instance this can be slow and, per Redis's own documentation, may block the server for the duration of the scan. The analyzed source does not use SCAN (the non-blocking, cursor-based alternative).
  • keys_by_tenant() performs one EXISTS call per tenant key on every invocation, which is O(n) round-trips for a tenant with n cached entries.

Common Mistakes

  • Assuming RedisStorage(config) raising nothing means Redis is actually in use — check logs (or add your own health check) since a silent fallback to SQLite is by design.
  • Deleting entries via some other path (e.g. directly through the Redis CLI) and expecting keys_by_tenant/flush_tenant bookkeeping to stay consistent — the tenant index set is only maintained by this class's own set/flush_tenant/keys_by_tenant methods.

Related pages