Skip to content

feat(backend): bound L1 backfill by the server's remaining freshness (LAB-557) - #268

Open
27Bslash6 wants to merge 1 commit into
mainfrom
lab-557-fresh-for-l1-bound
Open

feat(backend): bound L1 backfill by the server's remaining freshness (LAB-557)#268
27Bslash6 wants to merge 1 commit into
mainfrom
lab-557-fresh-for-l1-bound

Conversation

@27Bslash6

Copy link
Copy Markdown
Contributor

Bounds L1 backfill by the server's remaining freshness (LAB-557): CachekitIO reads parse the new X-CacheKit-Fresh-For response header (protocol#51, emitted by saas#325) and L1 backfill uses min(ttl, fresh_for) — an entry read late in its server-side freshness window is never served fresh from L1 past the server's fresh_until. Origin: CodeRabbit outside-diff finding on #233 (LAB-506), deferred there because it wasn't fixable SDK-side alone.

What ships

  • CachekitIOBackend.get_with_freshness(bytes, is_stale, fresh_for); absent header = None (pre-signal server, legacy behavior); unparseable/negative = 0 (conservative, mirrors unrecognized-freshness → stale; debug-logged so a garbage-emitting proxy is diagnosable). Threaded through the handler/operation-handler chain; a third-party backend still returning the released 2-tuple degrades to fresh_for=None via a length-tolerant unpack instead of a swallowed unpack error turning every hit into a miss.
  • The freshness read path now gates on backend capability (class-level supports_swr — instance hasattr read Mock/__getattr__ proxies as capable), not just configured SWR: the unbounded backfill predates SWR and applied to every CachekitIO read. Revalidation scheduling stays gated on an actually-configured stale window.
  • _l1_backfill_from_l2 holds both invariants at all three backfill sites in lockstep (stale never recorded; fresh bounded); the post-lock double-checks use a freshness-aware read (_l2_double_check) so an L2-read-error + still-live-old-entry double fault can't sneak an unbounded backfill through a side door.
  • Shorten-only guarantee: with ttl=None the bound clamps to L1's own DEFAULT_L1_TTL_SECONDS (300s) — a long server remainder must never extend local service toward the 30-day cap (DELETE-as-revocation relies on the ≤300s ageout).

Tests: regression per the ticket AC (fresh hit with 0s remaining is not L1-recorded; next read reaches L2), bound/legacy/no-SWR/mixed-reader-stale cases, clamp-never-extend, 2-tuple compat, header-parse vectors. tests/unit/ 1960 passed; ruff + basedpyright clean; full-suite failure set identical to main modulo timing-flaky perf benchmarks.

Expert-panel review (4 agents, high stakes — crypto/protocol gate): FIX-FIRST → applied: ttl=None clamp (CWE-613 — the bound had become an extension), 2-tuple tolerance, backfill-guard dedup, garbage-header debug log, honest _l2_double_check docstring. Rejected: scheduling revalidation from the double-check (spec-permitted asymmetry on a double-fault rarity — documented instead).

Docs: docs/configuration.md SWR section documents the bound; protocol matrix row stays 🚧 until this ships in a release (matrix verifies released artifacts). Ticket: LAB-557.

…(LAB-557)

The read response now carries X-CacheKit-Fresh-For (protocol
spec/saas-api.md#remaining-freshness). CachekitIO reads parse it
(absent = None/legacy; unparseable/negative = 0, the conservative
action) and thread (bytes, is_stale, fresh_for) through the freshness
chain; L1 backfill uses min(ttl, fresh_for) so an entry read late in
its server-side freshness window is never served fresh from L1 past
the server's fresh_until.

The freshness read path now gates on backend capability, not just
configured SWR — the unbounded backfill predates SWR and applied to
every CachekitIO read. Revalidation scheduling stays gated on an
actually-configured stale window. Post-lock double-check reads share
the same bound and stale-exclusion via _l2_double_check.
@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 13 minutes.

View limit details

Limit details: You’ve used all 6 included reviews currently available. Your 45 included PR review attempts over the past 7 days set your current allowance at 6 reviews per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 80045a56-f796-4787-898c-b86ab993e7f0

📥 Commits

Reviewing files that changed from the base of the PR and between e1b05ce and f875957.

📒 Files selected for processing (8)
  • .secrets.baseline
  • docs/configuration.md
  • src/cachekit/backends/cachekitio/backend.py
  • src/cachekit/cache_handler.py
  • src/cachekit/decorators/wrapper.py
  • src/cachekit/l1_cache.py
  • tests/unit/backends/test_cachekitio_swr_transport.py
  • tests/unit/test_swr_decorator.py

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant