Skip to content

feat(workers): re-enable SWR on Workers via ctx.waitUntil (LAB-751) - #80

Merged
27Bslash6 merged 2 commits into
mainfrom
lab-751-workers-swr-waituntil
Jul 24, 2026
Merged

feat(workers): re-enable SWR on Workers via ctx.waitUntil (LAB-751)#80
27Bslash6 merged 2 commits into
mainfrom
lab-751-workers-swr-waituntil

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Closes LAB-751 (phase-2 follow-up to LAB-595).

What

Re-enables stale-while-revalidate on Cloudflare Workers by threading the request's ExecutionContext into the cache runtime, replacing the phase-1 hard-off.

  • cache.withExecutionContext(ctx) (new, Workers surface — WorkersCache): returns a request-scoped view over the per-isolate singleton (shared L1/backend/encryptor — no new nonce counter). Functions wrapped through the view register their SWR background refreshes via ctx.waitUntil, so workerd keeps them alive past response return.
  • Fail-safe gate: the Workers runtime sets swrRequiresWaitUntil; wrapped reads without a bound context fall back to plain (no-SWR) L1 gets — no refresh marker is taken, so nothing can wedge. Node semantics are untouched (fire-and-forget stays safe there).
  • refreshingKeys hardening: SetMap with expiring markers (SWR_REFRESH_MARKER_TTL_MS, 60s). A refresh torn down without settling — e.g. workerd dropping waitUntil work at its deadline while the isolate stays warm — can no longer leave a permanent "refresh in progress" state or eat a maxConcurrentRefreshes slot forever. An expired marker only risks a duplicate refresh, which L1 version tokens already make benign.

Why the per-request view (not a setter on the singleton)

workerd ties a refresh's I/O to the request that started it, and scheduleRefresh fires synchronously inside the wrapped call — so the waitUntil used must belong to that same request. A mutable "current ctx" slot on the singleton would be racy under concurrent requests interleaving in one isolate; a lexically captured view is correct by construction.

Tests

  • New workerd lane test/workers/swr.workers.test.ts (real createExecutionContext/waitOnExecutionContext): stale value served immediately; refresh completes under waitUntil (L1 + L2 both updated); N concurrent stale hits single-flight to one refresh; a failing refresh clears its marker instead of wedging; unbound reads schedule nothing and leave the key refreshable.
  • Node lane: marker-expiry + slot-sweep tests (lru-cache.test.ts), waitUntil registration + never-rejects tests (background-refresh.test.ts).
  • Gates: eslint clean, tsc clean, Node lane 565 passed, workers lane 62 passed, workers bundle guard OK.

Docs

README Workers section, /workers entry docblocks, and the existing workers smoke test updated from "SWR forced off" to the phase-2 behaviour. Companion PRs update protocol/sdk-feature-matrix.md and the docs.cachekit.io Workers quickstart.

Crypto/protocol gate

Not triggered: control-flow only — no encryption, AAD, key-derivation, cache-key, or ByteStorage wire change (as scoped in the issue).

Summary by CodeRabbit

  • New Features

    • Added support for stale-while-revalidate refreshes in Cloudflare Workers using the request execution context.
    • Added request-scoped cache views for reliable background refreshes after responses begin.
    • Exported Workers-specific cache and execution-context types.
  • Bug Fixes

    • Prevented stalled refresh markers from blocking future refreshes indefinitely.
    • Ensured failed background refreshes clean up correctly and can be retried.
  • Documentation

    • Updated Workers examples and guidance for execution-context-bound caching and platform differences.

Phase 1 (LAB-595) forced stale-while-revalidate off on Workers: workerd
cancels fire-and-forget work at response return, so a background refresh
died mid-flight and left its key wedged in refreshingKeys — enough wedged
keys would hit maxConcurrentRefreshes and silently disable SWR cache-wide.

Phase 2 threads the platform handle through instead of keeping the hard-off:

- CacheRuntime gains swrRequiresWaitUntil; the Workers runtime sets it and
  drops the forced swrEnabled: false. Without a bound handle, wrapped reads
  fall back to plain (no-SWR) L1 gets — no refresh marker taken, nothing to
  wedge (fail-safe, not fail-wedged).
- cache.withExecutionContext(ctx) returns a request-scoped view (shared L1,
  backend, encryptor — no new nonce counter) whose wrap/with/secure.wrap
  register each refresh via ctx.waitUntil. The handle is captured lexically
  per request because workerd ties a refresh's I/O to the request that
  started it; a mutable current-context slot on the singleton would be racy
  under concurrent requests in one isolate.
- refreshingKeys hardened from Set to Map with expiring markers
  (SWR_REFRESH_MARKER_TTL_MS): a refresh torn down without settling (e.g.
  workerd dropping waitUntil work at its deadline in a warm isolate) can no
  longer wedge its key or permanently consume a refresh slot; an expired
  marker only risks a duplicate refresh, which version tokens make benign.
- workerd test lane (swr.workers.test.ts, real ExecutionContexts): stale
  value served immediately, refresh completes under waitUntil (L1+L2),
  concurrent stale hits single-flight, failing refresh clears its marker,
  unbound reads schedule nothing and leave the key refreshable.

Node semantics unchanged: swrRequiresWaitUntil unset, refreshes stay
fire-and-forget (safe there), waitUntil is a no-op arg. No encryption, AAD,
key-format, or wire-format change — control flow only.

Co-authored-by: multica-agent <github@multica.ai>
@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: 37 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: 3d384ed0-41c5-449d-9602-e29ef60f9c99

📥 Commits

Reviewing files that changed from the base of the PR and between 4965be9 and 9c303eb.

📒 Files selected for processing (1)
  • packages/cachekit/src/cache/background-refresh.ts

Walkthrough

Workers SWR support now binds refreshes to ExecutionContext.waitUntil, adds expiry recovery for stranded refresh markers, exposes a typed Workers cache surface, and documents and tests request-scoped usage with plain-L1 fallback when no context is bound.

Changes

Workers SWR lifecycle

Layer / File(s) Summary
Recoverable SWR refresh markers
packages/cachekit/src/constants.ts, packages/cachekit/src/l1/lru-cache.ts, packages/cachekit/src/l1/lru-cache.test.ts
SWR refresh markers now expire after 60 seconds, are swept during concurrency checks, and are covered by marker-recovery tests.
waitUntil refresh scheduling
packages/cachekit/src/cache/background-refresh.*, packages/cachekit/src/cache-core.ts
Refresh promises can be registered with waitUntil; CacheImpl conditionally requires and forwards the request callback for Workers SWR.
Workers cache surface
packages/cachekit/src/workers/*, packages/cachekit/src/intents-core.ts
Workers runtime configuration requires waitUntil, exposes WorkersCache.withExecutionContext, exports related types, and preserves the specialised cache type through cache factory intents.
Workers usage and validation
packages/cachekit/README.md, packages/cachekit/test/workers/*
Examples and tests use request-bound contexts and validate stale reads, single-flight refreshes, failure recovery, and unbound plain-L1 reads.

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

Sequence Diagram(s)

sequenceDiagram
  participant FetchHandler
  participant WorkersCache
  participant CacheImpl
  participant BackgroundRefreshManager
  participant L2Backend
  FetchHandler->>WorkersCache: withExecutionContext(ctx)
  FetchHandler->>WorkersCache: wrap() request
  WorkersCache->>CacheImpl: wrap(..., ctx.waitUntil)
  CacheImpl->>BackgroundRefreshManager: scheduleRefresh(..., waitUntil)
  BackgroundRefreshManager->>L2Backend: persist refreshed value
  BackgroundRefreshManager-->>FetchHandler: ctx.waitUntil(refresh)
Loading

Possibly related PRs

  • cachekit-io/cachekit-ts#78: Introduces the related Workers runtime and entrypoint changes preceding this request-context SWR integration.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: re-enabling SWR on Workers by routing refreshes through ctx.waitUntil.
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-751-workers-swr-waituntil

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

27Bslash6 added a commit to cachekit-io/protocol that referenced this pull request Jul 24, 2026
cachekit-io/cachekit-ts#80 re-enables stale-while-revalidate on the
Workers entry behind cache.withExecutionContext(ctx); update the LAB-595
footnote from 'forced off' to the phase-2 behaviour (fail-safe no-SWR
without a bound context).

Co-authored-by: multica-agent <github@multica.ai>
@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: 1

🤖 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 `@packages/cachekit/src/cache/background-refresh.ts`:
- Around line 107-112: Wrap the waitUntil?.(refresh) registration inside
scheduleRefresh in a try/catch so synchronous platform errors are contained and
do not propagate to the successful stale-read path in wrap. Preserve the
existing refresh promise handling and treat a registration failure as abandoning
only that refresh attempt.
🪄 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: a9d66d2b-21d5-4f0c-b658-97e76266fc21

📥 Commits

Reviewing files that changed from the base of the PR and between d70d225 and 4965be9.

📒 Files selected for processing (12)
  • packages/cachekit/README.md
  • packages/cachekit/src/cache-core.ts
  • packages/cachekit/src/cache/background-refresh.test.ts
  • packages/cachekit/src/cache/background-refresh.ts
  • packages/cachekit/src/constants.ts
  • packages/cachekit/src/intents-core.ts
  • packages/cachekit/src/l1/lru-cache.test.ts
  • packages/cachekit/src/l1/lru-cache.ts
  • packages/cachekit/src/workers/index.ts
  • packages/cachekit/src/workers/runtime.ts
  • packages/cachekit/test/workers/cache.workers.test.ts
  • packages/cachekit/test/workers/swr.workers.test.ts

Comment thread packages/cachekit/src/cache/background-refresh.ts
…duleRefresh

waitUntil() is a synchronous platform call that can throw (e.g. a caller reusing a wrapped function across requests with a stale ExecutionContext). An uncaught throw propagated out of scheduleRefresh and failed the stale-read path in wrap() that already had a valid value to serve. Wrap the registration in try/catch so a failed registration forfeits only this refresh attempt, never the read; stranded refreshingKeys markers remain bounded by SWR_REFRESH_MARKER_TTL_MS.

CodeRabbit-Resolved: background-refresh.ts:107:Guard the waitUntil registration call itself
Co-authored-by: multica-agent <github@multica.ai>
@27Bslash6
27Bslash6 merged commit 08fd853 into main Jul 24, 2026
11 checks passed
@27Bslash6
27Bslash6 deleted the lab-751-workers-swr-waituntil branch July 24, 2026 23:37
This was referenced Jul 24, 2026
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