fix(server): subscriptions survive hibernation wake (ADR-0019) - #35
Merged
Conversation
Hibernatable sockets survive DO eviction by design, but the subscription registry was instance memory: a wake restored the socket set and nothing else, so an idle client's live queries went silently dead on a still-open socket — its own mutations kept confirming while deltas fanned out to nobody, and the client's only re-subscribe trigger (the close path) never fired. Field-reported against 0.5.1. Subscriptions now write through to a durable _sync_subs table keyed by a per-socket id tag stamped at accept (tags survive hibernation; the attachment is contractually the author's claims and 2 KiB-capped), and are restored onto surviving sockets during registerSync — before anything can dispatch or drain. A post-restore reconcile drops subs whose collection left the schema (reset; truncation is the terminal state), and a restored predicate that no longer compiles closes the socket with a non-terminal code so reconnect re-subscribes through the normal rejection path — a reset would strand the query while its cursor advanced (codex review). Orphaned rows sweep during existing compaction housekeeping; no idle timers. No wire or client change: an unmodified 0.5.1 client against a fixed server recovers fully. Pinned by tests/hibernation.test.ts under real evictions (evictDurableObject, unlocked by the vitest 4 migration) in five shapes, plus an orphan-sweep pin driven through the real maybeCompact path. Closes #29 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gpt-5.5 reference was stale (user-level addendum already says gpt-5.6-sol), and an invocation that neither pipes stdin nor closes it hangs codex exec forever — both bit this session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cord null hypothesis Double-adversarial round (Claude + codex gpt-5.6-sol, independent) on whether ADR-0019's outcome could be simpler. Both converged on keeping the SQLite design; the round produced corrections the ADR now carries: - serializeAttachment caps at 16,384 bytes per current Cloudflare docs, not the stale 2 KiB — the rejection now rests on its real grounds (author contract, no honest aggregate bound, O(state) write amplification), and concedes the attachment's genuine hygiene advantage. Cloudflare's own larger-state guidance (storage + key on the socket) is this ADR's shape. - close-all-sockets-on-wake recorded as the rejected null hypothesis: ~10 s idle hibernation cadence would turn ordinary quiet into reconnect waves, nullifying the hibernation API. - future capability-negotiated protocol resub recorded as a machinery-deletion candidate, not a runtime win. - no-TTL row lifecycle made explicit: liveness-keyed sweep; the table holds exactly the live subs of open sockets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug (field-reported against 0.5.1)
Hibernatable WebSockets survive a DO eviction by design — but the subscription registry was instance memory. A wake restored the socket set (
mixin.tsconstructor) and nothing else:committedflowed) while deltas fanned out to nobodyReported by a downstream consumer; their verification recipe checks both idle-before-first-broadcast and idle-after-successful-run — both are pinned here (their earlier "first-broadcast-only" narrowing was an artifact of editor activity keeping the DO awake, since retracted).
The fix (ADR-0019)
_sync_substable, keyed by a per-socket id tag stamped at accept — tags survive hibernation; the attachment was rejected (it is contractually the author's claims, read raw bydeserializeAttachmentand the ADR-0015__pkdiscriminator, andserializeAttachmentcaps at 16 KiB vs 256 subs × unbounded predicate IR)registerSync, before anything can dispatch or drainreset— truncation is the correct terminal stateresetwould strand the query while its cursor advanced (adversarial review catch)Sharp edge: sockets accepted by a pre-fix build that survive an in-place upgrade carry no id tag and keep the old behavior (dead until reconnect) — one release, documented.
Tests
First real eviction coverage in CI (
evictDurableObject, unlocked by #34's vitest 4 migration — closes #29): five shapes intests/hibernation.test.ts(post-broadcast wake, pre-first-broadcast wake, cohosted base with host socket untouched, absent-collection reconcile, uncompilable-predicate close), plus an orphan-sweep pin driven through the realmaybeCompactpath. Suite: 231/231.Adversarial review (codex
gpt-5.6-sol) ran against the full diff; both High findings are fixed and pinned, Mediums documented as deliberate (cap grandfathering; platform output-gate atomicity), Lows addressed.Closes #29
🤖 Generated with Claude Code