Skip to content

Read-only replay mode (on_miss=error), SWR no-revalidate under it, loud signal for permanent SET failures (413) #271

Description

@27Bslash6

Feature request: read-only replay mode, SWR no-revalidate under it, and a loud signal for permanent SET failures

Context / use case

We run cachekit as the record/replay substrate for deterministic LLM-call replay in a content pipeline: a record run computes and caches every LLM response; a replay run must serve only from cache, so two replays of the same run are byte-identical (CI, review diffing, audit). This is a general pattern — golden-master testing, offline eval harnesses, and cost-capped CI all want "cache or fail, never compute".

cachekit currently cannot express it. Every degradation path in the decorator ends in live compute, and two write paths fire even on a read-hit workload. We've shipped an app-level gate above cachekit as a stopgap, but the primitive belongs in the library where it can be enforced below every caller.

All line references below are at v0.17.1 (verified identical to v0.16.0 for the cited files).

Ask 1 — read-only replay mode: on_miss="compute" | "error"

Behavioral contract (API surface illustrative — naming/plumbing is yours):

  • Default "compute" = today's behavior, unchanged.

  • In "error" mode, a cache miss raises a typed exception (e.g. CacheReplayMissError) carrying the redacted cache key — a distinct exception type, not a RuntimeError subclass callers may already be catching broadly around cache degradation.

  • The four fail-open degradation sites must also raise in this mode instead of silently computing:

    • key-generation failure → uncached execution (src/cachekit/decorators/wrapper.py:1035, and the sync twin at :1433-1435)
    • circuit breaker open → uncached execution (src/cachekit/decorators/wrapper.py:1104-1112)
    • cache GET failure → uncached execution (src/cachekit/decorators/wrapper.py:1300)
    • backend unavailable / client failure → uncached execution (src/cachekit/decorators/wrapper.py:1371)

    For a replay consumer, "silently computed instead" is corruption, not graceful degradation: the run completes with different content and no signal.

  • All writes suppressed in "error" mode — including background TTL-refresh PUTs (refresh_ttl_on_get), so a replay run cannot mutate the recording it reads.

Ask 2 — SWR must not revalidate in read-only mode

_l2_swr_recompute_store_async calls the wrapped function and stores the result (src/cachekit/decorators/wrapper.py:759-768; sync twin below it). Its docstring is explicit that revalidation failures are silent by design — which is exactly right for freshness workloads and exactly wrong for replay: a stale-window hit triggers a background recompute that overwrites the recording with a new live result, invisibly.

This is the sharpest trap of the three because @cache.io defaults the stale window to ttl, so any consumer using the io preset is one stale hit away from a silently mutated cache.

Behavioral ask: in read-only mode, either serve stale without revalidating (never call the wrapped fn, never store), or reject the read-only + stale_ttl combination at decoration time. Either is fine; silent recompute-and-store is not.

Ask 3 — loud signal for PERMANENT SET failures (413 is the canonical case)

Today a SET failure is swallowed and the computed result returned (src/cachekit/decorators/wrapper.py:1356-1357, sync twins at :1796 and :1878 — "Caching failed but function succeeded - return result anyway"). An HTTP 413 from the cachekit.io backend classifies PERMANENT (src/cachekit/backends/cachekitio/error_handler.py:79-90) and the decorator "degrades: runs uncached, once".

For a freshness cache that's the right default. For a record run it's a silent hole: the run believes it cached a large response, the replay run misses forever, and nothing ever said so. We hit this concretely with LLM responses near the 25 MB value cap.

Behavioral ask: surface permanent store failures through something a recording context can observe — a per-decorator callback, a counter on the stats surface, anything programmatically consumable at the call site (log lines are not). Notably, v0.17.x already carved out InteropError to fail loud on SET (src/cachekit/decorators/wrapper.py:1353-1355, "never 'computed but silently never cached'") — this ask extends that same principle to permanent backend rejections, at least as an opt-in signal.

Notes

  • Asks 2 and 3 stand on their own even if Ask 1 is rejected: silent recording mutation and silent permanent SET loss are footguns for any consumer treating cache contents as ground truth, replay mode or not. Filed as one issue for coherence; happy to split if you prefer.
  • Duplicate check: no existing issue on cachekit-py or cachekit-core covers replay/read-only mode, SWR-under-read-only, or loud 413 (searched 2026-09-01).
  • Internal provenance: GEO pipeline tickets LAB-2357 (design) / LAB-2419 (this FR); our app-level stopgap is LAB-2417.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions