Skip to content

feat(l1): LAB-728 stale-while-revalidate — serve stale + single-flight background refresh - #47

Merged
27Bslash6 merged 2 commits into
mainfrom
lab-728-l1-swr
Jul 25, 2026
Merged

feat(l1): LAB-728 stale-while-revalidate — serve stale + single-flight background refresh#47
27Bslash6 merged 2 commits into
mainfrom
lab-728-l1-swr

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Closes LAB-728 — the stretch item deferred from LAB-518.

What

L1 stale-while-revalidate for #[cachekit]: a hit past the freshness threshold (but before hard expiry) is served immediately while exactly one background task re-executes the function and rewrites both cache layers. No caller ever blocks on a merely-stale entry; concurrent stale readers never stampede the origin (misses are billable).

Semantics (sibling parity)

  • Threshold: swr_threshold_ratio × the L1 entry's own TTL, ±10% jitter — cachekit-py's elapsed-lifetime semantics (L1Cache.get_with_swr), same default 0.5 as py and ts. Note: py measures elapsed > ratio×TTL, ts measures remaining < ratio×TTL; identical at 0.5, and rs deliberately mirrors py (the elder SDK) — documented on swr_threshold_ratio.
  • Default on (py L1CacheConfig.swr_enabled=True, ts swrEnabled: true). Off switch: .swr_enabled(false) restores the exact pre-SWR expire-or-serve contract (test-verified).
  • Refresh dedup is single_flight() — no SWR-specific dedup path. The spawned refresh task acquires the same per-key flight the cold-miss path uses: first task leads and re-executes; followers see the still-present entry and stand down. On lock-capable backends this also suppresses refreshes across processes for free.
  • Hard expiry is moka's expiry: an expired entry is never returned, so SWR structurally cannot serve past it — the read falls through to the normal blocking miss + fill.

Acceptance criteria → evidence (all live call paths, no LAB-388s)

Criterion Evidence
Config mirrors py/ts, sibling default CacheKitBuilder::swr_enabled / swr_threshold_ratio (validated (0.0, 1.0]), default on/0.5; swr_tests::threshold_ratio_is_validated_at_build
Stale read doesn't block swr_tests::stale_reads_serve_immediately_and_refresh_exactly_once — 8 concurrent stale reads complete in <300 ms against a 400 ms origin
Exactly one refresh via single_flight() same test: 8 readers (on 8 client clones) → origin call count goes 1 → 2, never more
Hard expiry → blocking miss swr_tests::hard_expired_entry_takes_the_blocking_miss_path — asserts the call blocks ≥ origin latency and recomputes
Sync fn: clear error, no silent no-op macro rejects non-async fn at decoration time with an actionable message; unit-tested in cachekit-macros (sync_fn_is_a_clear_decoration_time_error)
30 s backfill cap reconciled freshness window derives from each entry's own TTL: backfilled entries refresh at ~ratio × 30 s and the refresh restores the full write-path TTL. Never silently clamped; documented on L1_BACKFILL_TTL_SECS, get_with_swr, README
Doc gate README (features table, dual-layer diagram, new SWR section, reliability table), rustdoc on every new surface, doc-tests green; protocol matrix row in cachekit-io/protocol (companion PR)
Interop untouched no key/wire/AAD changes; interop_vector_tests, encryption_interop_tests, aad_tests all pass unchanged

Notable mechanics

  • CacheKit is now Clone (all-Arc, cheap): the 'static refresh task must own the client. Clones share L1, single-flight map, backend, encryption — required so clones dedup against each other (test: clones_share_l1_state).
  • Reference args survive the 'static capture: the macro re-materialises &T args via ToOwned and rebinds them under the original name/shape inside the task (&str stays &str via .as_str()); owned args are cloned (already a macro requirement). Test: str_argument_refreshes_in_the_background.
  • Secure path: staleness is judged on the L1 ciphertext entry — zero-knowledge preserved; decryption stays fail-closed.
  • No tokio runtime → refresh skipped (stale already served; next stale read retries; hard expiry surfaces errors on the blocking path). Panicking would turn a cache optimisation into an availability bug on non-tokio executors. Documented.
  • Native only: workers+l1 were already mutually exclusive; under unsync/wasm the SWR builder surface doesn't exist (compile error, not silent no-op) and SwrRead::Stale is unreachable.
  • random_unit() (uuid-v4-backed jitter) moved from reliability.rs to the crate root, shared with L1.
  • l1 feature now pulls tokio/rt (for Handle::try_current + spawn); compile-time only when unused.

Gates run

cargo fmt --check ✓ · clippy --all-targets (CI feature set) -D warnings ✓ · full test suite (CI feature set) ✓ incl. 7 new SWR integration tests + 2 macro unit tests · doc-tests ✓ · MSRV 1.85 check --all-targets ✓ · wasm32 check (workers,encryption) ✓ (pre-existing warnings only).

Crypto/protocol review gate: not triggered — no encryption, AAD, key-derivation, cache-key, or wire-format change (SWR is an L1 serving policy; the secure path reuses the existing ciphertext read + decrypt unchanged).

Summary by CodeRabbit

  • New Features

    • Added stale-while-revalidate caching for faster responses from stale entries while refreshing them in the background.
    • Added configurable SWR enablement and freshness-threshold settings.
    • Added single-flight refresh handling to prevent duplicate background work.
    • Added equivalent support for encrypted cache reads.
  • Bug Fixes

    • Improved handling of expired or undecodable entries by treating them as cache misses.
  • Documentation

    • Expanded documentation covering SWR behaviour, expiry, refresh failures and reliability composition.
    • Added clearer guidance that decorated cache functions must be asynchronous.

…ound refresh (LAB-728)

An L1 hit past swr_threshold_ratio × entry TTL (default 0.5, ±10% jitter,
mirroring py/ts) but before hard expiry is returned immediately while one
background task re-executes the #[cachekit] function and rewrites both
layers. Refresh dedup rides the existing cold-miss single_flight() — no
SWR-specific dedup path — so N concurrent stale readers cost exactly one
origin execution, in-process and (on lock-capable backends) per fleet.
Hard-expired entries take the normal blocking miss path.

- builder: swr_enabled (default on, sibling parity) + swr_threshold_ratio
  (validated (0.0, 1.0]); native-only surface — no silent no-op elsewhere
- CacheKit is now Clone (all-Arc; clones share L1 + single-flight state) so
  the 'static refresh task can own the client
- interop_get_swr on CacheKit + SecureCache (secure judges staleness on the
  L1 ciphertext entry — zero-knowledge preserved)
- macro: sync fns rejected with a clear decoration-time error; refresh task
  re-materialises reference args via ToOwned (&str stays &str via as_str)
- 30 s backfill cap reconciled: freshness window derives from each entry's
  own TTL, so backfilled entries background-refresh at ~ratio × 30 s and the
  refresh restores the full write-path TTL (documented, never silently
  clamped)
- refresh needs a tokio runtime (Handle::try_current); other executors skip
  the refresh and keep serving until hard expiry — documented
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 2 minutes

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).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ed55e354-5781-4b28-b615-92dbee0862f3

📥 Commits

Reviewing files that changed from the base of the PR and between c96f4bf and 3a8e08d.

📒 Files selected for processing (5)
  • crates/cachekit-macros/src/lib.rs
  • crates/cachekit/src/client.rs
  • crates/cachekit/src/l1/mod.rs
  • crates/cachekit/src/lib.rs
  • crates/cachekit/tests/swr_tests.rs

Walkthrough

Adds native L1 stale-while-revalidate support with configurable freshness thresholds, background single-flight refreshes, secure and non-secure client reads, async macro enforcement, runtime integration, documentation, and integration tests.

Changes

Stale-while-revalidate

Layer / File(s) Summary
SWR state and configuration
crates/cachekit/Cargo.toml, crates/cachekit/src/l1/mod.rs, crates/cachekit/src/client.rs, crates/cachekit/src/lib.rs, crates/cachekit/src/reliability.rs
L1 entries track insertion time and classify reads as fresh, stale, or missed. Builder options validate SWR settings, shared flight storage is feature-aware, and jitter support is centralised.
Client reads and refresh runtime
crates/cachekit/src/client.rs, crates/cachekit/src/lib.rs
Cache clients expose SWR-aware interop reads, including encrypted reads, while supported native Tokio runtimes can spawn refresh tasks.
Macro-generated SWR execution
crates/cachekit-macros/src/lib.rs
Decorated functions must be async. Generated code returns stale values immediately, captures refresh arguments, single-flights recomputation, rewrites caches, and treats refresh failures as non-fatal.
Behaviour validation and documentation
crates/cachekit/tests/swr_tests.rs, README.md
Tests cover stale serving, refresh deduplication, hard expiry, disabled SWR, argument capture, validation, and clone sharing. Documentation describes SWR and reliability composition.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant CacheKitMacro
  participant CacheKit
  participant L1Cache
  participant Origin
  Caller->>CacheKitMacro: invoke async cached function
  CacheKitMacro->>CacheKit: interop_get_swr
  CacheKit->>L1Cache: classify entry
  L1Cache-->>CacheKit: stale value
  CacheKit-->>Caller: return stale value immediately
  CacheKitMacro->>Origin: single-flight background refresh
  Origin-->>CacheKitMacro: refreshed value
  CacheKitMacro->>CacheKit: rewrite cache layers
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarises the main change: L1 stale-while-revalidate with stale serving and single-flight background refresh.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lab-728-l1-swr

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

@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/cachekit-macros/src/lib.rs`:
- Around line 358-390: Reject mutable reference parameters in the SWR capture
logic before processing `non_client_params`, using the `Type::Reference` branch
and its `Reference::mutability` indicator to emit a clear macro error. Leave
immutable reference and owned-argument capture behavior unchanged.

In `@crates/cachekit/src/client.rs`:
- Around line 91-134: Add Debug, Clone, and PartialEq derives to SwrRead<T> in
crates/cachekit/src/client.rs lines 91-134, with the standard generic bounds,
and add the same derives to L1SwrRead in crates/cachekit/src/l1/mod.rs lines
25-35. No other changes are needed.

In `@crates/cachekit/src/lib.rs`:
- Line 69: Add SwrRead to the public prelude module exports, reusing the
existing SwrRead symbol already re-exported from client alongside CacheKit and
related types. Ensure users importing cachekit::prelude::* can reference the
return type of CacheKit::interop_get_swr and SecureCache::interop_get_swr
without an additional import.
- Around line 110-119: Update __swr_spawn to retain and observe the spawned
task’s JoinHandle result instead of dropping it silently. Add a
production-visible tracing warning or existing metrics hook when the refresh
task returns an error or panics, while preserving the fail-soft behavior by not
propagating the failure to callers.

In `@crates/cachekit/tests/swr_tests.rs`:
- Around line 179-198: Increase the timing margin in
disabled_swr_serves_until_hard_expiry_without_refreshing so the second probe
remains safely before the 2-second hard expiry, matching the wider real-clock
slack used elsewhere in the tests; adjust the TTL or trailing sleep without
changing the assertion that no background refresh occurs.
- Around line 6-12: Update the file-level cfg gate in the SWR tests to also
require a non-wasm32 target, matching the native-only constraint documented in
the header and the target-specific availability of client().swr_threshold_ratio.
Preserve the existing feature and unsync conditions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3e6fd1af-8fc9-4d86-9c9b-f37d9518a9b7

📥 Commits

Reviewing files that changed from the base of the PR and between e9b9a1e and c96f4bf.

📒 Files selected for processing (8)
  • README.md
  • crates/cachekit-macros/src/lib.rs
  • crates/cachekit/Cargo.toml
  • crates/cachekit/src/client.rs
  • crates/cachekit/src/l1/mod.rs
  • crates/cachekit/src/lib.rs
  • crates/cachekit/src/reliability.rs
  • crates/cachekit/tests/swr_tests.rs

Comment thread crates/cachekit-macros/src/lib.rs
Comment thread crates/cachekit/src/client.rs
Comment thread crates/cachekit/src/lib.rs
Comment thread crates/cachekit/src/lib.rs
Comment thread crates/cachekit/tests/swr_tests.rs Outdated
Comment thread crates/cachekit/tests/swr_tests.rs
…, test cfg + timing

CodeRabbit-Resolved: cachekit-macros/src/lib.rs:390:Reject &mut parameters
CodeRabbit-Resolved: client.rs:134:SWR enums lack common derives
CodeRabbit-Resolved: lib.rs:69:SwrRead prelude export
CodeRabbit-Resolved: swr_tests.rs:12:cfg gate missing not(wasm32)
CodeRabbit-Resolved: swr_tests.rs:198:timing margin hard expiry
@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@27Bslash6
27Bslash6 merged commit 068b84a into main Jul 25, 2026
8 checks passed
@27Bslash6
27Bslash6 deleted the lab-728-l1-swr branch July 25, 2026 00:14
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