Skip to content

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:

uv add "cashews[redis]"
"""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:

uv add redis
"""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:

get(key, default=None)
set(key, value, *, expire=None)
delete(key)
delete_match(glob_pattern)

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.