- C2: 'at-least-once wake delivery' collapsed to 'best-effort hints' (ADR-006 §1 + core-contract) — coalescing and the pg no-replay hole contradict per-commit wake promises; the guarantee table's row was already correct. - W2: uniform job-handle validity predicate pinned (ADR-010 §2, core-contract, queues.md; verification-backlog row): processing state + unexpired deadline; D-12's missing-check not inherited. - W1: outbox run_once worker semantics pinned in core-contract (pull op, ack/retry-on-curve, no heartbeat in delivery — honker parity, dual-execution window documented). - W3: named-locks tx-seam posture stated (no lock_tx; acquisition is auto-commit; TTL discipline governs). - C1 -> OQ-12: streams depth (key semantics w/ honker ground, event shape, ordering row, retention); method names pinned, depth open. - New find -> OQ-13: the v1 TxHandle surface cannot express the transactional outbox enqueue (derived backing-queue name is reserved-prefix-rejected; honker's raw-Transaction seam unavailable); option set + decision rule sketched. - README resolution order refreshed (OQ-06 resolved); ScheduleOpts + stream consumption-trigger lines added to core-contract.
7.7 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-10-05 |
Postgres engine
The alkstore-postgres engine implements
core-contract.md on tokio-postgres +
deadpool-postgres. This spec records WHAT the engine is internally —
connection architecture, the LISTEN forwarder, queue machinery
re-derivation — not code-level HOW. Decisions live in ADRs; contract
obligations live in the core spec.
Identity and posture
- Single driver, natively async: tokio-postgres 0.7.x +
deadpool-postgres 0.14.x (ADR-004).
No bridge, no
spawn_blocking; the tx handle holds the pooled object directly (client isSend + Sync— POC #2 compile-probe verified). - Multi-host by nature: connections are per-process state; nothing assumes a shared host (ADR-004, verified all-through-network in POC #2). See deployment.md.
- Queue machinery re-derived on this driver with the pg-boss schema
family as design reference
(ADR-005); semantics depth
pinned by ADR-010 — all
engine-owned tables (job, dead, stream, offsets, schedule) in one
PostgreSQL schema (default
alkstore), queues as rows, no per-queue tables.
Connection architecture
- Pool (deadpool) — queries, claims, and all non-transactional
work. Per-connection statement cache,
RecyclingMethod::Fast(noDISCARD ALLrecycling; claim SQL re-prepared implicitly with zero errors at POC scale). - Listener connection — one dedicated, non-pooled connection
per process that listens. Pooled connections cannot carry LISTEN
(deadpool#360 — registration succeeds, delivery is impossible; the
client-wrapper exposes no notification surface, source-verified and
test-pinned as
pooled_listen_registers_but_cannot_deliver). The listener is therefore a per-process budget line outside the pool:max_size + 1per LISTEN-ing process (deployment.md). - Forwarder — the listener's loop:
poll_messagefanning out into a bounded broadcast channel (lag surfaced, not silent), re-LISTEN from the channel list after every reconnect (exponential backoff 50 ms → 2 s cap), and the synthetic reconnect-wake on the reserved channel (__alkstore_listener_reconnected__, ADR-008 §4) — broadcast to every subscriber's receiver, per the wake contract's reconnect-recovery semantics (ADR-006). - One listener serves N channels and N subscribers; re-attach is a broadcast re-subscribe (no server round-trips); per-channel connections are never warranted at this scale (POC-verified).
Mapping the contract
| Contract piece | Engine realization |
|---|---|
| notify / listen | pg_notify(...) inside the caller's tx (delivers at commit — native commit-atomicity, ADR-007); listen() via LISTEN on the forwarder's connection, fanout to receivers |
| streams | durable event table + per-consumer offset cursors; pg_notify as the wake trigger (ADR-006 mechanism split: durable row, LISTEN wake — the pg-boss-family shape) |
| queues | re-derived queue table + FOR UPDATE SKIP LOCKED claim + LISTEN-driven wake with re-poll safety net (default consumption posture, measured 5–16× vs 50 ms poll; poll-only fallback); states/dead-letter/backoff/visibility per ADR-010, all in the engine-owned schema; the curve/stamps arithmetic is computed engine-side per ADR-012 §2 (equivalence with the SQLite engine pinned by the contract suite) |
| named locks | advisory-lock-semantics TTL locks (pg-boss-family design reference; guarantee row pinned by ADR-008 §7) |
| scheduler / outbox | collapse shape (ADR-009): schedule rows in the engine-owned schema, tick re-derived (boundary advance + 64-boundary catch-up cap, honker parity), leadership via the engine's lock machinery on __alkstore_scheduler; outbox = helper over queues |
| begin_tx | pool checkout + BEGIN, returning the caller-held handle (ADR-007) |
| handle ops | straight .awaits through the held object; commit/rollback returns the object to the pool |
Owned failure modes (all test-pinned in POC #2)
Hand-rolling the forwarder means owning its pitfalls — they are learned territory, pinned as passing tests:
- Query-vs-poll starvation deadlock — the poll loop must be running before the first client query on the listener connection.
- Client-drop closes the server session — a long-lived listener
keeps its
Clientalive for the listener's lifetime. - The no-replay hole — commit during a connection gap is never
re-delivered; recovery = reconnect + synthetic wake + consumer
re-read (ADR-006).
The
!saw_replaytest pins the honesty. - Payload boundary —
pg_notify≤ 8000 bytes; client-side checked, typed error before the round-trip. Large payloads ride a table row with the id in the notification (outbox shape). - Read-your-writes —
read committeddefault verified in both directions (in-tx and post-commit).
Constraint also carried: mixed rusqlite+sqlx binaries would need a vendored patch today — excluded by construction in this engine (single driver, ADR-001), noted for the record in ADR-003.
Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| 001 | Crate split | single-driver engine crate |
| 002 | Feature scope | which rows this engine serves |
| 004 | Driver | tokio-postgres + deadpool; hand-rolled forwarder; re-derived queues |
| 005 | Ownership | published libs as-is; postgres-notify derive-not-adopt |
| 006 | Wake contract | LISTEN push, no replay, synthetic reconnect-wake |
| 007 | Tx seam | direct pooled-object handle, no bridging |
| 008 | Contract v1 | pinned surface; reserved reconnect-wake channel string; PayloadTooLarge taxonomy variant |
| 009 | Scheduler collapse | schedule rows in the engine schema, re-derived tick, row-locked fire tx, __alkstore_scheduler leadership |
| 010 | Queue depth | job-stamped opts, equal-jitter backoff, dead-letter move, no-stranded-rows sweep, one engine-owned schema |
| 012 | Fork design | contract-blind substrate (SQLite side); pg engine owns its own curve/stamps arithmetic, equivalence pinned by the contract suite |
Open Questions
Open questions are tracked in open-questions.md. Key questions affecting this document:
- OQ-08: capability surface (shared with deployment.md) (open)
- OQ-12: streams depth (affects this engine's stream realization — event-shape/ordering/retention equivalents on the pg side) (open)
- OQ-13: transactional outbox enqueue shape (affects this
engine's
TxHandleimpl) (open)
Resolved: OQ-09 (scheduler collapse — ADR-009) and OQ-05 (queue semantics depth — ADR-010), 2026-10-05.