Files
alkstore/docs/architecture/engine-postgres.md
T
glm-5.3-flash 2949612e2c ADR-012: forked-substrate design — contract-blind boundary, fidelity posture, port deltas
Follow-through on OQ-06/ADR-011: pin the fork's structural decisions
(alkstore-substrate as a vendored path-dep crate, contract-blind API
boundary with contract formulas computed engine-side and pinned
equivalent by the contract suite, keep-the-kept-half API fidelity for
cheap cherry-picks, the W-1/W-2/dead-man's-switch/W-4 port deltas
decided per item, bootstrap re-keying off error-string matching, no
rename migration, deliberate upstream tracking).

Consistency sweep across the doc set for the fork: annotate ADR-003/
005/009/010 and core-contract for superseded ownership facts, fix
schedule-storage table naming (ADR-009 §5, queues.md), re-key ADR-010
§6's notifications hygiene to the at-attach cap the fork scope
realizes, add OQ-11 (scaffold-time residue), and complete both ADR
indexes. Independent review: 0 critical, warnings addressed.
2026-10-05 05:00:55 +00:00

125 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: draft
last_updated: 2026-10-05
---
# Postgres engine
The `alkstore-postgres` engine implements
[core-contract.md](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](decisions/004-postgres-driver.md)).
No bridge, no `spawn_blocking`; the tx handle holds the pooled
object directly (client is `Send + Sync` — POC #2 compile-probe
verified).
- Multi-host by nature: connections are per-process state; nothing
assumes a shared host ([ADR-004](decisions/004-postgres-driver.md),
verified all-through-network in POC #2). See
[deployment.md](deployment.md).
- Queue machinery re-derived on this driver with the pg-boss schema
family as design reference
([ADR-005](decisions/005-dependency-ownership.md)); semantics depth
pinned by [ADR-010](decisions/010-queue-semantics-depth.md) — 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`
(no `DISCARD ALL` recycling; 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 + 1` per LISTEN-ing process
([deployment.md](deployment.md)).
- **Forwarder** — the listener's loop: `poll_message`
fanning 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](decisions/008-contract-v1-pinning.md) §4) — broadcast to
*every* subscriber's receiver, per the wake contract's
reconnect-recovery semantics
([ADR-006](decisions/006-wake-and-delivery-contract.md)).
- 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](decisions/007-transactional-seam.md)); `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](decisions/006-wake-and-delivery-contract.md) 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](decisions/010-queue-semantics-depth.md), all in the engine-owned schema; the curve/stamps arithmetic is computed engine-side per [ADR-012](decisions/012-forked-substrate-design.md) §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](decisions/008-contract-v1-pinning.md) §7) |
| scheduler / outbox | collapse shape ([ADR-009](decisions/009-scheduler-collapse.md)): 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](decisions/007-transactional-seam.md)) |
| handle ops | straight `.await`s 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:
1. **Query-vs-poll starvation deadlock** — the poll loop must be
running before the first client query on the listener connection.
2. **Client-drop closes the server session** — a long-lived listener
keeps its `Client` alive for the listener's lifetime.
3. **The no-replay hole** — commit during a connection gap is never
re-delivered; recovery = reconnect + synthetic wake + consumer
re-read ([ADR-006](decisions/006-wake-and-delivery-contract.md)).
The `!saw_replay` test pins the honesty.
4. **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).
5. **Read-your-writes** — `read committed` default 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](decisions/001-crate-split.md)), noted for
the record in [ADR-003](decisions/003-sqlite-driver.md).
## Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| [001](decisions/001-crate-split.md) | Crate split | single-driver engine crate |
| [002](decisions/002-feature-scope.md) | Feature scope | which rows this engine serves |
| [004](decisions/004-postgres-driver.md) | Driver | tokio-postgres + deadpool; hand-rolled forwarder; re-derived queues |
| [005](decisions/005-dependency-ownership.md) | Ownership | published libs as-is; `postgres-notify` derive-not-adopt |
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | LISTEN push, no replay, synthetic reconnect-wake |
| [007](decisions/007-transactional-seam.md) | Tx seam | direct pooled-object handle, no bridging |
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 | pinned surface; reserved reconnect-wake channel string; `PayloadTooLarge` taxonomy variant |
| [009](decisions/009-scheduler-collapse.md) | Scheduler collapse | schedule rows in the engine schema, re-derived tick, row-locked fire tx, `__alkstore_scheduler` leadership |
| [010](decisions/010-queue-semantics-depth.md) | Queue depth | job-stamped opts, equal-jitter backoff, dead-letter move, no-stranded-rows sweep, one engine-owned schema |
| [012](decisions/012-forked-substrate-design.md) | 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](open-questions.md). Key
questions affecting this document:
- **OQ-08**: capability surface (shared with
[deployment.md](deployment.md)) (open)
Resolved: **OQ-09** (scheduler collapse —
[ADR-009](decisions/009-scheduler-collapse.md)) and **OQ-05** (queue
semantics depth — [ADR-010](decisions/010-queue-semantics-depth.md)),
2026-10-05.