Tiered cache: diskcache FanoutCache plus cachetools LRU: working versions (tested 2026-10-16)
A MutableMapping two-tier cache that promotes values from a sharded diskcache FanoutCache into a cachetools LRUCache, covering persistence, expiration and promotion gotchas with offline tests.
passed6 passed · 0 failed2026-10-16 00:56 UTC- Status
- passed
- Last passed
- 2026-10-16 00:56 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38832319459
The code
Exactly the files that passed. Your agent can pull them with get_recipe("diskcache-cachetools-tiered") (install), or see them on GitHub.
diskcache==5.6.3 cachetools==7.2.2 pytest==9.1.1
from __future__ import annotations
import threading
from collections.abc import Iterator, MutableMapping
from typing import Any, Callable
from cachetools import LRUCache
from cachetools.keys import hashkey
from diskcache import FanoutCache
MISSING = object()
class TieredCache(MutableMapping):
"""L1 cachetools.LRUCache promoted from an L2 diskcache.FanoutCache."""
def __init__(self, directory: str, maxsize: int = 128, shards: int = 4, size_limit: int = 1 << 24):
self.memory: LRUCache = LRUCache(maxsize=maxsize)
self.disk: FanoutCache = FanoutCache(directory, shards=shards, size_limit=size_limit)
self._lock = threading.RLock()
self.stats = {"memory": 0, "disk": 0, "miss": 0}
def __getitem__(self, key: Any) -> Any:
with self._lock:
if key in self.memory:
self.stats["memory"] += 1
return self.memory[key]
value = self.disk.get(key, default=MISSING)
if value is MISSING:
self.stats["miss"] += 1
raise KeyError(key)
with self._lock:
self.memory[key] = value
self.stats["disk"] += 1
return value
def __setitem__(self, key: Any, value: Any) -> None:
self.set(key, value)
def __delitem__(self, key: Any) -> None:
with self._lock:
self.memory.pop(key, None)
del self.disk[key]
def __iter__(self) -> Iterator[Any]:
return iter(self.disk)
def __len__(self) -> int:
return len(self.disk)
def set(self, key: Any, value: Any, expire: float | None = None) -> None:
with self._lock:
self.memory[key] = value
self.disk.set(key, value, expire=expire)
def hits(self) -> tuple[int, int]:
return self.stats["memory"], self.stats["disk"]
def close(self) -> None:
self.disk.close()
def __enter__(self) -> "TieredCache":
return self
def __exit__(self, *exc: object) -> None:
self.close()
def memoize(cache: MutableMapping, key: Callable[..., Any] = hashkey):
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
def wrapper(*args: Any, **kwargs: Any) -> Any:
cache_key = key(*args, **kwargs)
try:
return cache[cache_key]
except KeyError:
value = func(*args, **kwargs)
cache[cache_key] = value
return value
wrapper.cache = cache # type: ignore[attr-defined]
return wrapper
return decorator
import time
import pytest
from cachetools import cached
from tiered import TieredCache, memoize
def test_cached_memory_hit_skips_disk(tmp_path):
with TieredCache(tmp_path, maxsize=8) as cache:
calls = []
@cached(cache)
def square(n):
calls.append(n)
return n * n
assert square(4) == 16
assert square(4) == 16
assert calls == [4]
assert cache.stats["memory"] == 1
assert cache.stats["disk"] == 0
def test_lru_eviction_falls_back_to_disk_and_repromotes(tmp_path):
with TieredCache(tmp_path, maxsize=1) as cache:
cache["a"] = 1
cache["b"] = 2
assert "a" not in cache.memory
assert cache["a"] == 1
assert "a" in cache.memory
assert "b" not in cache.memory
assert cache.stats["disk"] == 1
def test_stored_none_is_a_hit_but_missing_uses_default(tmp_path):
with TieredCache(tmp_path, maxsize=8) as cache:
cache["k"] = None
cache.memory.clear()
assert cache.get("k", "fallback") is None
assert cache.get("missing", "fallback") == "fallback"
assert cache.stats["miss"] == 1
def test_disk_tier_survives_reopen(tmp_path):
first = TieredCache(tmp_path, maxsize=2)
first["k"] = {"n": 1}
first.close()
second = TieredCache(tmp_path, maxsize=2)
try:
assert dict(second.memory) == {}
assert second["k"] == {"n": 1}
assert second.stats["disk"] == 1
finally:
second.close()
def test_memory_tier_ignores_disk_ttl(tmp_path):
with TieredCache(tmp_path, maxsize=8) as cache:
cache.set("k", "v", expire=0.05)
time.sleep(0.12)
assert cache.disk.get("k") is None
assert cache["k"] == "v"
def test_memoize_reuses_disk_after_memory_eviction(tmp_path):
with TieredCache(tmp_path, maxsize=1) as cache:
calls = []
@memoize(cache)
def add(x, y):
calls.append((x, y))
return x + y
assert add(2, 3) == 5
cache["unrelated"] = 1
assert add(2, 3) == 5
assert calls == [(2, 3)]
assert cache.stats["disk"] == 1
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
cachetools TTLCache + LRUCache memoization
Memoize calls with cachetools TTLCache and LRUCache, control expiry with an injectable timer, and cache methods with cachedmethod.
cachetools TTLCache wrapped with funcy decorators
Memoize through a cachetools TTLCache with an injectable clock, then layer funcy retry and fallback so only successful results get cached. Fully offline tests.
Idempotent job cache with diskcache FanoutCache guarded by filelock
Memoize expensive jobs in a sharded diskcache FanoutCache and run each one once across threads and processes with a filelock file lock, tested fully offline.