6.7 KiB
ADR-006: Wake contract — opaque wake + re-read, and the notify-vs-streams delivery split
Status
Accepted (the contract skeleton the evidence supports); the surface pinning is done — ADR-008 §3/§4 pin the wake type and the reserved strings
Context
The two engines' wake mechanisms are structurally different — SQLite
wakes by a watcher polling PRAGMA data_version (deliver-on-commit,
no server push), Postgres by LISTEN/NOTIFY (server push,
connection-bound, no replay). A naive unification either collapses to
polling behavior on the strong side or promises semantics only one
engine has. Both POCs (2026-10-04) measured the pieces and — the
load-bearing finding — verified that the two engines can share the
same wake contract unchanged: wake is opaque, delivery is not
guaranteed to be precise, and consumers re-read state.
Honker's own processing-guarantees table is the cautionary prior art: per-binding auto-checkpoint-vs-manual-save ambiguity produces different guarantees under one function name. A single-crate version must pick one answer per mechanism, not inherit the table.
Decision
The contract fixes three things:
1. The unified wake contract: opaque wake + re-read state
listen() (and stream/queue subscription wake) delivers an opaque
wake signal: "something changed; re-read the state you care about."
The contract never carries semantic content, ordering promises, or
change descriptions. Consumers that need the actual data re-read it
(the hot-path pattern the ecosystem already uses: a subscriber holding
a cache invalidates on wake and re-reads, instead of re-polling).
- SQLite:
data_versionwatcher fires on commit; wakes coalesce under bursts (overtriggering on purpose — "one indexed SELECT is cheap; a missed wake is a correctness bug"). - Postgres: LISTEN delivers per-notification (no coalescing), and the forwarder's synthetic reconnect-wake covers connection gaps.
- Both: consumer code must be correct if wakes repeat, coalesce, or arrive in any order. Wakes are best-effort hints, not a per-commit delivery guarantee — SQLite coalescing and Postgres's no-replay hole (a commit during a connection gap is never re-delivered) both mean individual changes can get no wake; recovery is consumers' re-read, the reconnect-wake, and idempotence — never a promised per-commit deliver. Exactly-once processing semantics belong to queues/streams (below), never to the wake layer.
Measured ground: wake latency p50 ≈ 1.1–2.2 ms on both engines at default cadence; per-payload burst delivery verified (30/30 on pg, coalesced-but-complete re-read on SQLite).
2. Delivery guarantees, pinned per mechanism (no per-binding table)
| Mechanism | Durability | Replay | Atomicity | Guarantee |
|---|---|---|---|---|
notify / listen |
none | never | commit-atomic (delivers at commit; rollback drops) | fire-and-forget, at-most-once per listener session |
| streams | durable table row | yes, per-consumer offset, replay-on-attach default | publish is commit-atomic | every committed event readable by every consumer that hasn't passed its offset; global FIFO by offset within each stream (read paths yield offset ASC; offsets immutable, never renumbered — ADR-015 §4) |
| queues | durable row | claim/ack model | enqueue is commit-atomic | at-least-once work with visibility timeouts |
- The trait does not promise replay under
listen()— on either engine. Durability+replay needs are what streams are for; this split is the contract's load-bearing line. - Listener sessions start "from now" (SQLite: the watcher's current
state; Postgres:
MAX(id)-equivalent at attach). History belongs to streams. - Wake delivery failures surface, not silence: watcher death closes
subscriber channels (SQLite, honker-core's
WatcherDeathGuardbehavior); Postgres connection gaps surface as the synthetic reconnect-wake on a reserved channel ([ADR-004], [engine-postgres.md]).
3. Reserved names are a contract surface
The Postgres forwarder's synthetic reconnect-wake needs a reserved channel namespace no consumer channel may collide with. The naming convention for reserved/meta channels (and any internal queue/stream names) is pinned in the core contract — exact strings pinned by ADR-008 §4; the existence of a reserved namespace is decided here.
The caching-subscriber pattern rides the same contract
A subscriber that also holds a cache gets invalidation from the
opaque wake + re-read: the contract deliberately does not carry
key-level payloads, so cache clients key their invalidation on their
own read-set, informed by channel names (the channel name is the one
piece of semantic content listen() carries). Key-payload design,
if any consumer needs richer invalidation, is a
consumer-inventory-row-gated extension — not assumed now.
Consequences
Positive
- One wake contract, verified identically on both engines — the "how does a change become visible" question alkstore exists to answer, answered once.
- The delivery-guarantee table is pinned per mechanism, killing the per-binding ambiguity class honker's docs exhibit.
- Consumer code branches on mechanism choice (notify vs streams vs queues), never on engine type (guiding principle 4; the honest single/multi-host line is [deployment.md]'s and OQ-08's, not this contract's). (OQ-08 resolved 2026-10-06 by ADR-016: nowhere at runtime — compile-time engine identity + the documented matrix.)
Negative
- The opaque wake is deliberately less informative than key-payload notify systems; consumers needing fine-grained invalidation pay a re-read or need streams.
- Wake overtriggering on SQLite means consumers must be idempotent on wake (a documented consumer obligation, part of the contract text).
References
docs/research/poc-sqlite-posture-findings.md§Probe 2, §"What feeds where";docs/research/poc-pg-posture-findings.md§Sub-module W.- OQ-ST-04 (
docs/research/phase-0.md) — de-risked by these measurements; the contract remainder resolved by ADR-008 overcore-contract.md. - ADR-002 — notify/listen and streams are first-class; the guarantee split is why both exist.
- ADR-004 — the forwarder owning the Postgres side of this contract.
- ADR-007 — commit-atomicity's mechanism
(the
*_txseam). - core-contract.md — the spec carrying this contract's full surface. [ADR-004]: 004-postgres-driver.md [deployment.md]: ../deployment.md [engine-postgres.md]: ../engine-postgres.md