FanoutCache
A disk cache split over several SQLite databases (shards), so reads and writes on different keys
Usage
FanoutCache(
directory,
timeout=None,
*,
shards=None,
size_limit=None,
cull_limit=None,
serializer="msgpack",
pickle=False,
types=None,
clock=None,
max_connections=None,
sqlite_cache_size=None,
wal_size_limit=None,
wal_checkpoint="manual",
on_decode_error=None,
durable=False
)don’t queue on one file’s locks.
Each key lives in exactly one shard, chosen by a hash that is part of the on-disk format: every process, and every release, sends a key to the same shard. The root records its shard count, so every handle agrees on it. Single-key calls behave exactly as on Cache, on the key’s shard. len() and volume() add up every shard under one timeout.
clear(), expire(), cull() and checkpoint() run on every shard in turn, under one timeout for the whole call. If a shard fails, the others are still done, and then the first failure raises, as its own type (so except Timeout still works), with two extra attributes and a note naming them:
failed_shards: a tuple of the indexes of the shards that failed, in order.removed(not on checkpoint()): how many entries the call removed in all, from every shard, including what a failed shard removed before it failed.
Parameters
directory: str | os.PathLike[str]-
The root directory. It holds the manifest (
diskcache-oxide-fanout) and one directory per shard,000toNNN. It must not be a plain Cache’s directory. timeout: float | None = None-
As for Cache (default 60). Opening,
len(), volume(), clear(), expire(), cull(), checkpoint() and close() share one timeout across every shard, not one each. shards: int | None = None-
How many shards, 1 to 256.
None(the default) uses the count the root records, or 8 for a new root. A count that differs from the recorded one raises LayoutMismatchError (aValueError): changing it needs a new directory. Each shard takes about 4 file descriptors to open, and 2 more per connection it opens in use, so 256 shards need over 1,024: more than Linux’s defaultulimit -n. Too few raisesOSError(EMFILE) with the numbers. size_limit: int | None = None-
The disk budget in bytes for the whole cache, split evenly (floored) across the shards, each of which culls on its own. Each shard needs at least 1 MiB, so the total must be at least shards MiB. With none given, a new root gets 1 GiB in all.
cull_limit: int | None = None-
As for Cache, per shard.
serializer: str = "msgpack"-
As for Cache.
pickle: bool = False-
As for Cache.
clock: Callable[[], float] | None = None-
As for Cache.
max_connections: int | None = None-
The most SQLite connections this process opens for the whole cache, split across the shards (the first
max_connections % shardsget one more). At least shards. The default is two per shard, and never fewer than 8 in all. sqlite_cache_size: int | None = None-
As for Cache, per connection.
wal_size_limit: int | None = None-
As for Cache, per shard (each shard has its own write-ahead log).
wal_checkpoint: Literal["manual", "auto"] = "manual"-
As for Cache.
on_decode_error: Literal["raise", "miss"] | Callable[[str, Exception], object] | None = None-
As for Cache.
durable: bool = False- As for Cache: single-key calls on a shard that fails give their miss values. One report covers every shard, named by the root directory.
Raises
ValueError-
A setting is out of range, or
max_connectionsis below shards. LayoutMismatchError-
shards differs from the root’s, or the directory belongs to a plain Cache (or a shard of it does). Also a
ValueError, and a FormatMismatchError. TypeError-
An argument has the wrong type, including a
boolfor timeout or shards. FormatMismatchError-
The root’s manifest, or a shard’s database, is in a format this version can’t read.
OSError-
The filesystem doesn’t support hard links, which creating the manifest needs, or the process can’t open enough files for every shard (
ulimit -n). Timeout- A shard stayed locked, or a fork stayed pending, for longer than timeout.
Examples
from diskcache_oxide import FanoutCache
cache = FanoutCache('/var/cache/my-app-counters', shards=8) # shards is keyword-only
cache.incr('ip:203.0.113.7') # as on Cache, on the key's shard
cache.shard_of('ip:203.0.113.7') # 0 to 7, the same in every process and release
with cache.transact(key='totals') as tx: # one shard's write lock, not all of them
tx.set('totals', tx.get('totals', 0) + 1)