Expand description
Local filesystem backend, byte-compatible with cachekit-py’s File backend. Local filesystem backend, byte-compatible with cachekit-py’s File backend.
On-disk layout (shared cross-SDK contract — a py and an rs process pointed at the same directory read each other’s entries):
- Filename: lowercase-hex Blake2b-128 (16-byte digest) of the UTF-8 cache key — filesystem-safe, no directory traversal.
- Header (14 bytes):
b"CK"magic[0:2], format version1[2:3], reserved0[3:4], flagsu16BE[4:6], expiry unix-secondsu64BE[6:14](0= never expires). Payload follows the header. - Writes are atomic: payload lands in a unique
{name}.tmp.{pid}.…file, fsynced, then renamed over the final path. Readers see the old or the new entry, never a torn one. - Expiry is lazy: expired and corrupt entries are unlinked when a read touches them, exactly like cachekit-py.
- Orphan sweep on first write: temp files older than 60 s, left by
crashed writers (py’s or rs’s — both use the
.tmp.marker), are removed by each backend instance’s firstset, once. cachekit-py sweeps at startup instead; rs defers it because the sweep reads the whole directory, and a short-lived reader of a large cache (a CLI hit) would pay that on every run. Only writers leave orphans, so a reader-only process never needs it.
§Concurrency model
- In-process: operations on the same key serialize on one of
64 mutexes (
LOCK_STRIPES), picked by the first byte of the entry’s Blake2b-128 filename. This closes the expire-unlink vs. concurrent-set lost-write race within a process: that race only ever involves one entry path, so one key always maps to one stripe. Keys on different stripes do not wait on each other’s fsync; two keys share a stripe about 1 time in 64 and then still serialize (cachekit-py serializes everything on onethreading.RLock, because its RLock also guards LRU and size bookkeeping; rs has none yet — porting eviction or size caps adds shared cross-key state, and these stripes must be revisited then). - Cross-process (unix): advisory
flock— shared on reads, exclusive on the in-place expiry rewrite — matching py’sfcntl.flockusage, non-blocking with contention surfacing as a timeout error like py. Unlinks are additionally inode-validated: an entry is only removed if the path still names the inode the decision was made on. Combined with the in-process lock this fully closes the stale-unlink race within a process; cross-process it narrows the exposure to the stat-to-unlink instruction window (a rename landing exactly there loses one fresh entry — a cache miss, never corruption — the same class py accepts). - Remaining accepted exposure, identical to py:
refresh_ttlracing a concurrent replace in another process returnsfalse(refresh what was opened is no longer the live entry), and an in-place expiry rewrite torn by power loss yields a wrong expiry, never a corrupt payload.
Deliberate v1 divergences from cachekit-py (documented, not silent): LRU eviction / size caps / entry-count caps and the mmap zero-copy read path are not implemented — the directory grows until entries expire or the caller clears it.
Requires a tokio runtime: file I/O runs on the blocking thread pool via
spawn_blocking (except under the unsync feature, where it runs inline
on the caller — the sync-in-async trade-off cachekit-py documents).
Structs§
- File
Backend - Local filesystem cache backend.
- File
Backend Builder - Builder for
FileBackend.