Summary
On deserialize/integrity failure, the L1 path invalidates the entry (self-heals), but the L2 path catches SerializationError and returns None (miss → recompute) without deleting the corrupt/tampered backend entry. Every subsequent reader re-pays the full backend read + decompress + xxHash3 (+ decrypt) before recomputing; under thundering-herd every waiter does it.
Evidence
- L1 heals:
src/cachekit/decorators/wrapper.py:695 (sync), :955 (async)
- L2 does not delete:
src/cachekit/cache_handler.py:822-824 (sync), :855-857 (async); async post-lock double-check wrapper.py:1100-1101 only warns
- Sync L2 also lacks a separate
except SerializationError branch (async has one at wrapper.py:1037-1048) — corruption is indistinguishable from a network error on the sync path
Impact
Poisoned L2 entries persist until TTL/overwrite, repeatedly burning full read+verify work and masking tamper as transient miss.
Fix
On L2 integrity/auth failure, delete the offending key and emit a distinct cache_get_deserialize metric on both sync and async paths.
Summary
On deserialize/integrity failure, the L1 path invalidates the entry (self-heals), but the L2 path catches
SerializationErrorand returnsNone(miss → recompute) without deleting the corrupt/tampered backend entry. Every subsequent reader re-pays the full backend read + decompress + xxHash3 (+ decrypt) before recomputing; under thundering-herd every waiter does it.Evidence
src/cachekit/decorators/wrapper.py:695(sync),:955(async)src/cachekit/cache_handler.py:822-824(sync),:855-857(async); async post-lock double-checkwrapper.py:1100-1101only warnsexcept SerializationErrorbranch (async has one atwrapper.py:1037-1048) — corruption is indistinguishable from a network error on the sync pathImpact
Poisoned L2 entries persist until TTL/overwrite, repeatedly burning full read+verify work and masking tamper as transient miss.
Fix
On L2 integrity/auth failure, delete the offending key and emit a distinct
cache_get_deserializemetric on both sync and async paths.