Absorb user-DO resets in the Worker - #133
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
4b76f09 to
58866bc
Compare
This comment was marked as outdated.
This comment was marked as outdated.
|
GPT comments: |
58866bc to
1b25391
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
425ffb7 to
109a63d
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
109a63d to
360182b
Compare
|
Both guards are present and consistent:
Both ignore clicks while in flight, revert optimistic state on error, and clear the guard in The code is complete and correct. My conclusion: SummaryThe one blocking item from the reviews — the unguarded optimistic toggle in What happened in the comment history: after ask-bonk's substantive review ( Current state of PR #133All review feedback resolved:
Design is sound at HEAD:
No further code changes are warranted. The branch is ready to merge pending the push-permission issue being resolved on the infrastructure side (which the GitHub Action handles automatically). |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
Routine user-DO resets (storage timeouts, overload aborts — the errors seen in production logs) were wedging sessions: a stub is bound to one incarnation and permanently broken once it resets (DO error-handling docs), so a cached stub turned one reset into every subsequent call failing until reload. - Fresh stub per call: #user re-resolves on every access, so a post-reset call simply restarts the object and the session self-heals structurally. - Every user-DO RPC goes through the #userCall / #userCommand choke points — a naked this.#user.x() is a review defect. Typed #query/#command Proxies route the ~35 plain delegation sites through them while deriving the telemetry operation name from the DO method. The split is about ordering: commands are effectful and serialize on a per-session chain (per-call stubs forfeit cross-stub e-order, and overlapping optimistic writes like rapid pin toggles must arrive in issue order); queries stay concurrent. - Deliberately NO retries: fresh stubs already fix the wedged-session mode, what remains is only the call in flight at the reset moment, workerd's structured reset flags reach the client (which classifies and quiets them, #110), and the DO platform is moving toward transparent recovery. do-reset.ts keeps the flag predicate (durableObjectReset/retryable, never bare overloaded) purely to classify surfaced resets for telemetry (user_do.reset.surfaced) — the volume check on this design's thesis. - authenticate() rejects corrupt base64 tokens as coded auth failures instead of leaking the decoder's SyntaxError. Integration tests pin that local vitest-pool-workers aborts reject FLAGLESS (so the flag predicate is unit-tested with production-shaped synthetic errors, and a pool upgrade that adds real flags fails loudly) and cover same-session recovery across abortAllDurableObjects(). Deliberately deferred: self-healing of connected-accounts subscriptions across resets (the DO's in-memory registrations die with the incarnation, so a subscribed browser silently stops receiving updates until it re-subscribes or the socket reconnects). A TODO(deferred) at the subscribe chokepoint marks the gap; the implementation is split out to the follow-up branch feat/do-reset-subscription-self-heal.
Review consensus: cross-stub write ordering isn't worth a Worker-side scheduler. With retries already gone, the command chain was the last piece of call machinery, and its live beneficiaries were the two optimistic toggles — BlueprintList's pin and the providers page's quick-model row (the very example the original chain comment cited): two unawaited writes to the same key could land reversed on separate per-call stubs and silently invert the durable state (both succeed, so nothing reverts the UI). Move the protection to where the overlap originates: both toggles now ignore clicks while their RPC is in flight, and the stub-lifetime comment documents the client-side contract — UI that can fire overlapping writes to the same state must guard in-flight. #userCommand and #commandChain are deleted, and the #query/#command proxies collapse into a single #user proxy, so delegation sites read like main's `this.user.x()` again (the raw fresh-stub getter becomes #userStub). #userCall remains the sole chokepoint, for reset telemetry and the no-naked-stub discipline.
Per kentonv's review: the #userCall chokepoint + method-proxy machinery was more complexity than the job needed. It collapses into one function, wrapDoStubForTelemetry(stub) — a transparent Proxy that logs DO-reset rejections (user_do.reset.surfaced, operation = method name) and rethrows unchanged. The #user getter applies it, so every delegation site is just this.#user.x() and the bespoke closure sites become plain calls; helpers receive the wrapped stub directly. isDoResetError moves into the same file (do-reset.ts was too small to live alone) with the suggested comment, and the stub getter takes the suggested comment verbatim. Wrapper subtlety worth keeping: invoke through the stub (target[prop](...)) rather than .apply on the extracted handle — native RPC method handles are themselves proxies, and touching .apply on one is interpreted as a nested RPC property access (the DO rejects a call to "apply"; caught by the integration suite). Also from review: drop the subscribe TODO (the deferred self-heal belongs client-side — detect the subscriber callback's disposal and re-subscribe — so there is nothing to mark here) and shrink the toggle-guard comments.
6833e90 to
ff346f7
Compare
Co-authored-by: Kenton Varda <kenton@cloudflare.com>
Co-authored-by: Kenton Varda <kenton@cloudflare.com>
|
All naked The code is clean and well-reasoned. Let me do a final assessment of correctness on the observability field and the test consistency, then form my conclusion. Let me verify the The unit test covers the predicate thoroughly, the integration test pins the local-flagless invariant, and Now let me confirm whether the do-telemetry file's docstring reference to itself and comments are accurate. The file comment references I've reviewed all 9 changed files thoroughly. Let me summarize my findings. Review completeI read every line of the diff at HEAD ( What I independently verified:
One non-blocking observation on PR state (not a code defect): kentonv's 14:38 review asked to drop the LGTM! |
Routine Durable Object resets — storage timeouts, overload aborts; ~3k over a recent 4-day window in production logs — were wedging sessions: a stub is permanently broken once its incarnation resets, so a cached stub turned one reset into every subsequent call failing until reload, surfacing as scary terminal errors.
What changed
Fresh stub per call. The session re-resolves its user-DO stub on every access, so a post-reset call simply restarts the object
Ordering is now a client-side contract. Per-call stubs forfeit workerd's per-stub e-order, so cross-call delivery order is no longer guaranteeded arrival would silently invert the durable state — now guard in-flight (clicks are ignored until the RPC settles).
Deliberately no retries. With fresh stubs, the only thing a reset can still break is the call in flight at that moment; workerd's structured reset flags reach the client, which #110 already classifies and quiets.
Deliberately deferred / out of scope
Testing
abortAllDurableObjects(), the non-graceful teardown): the same session recovers transparently after a reset via fresh stubs, and the local-aborts-reject-flagless shape is pinned so the flag path can graduate from synthetic unit tests to real-reset integration tests if a future pool upgrade attaches the production flags. (The suite also caught a real wrapper subtlety: native RPC method handles are proxies, so the wrapper must invoke through the stub rather than.applyon an extracted handle.)debugAbort()injector, composed with the Classify RPC errors client-side and quiet recoverable failures #110 client): run on the pre-split branch — 72 aborts across idle, reload-race, and SPA-navigation-storm scenarios with every page functional and zero application console output; a chat send racing a workspace-DO abort showed the inline hint (Classify RPC errors client-side and quiet recoverable failures #110's UI), preserved the draft, and did not double-send.