Cache with Redis or disk¶
The CacheLayer store is a structural four-method protocol: get, set,
delete, and delete_match. Use Cashews
directly with async storix, or adapt an existing cache client in a few methods.
Cashews with async storix¶
cashews.Cache already has the asynchronous method shapes required by
storix.aio.CacheStore, so it needs no adapter. Configure it with Redis for a
cache shared across processes or with disk storage for a cache that survives
restarts:
"""Cache with Redis (or disk) through Cashews and async storix.
CacheLayer's store is a small cashews-shaped protocol (get / set / delete /
delete_match). A cashews.Cache plugs directly into the async CacheLayer. Point
it at Redis for a cache shared across processes, or at a disk backend for one
that survives restarts. Keys are namespaced, so environments never collide in
a shared store.
"""
from __future__ import annotations
import asyncio
from cashews import Cache
from storix.aio import CacheLayer, cache, get_storage
store = Cache()
store.setup('redis://localhost:6379') # or 'disk://.cache' for a local disk cache
async def main() -> None:
fs = get_storage('local', base='~/storix-data/cache-recipe').with_layer(
CacheLayer,
store=store, # Cashews satisfies the async CacheStore protocol as-is.
du=cache(ttl=60),
read=cache(max_bytes=8 * 1024 * 1024),
environment='prod',
)
try:
await fs.echo('cached content', '/example.txt')
await fs.cat('/example.txt') # fills the content cache
await fs.cat('/example.txt') # served from the content cache
finally:
await fs.close()
await store.close()
if __name__ == '__main__':
asyncio.run(main())
Cashews is asynchronous. Its object does not directly satisfy the synchronous
storix.CacheStore, whose methods are called rather than awaited.
Raw redis-py with sync storix¶
The raw redis-py API
uses different names and signatures (ex/px, no default, and no
delete_match). If your project already uses it, this small adapter is the
complete boundary:
"""Adapt the synchronous redis-py client to storix CacheStore.
CacheLayer stores Python objects, not only byte strings. This compact example
uses pickle and therefore assumes Redis is private and trusted. Use a safer
application-specific serializer when cache writers are not fully trusted.
"""
from __future__ import annotations
import math
import pickle # noqa: S403 - trusted-cache example, documented above
from typing import Any
from redis import Redis
from storix import CacheLayer, cache, get_storage
class RedisStore:
"""The four synchronous methods required by storix CacheStore."""
def __init__(self, client: Redis) -> None:
self._client = client
def get(self, key: str, default: Any = None) -> Any:
payload = self._client.get(key)
if payload is None:
return default
return pickle.loads(payload) # noqa: S301 - trusted-cache example
def set(self, key: str, value: Any, *, expire: float | None = None) -> None:
ttl_ms = None if expire is None else max(1, math.ceil(expire * 1000))
self._client.set(key, pickle.dumps(value), px=ttl_ms)
def delete(self, key: str) -> None:
self._client.delete(key)
def delete_match(self, pattern: str) -> None:
batch_size = 500
keys: list[bytes] = []
for key in self._client.scan_iter(match=pattern, count=batch_size):
keys.append(key)
if len(keys) == batch_size:
self._client.unlink(*keys)
keys.clear()
if keys:
self._client.unlink(*keys)
def main() -> None:
client = Redis.from_url('redis://localhost:6379')
fs = get_storage('local', base='~/storix-data/cache-recipe').with_layer(
CacheLayer,
store=RedisStore(client),
read=cache(ttl=60, max_bytes=8 * 1024 * 1024),
environment='prod',
)
try:
fs.echo('cached content', '/example.txt')
fs.cat('/example.txt')
fs.cat('/example.txt')
finally:
fs.close()
client.close()
if __name__ == '__main__':
main()
The async equivalent uses redis.asyncio.Redis and makes the same four methods
coroutines. The example uses pickle because cache values include typed storix
objects as well as bytes and strings. Only unpickle data from a private,
trusted cache; otherwise use an application-specific safe serializer.
Adapt any cache¶
Your store does not inherit from a storix class. It only provides these four
methods, synchronously for storix or as coroutines for storix.aio:
expire is seconds or None; return values are ignored. delete_match is used
to evict path subtrees and to clear the layer's namespace. Implement it with a
cursor or scan operation rather than a blocking full key listing.
The store is the only thing that changes; the per-operation options
(metadata / du / read / url, each True or a cache(...) spec) work the
same over any store. See Layers for the full set.
Single writer
A cache cannot see writes made outside the layer (another process, the cloud
console). Every write through the layer evicts what it touched, so your own
session stays consistent, but pass a ttl to bound staleness whenever a store
is shared with writers storix does not control.