Skip to content

fix(server): subscriptions survive hibernation wake (ADR-0019) - #35

Merged
grrowl merged 3 commits into
mainfrom
fix/hibernation-wake-subs
Jul 27, 2026
Merged

fix(server): subscriptions survive hibernation wake (ADR-0019)#35
grrowl merged 3 commits into
mainfrom
fix/hibernation-wake-subs

Conversation

@grrowl

@grrowl grrowl commented Jul 27, 2026

Copy link
Copy Markdown
Owner

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.ts constructor) and nothing else:

  • the client's live queries went silently dead on a still-open socket
  • its own mutations still confirmed (committed flowed) while deltas fanned out to nobody
  • the client's only re-subscribe trigger (the close path) never fired

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

  • Write-through to a durable _sync_subs table, 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 by deserializeAttachment and the ADR-0015 __pk discriminator, and serializeAttachment caps at 16 KiB vs 256 subs × unbounded predicate IR)
  • Restore during registerSync, before anything can dispatch or drain
  • Post-restore reconcile: a sub whose collection left the schema is dropped (memory + durable) with a reset — truncation is the correct terminal state
  • A restored predicate that no longer compiles closes the socket (1011, non-terminal) so reconnect re-subscribes through the normal rejection path — a reset would strand the query while its cursor advanced (adversarial review catch)
  • Orphan rows sweep during existing compaction housekeeping — no idle timers (ADR-0006 invariant intact)
  • No wire or client change: an unmodified 0.5.1 client against a fixed server recovers fully

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 in tests/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 real maybeCompact path. 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

grrowl and others added 3 commits July 27, 2026 22:02
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>
@grrowl
grrowl merged commit cd2267b into main Jul 27, 2026
1 check passed
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.

Add an eviction-based hibernation wake-restore test (blocked on vitest 4 / vitest-pool-workers >= 0.16.20)

1 participant