cache.storage.redis
On this page
RedisStorage
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
RedisStorage(config: Any, fallback_db: str = "./zerda_fallback.db") -> None
Construction attempts to:
import redis(deferred import — theredispackage is optional at the package level).- Build a
redis.ConnectionPoolfrom the fields onconfig, withdecode_responses=True. ping()the server to confirm connectivity.- On success: store
self._redis,self._prefix = config.key_prefix,self._fallback = None. - On any exception (import failure, connection refusal, auth failure, timeout, etc.):
log a warning, set
self._redis = None, and constructself._fallback = SQLiteStorage(fallback_db).self._prefixbecomes""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, thatRedisStorageinstance will route to SQLite for its entire lifetime — even if Redis becomes reachable again later — since_is_redis_ok()simply checks whetherself._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.infoon successful connection;logger.warningon fallback;logger.erroron 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 blockingKEYScommand, 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 useSCAN(the non-blocking, cursor-based alternative).keys_by_tenant()performs oneEXISTScall 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_tenantbookkeeping to stay consistent — the tenant index set is only maintained by this class's ownset/flush_tenant/keys_by_tenantmethods.
Related APIs
BaseStorage— interface implementedSQLiteStorage— the fallback implementationbuild_storage()— constructs this class forStorageBackend.REDIS