Files
alkstore/docs/research/poc-pg-posture-findings.md
T
glm-5.3-flash f9e350bf22 docs: POC #2 findings verified + folded — OQ-ST-03 closed (per-engine drivers)
Review pass in this repo: contract suite re-verified (11/11 pass under
--test-threads=1 against the harness server; default parallel runs
interfere across tests via the shared db/channels engine_for_test
harness — each test spawns its own listener on poc:q/s/n and truncates
shared tables, so parallel tests receive each other's notifications and
race truncates). Recorded as a harness caveat in the findings with the
Phase 1 note (per-test namespaces), NOT as a contract failure — every
test passes in isolation. Findings invocation note + artifacts section
updated; phase-0 frontmatter carries the verification qualifier.
OQ-ST-03 closure text already folded by the POC session stands.
2026-10-04 17:32:43 +00:00

499 lines
27 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: findings
title: "POC #2 findings — Postgres engine posture: LISTEN/NOTIFY wiring, the pool tx-seam, and reactive parity on tokio-postgres"
last_updated: 2026-10-04
---
# POC #2 findings — Postgres engine posture
**Verdict: PASS — tokio-postgres + deadpool-postgres carries the unified
surface with the transactional property intact, and LISTEN beats poll
decisively. OQ-ST-03 closes with per-engine drivers (rusqlite+honker-core
for SQLite, tokio-postgres+deadpool for Postgres); OQ-ST-04's
contract-pinning now has measured ground on both engines.** All three
sub-modules returned affirmative findings; the sqlx `PgListener` fallback
was never needed and stays retired. Every shared contract property from
POC #1 holds on Postgres, plus the pg-native facts (in-tx NOTIFY delivers
at commit; pooled connections cannot deliver notifications — deadpool#360
— pinned as our own test).
POC code: `/workspace/alkstore-pg-posture-poc` (standalone crate,
published-deps-only, same harness conventions as POC #1). Server:
dockerized `postgres:16-alpine` on :15432 (`pglo-poc` container, db
`blobs`, user/password `postgres/poc` — the alkblobs POC harness
convention). Toolchain: rustc/cargo 1.99.0.
## Versions / dependency surface (verified 2026-10-04)
- `tokio-postgres` 0.7.18 (updated 2026-09-03, actively maintained,
MIT/Apache-2.0). `with-serde_json-1` feature for JSONB/Value binding.
- `deadpool-postgres` 0.14.2 (pool + per-connection statement cache;
`RecyclingMethod::Fast` does not clobber LISTEN state — it recycles
with a health query only — which is also why a LISTEN-registered
connection *returning to the pool* keeps its registration but loses
the delivery path; see the #360 section).
- `postgres-notify` 0.3.8 (MIT, single maintainer, 5.9k downloads total,
~1.5k recent; published 2026-01-07 by the sole owner). Verified
in-probe; quirks below under Sub-module L (b).
- No version conflicts, no vendoring needed. The pg engine is
single-driver by construction (per OQ-ST-02's split); no
libsqlite3-sys-style link collision exists on this side.
## Sub-module L (listen plumbing) — findings
### The #360 constraint: verified empirically, pinned as a test
The spec's requirement (dedicated non-pooled LISTEN connection) was
asserted with our own probe rather than trusted from the issue tracker:
- `tests/contract.rs::pooled_listen_registers_but_cannot_deliver` —
three-part honest probe: (1) `LISTEN` issued through a pooled
deadpool connection **registers** server-side (the session shows it
in `pg_listening_channels()`), (2) the registered notification is
**never deliverable** (no delivery API exists on the pooled object —
verified by source read: deadpool 0.14.2's connect task
`spawn(async move { connection.await })` plain-awaits the
tokio-postgres `Connection` future, whose `Future` impl discards all
async messages; `ClientWrapper` exposes no notification surface
whatsoever), and (3) the control — a dedicated connection driven by
`poll_message` — delivers the identical notification immediately.
- Source-verified against the installed 0.14.2 (not just the issue):
`deadpool-postgres/src/lib.rs` lines ~221 and ~311 —
`create()` spawns the connection task with a bare `connection.await`,
and `ClientWrapper::drop` aborts it. **deadpool#360 remains open.**
Consequence recorded for the engine: the listener connection is a
*separate budget line*, outside the pool, per process. Pool
`max_size + 1` per LISTEN-ing process (pgdiag-st-4 verified the
accounting end-to-end: pool size 6 observed + 1 listener + probe conns
= exactly the server-side count).
### Hand-rolled forwarder (a) — built and measured (~90 lines)
`src/listen.rs`: dedicated connection per listener process, a spawned
task driving `Connection::poll_message` and fanning out into a tokio
`broadcast` channel (bounded 1024; lag is surfaced, not silent — the
bridge logs `Lagged(n)` and keeps going). Reconnect policy: exponential
backoff 50 ms → 2 s cap; re-LISTEN after every reconnect (the LISTEN
SQL is re-issued from the channel list on each reconnect), plus a
**synthetic reconnect wake** on a reserved channel after each
successful re-LISTEN — that synthetic wake is how the no-replay hole
is communicated to subscribers (below).
**Two deadlocks found and fixed during the build — both are exactly
the failure modes a hand-rolled forwarder owns and both are now pinned
by the passing tests:**
1. **Query-vs-poll starvation deadlock.** With the raw `Connection`
polled for async messages, any *client* query (`batch_execute`)
awaits its response through the same connection — if the poll loop
task is spawned *after* the first client query, the query never
completes. The poll loop must be running *before* the first query.
(The initial version deadlocked the LISTEN itself; caught in-probe,
restructured, pinned by every listen test passing.)
2. **Client-drop closes the connection.** Dropping
`tokio_postgres::Client` terminates the server session even if the
`Connection` task still runs. A long-lived listener must keep the
`Client` alive for the listener's lifetime (`std::mem::forget` in
the one-shot probe path; ownership in the shared listener path).
Verified in-probe (`examples/raw_listen_selfcheck.rs` etc).
Neither failure mode was visible from the docs; both are the "every
failure mode is maintained by us" cost the spec predicted for the
hand-rolled posture. They are contained (~90 lines, two pitfalls) and
now test-pinned.
### `postgres-notify` 0.3.8 (b) — built, verified working, with four documented quirks
`src/pgnotify.rs` wraps `PGRobustClient` as the listener substrate.
Verified in-probe (`pnlisten` command + `examples/pn_reconnect_flow.rs`):
1. **Initial connect does NOT execute the connect_script** (source:
`PGClient::connect` never runs it; only `reconnect()` does). The
initial LISTEN must be issued explicitly via `subscribe_notify` —
which conveniently registers channels into the config's
`subscriptions` set that the reconnect script re-issues. The spec's
caveat ("restoration behavior must be verified by test") was
sharper than doc'd: **restoration works on reconnect, but the first
LISTEN is entirely yours.**
2. **Reconnect is lazy.** The polling task reports a drop via a
`Disconnected` callback, but `reconnect()` only runs when the next
*query* fails its connection check. A killed listener connection
sits disconnected until the client issues something. The engine's
listener loop would need a periodic keepalive/trigger query to
make recovery timely — more machinery ours-with-a-dependency, not
fewer.
3. **Auto-reconnect + re-LISTEN verified:** backend kill (by pid, via
`pg_terminate_backend`) → `Disconnected` callback → trigger query →
`Reconnect` (attempt #1, backoff 500 ms base) → `Connected` → LISTEN
re-issued from the script → post-reconnect notification delivered.
`PASS pnlisten: reconnect + connect_script re-LISTEN verified`.
4. **`issue_listen`/`issue_unlisten` do not quote identifiers** —
`LISTEN poc:pn` is a syntax error at the server. Channel names
through this crate must be quote-free (letters/digits/underscore).
Also `application_name` (config field) is interpolated into the
connect *script*, so it does not apply to the initial connection
either — server-side kill-by-name targeting misses the initial
connection.
Comparison verdict (preference recorded, this is OQ-ST-04/OQ-ST-06
input, not a Phase 0 ADR): **(a) hand-rolled forwarder wins** — the
hand-rolled version is ~90 lines with immediate reconnect (no lazy
trigger dependency), full identifier control (quoted channels — our
`poc:` prefixes work), no dependency posture (single-maintainer crate
with 1.5k recent downloads and behavior quirks the docs misdescribe),
and the two deadlock pitfalls are already *learned and pinned*. The
(b) crate would still force us to own a keepalive + reconnect trigger
loop on top (its reconnect laziness), eroding most of the "turnkey"
advantage. Posture (b) stays recorded as the fallback if a future
upstream fix lands (channel quoting + eager reconnect) and the
maintenance posture improves.
### Multi-channel and fan-out
- One LISTEN connection serves N channels: verified (`LISTEN a;
LISTEN b; LISTEN c;` on one connection, all three deliver —
`shared_listener_multichannel.rs`, and every contract test runs
three channels through one listener). No per-channel connection is
ever warranted at this scale; fan-out to per-subscriber receivers is
a broadcast split, ~zero cost.
- 4 subscribers on one channel, every one wakes: verified
(`listen_wake_fanout_and_burst`).
### Payload boundary
`pg_notify` ≤ 8000 bytes confirmed. Probe: 7700-byte payload delivers;
the engine checks the limit client-side and returns a typed
`PayloadTooLarge` error before the round-trip for 8100 bytes
(`notify_payload_boundary`). Honest contract recorded: notify payloads
are hints; large payloads ride a table row with the id in the
notification (the honker-outbox shape, pg-native).
## Sub-module T (tx-seam over the pool) — findings
Both candidate shapes implemented:
- **(a) caller-owned tx handle** (`OwnedTxHandle`): the trait's
`begin_tx` checks a pooled connection out, issues `BEGIN`, and hands
the caller a handle owning the `Object`. `*_tx` methods downcast the
handle (`as_any_mut`, same as POC #1's sketch) and issue their SQL
through it. Commit/rollback return the object to the pool.
- **(b) closure-scoped** (`with_tx`): pool checkout + BEGIN + closure
+ COMMIT/ROLLBACK inside one future; the caller never holds the
transaction.
**The load-bearing structural finding: the whole SQLite side's
thread-affinity rigging does not exist on Postgres — deliberately.**
POC #1's SQLite tx handle needed an `Arc<Mutex<Option<Connection>>>`
with every op round-tripping `spawn_blocking`, because rusqlite's
connection is neither `Send`-across-await nor `Sync`, and the handle
had to be callable from async trait methods. tokio-postgres's `Client`
(and deadpool's `Object`) **is `Send + Sync`** (request-channel
design: `Arc<InnerClient>` + unbounded mpsc), verified by a
compile-time probe in this POC (`tmp sendsync` check: `Client` and
`Object` both asserted `Send + Sync`). So shape (a)'s handle is
`Option<Object>` held **directly**, ops are straight `.await`s, no
mutex, no spawn_blocking, no per-op hop cost. The `as_any_mut`
downcast friction from POC #1 remains (same trait shape), but the
thread-affinity friction does not carry over. This is the honest
per-engine delta the unified trait must absorb: same *seam shape*, but
the PostgreSQL handle is free to be held across await points while the
SQLite handle must own a blocking thread.
- **Commit-atomicity property holds and was verified directly:**
`NOTIFY` issued inside the caller's tx (via `pg_notify($1,$2)`) is
delivered to listeners **only at commit** of the sending transaction
(`notify_fires_only_on_commit`: notify in an open tx → listener
silent for 400 ms → commit → delivery < 3 s; `tx_enqueue_rollback_…
drops_all_ghost_free`: rollback drops the job row, the business row,
*and* the notification — no ghosts anywhere). This is pg's native
statement-vs-commit semantics, exactly the analogue of honker's
notify-in-tx atomicity — no emulation needed.
- **Read-your-writes under `read committed`** (the honest default):
verified both directions (in-tx reads see own writes; post-commit
reads from other pool connections see the commit). No surprises.
- **Shape (a) vs (b) for the engine posture:** (a) is the viable
contract seam (long transactions compose naturally; the caller drives
begin/commit — which is exactly what POC #1's `TxHandle` sketch
assumed). (b) works for self-contained ops but composes poorly with
the caching-subscriber pattern (state can't outlive the closure).
Preference recorded: **(a)**, with (b) available as a wrapper —
note (b) is *implementable over* (a) trivially but not vice versa.
Deadlock risk on (a): none found (the connection is checked out
exclusively; no re-entrant checkout happens inside the handle's ops).
- **Prepared-statement discipline:** deadpool's per-connection
statement cache is keyed per pooled connection and the cache is
re-usable across checkouts; with `RecyclingMethod::Fast` there is no
`DISCARD ALL`/`DEALLOCATE` recycling, so the "prepared statement
s1 does not exist" failure (alkblobs POC-era) does not recur: the
engine's claim SQL is prepared implicitly through the same pooled
connection repeatedly during the probes with zero errors (n=3000
seam iterations ×2 sync postures + pollvlisten's 12 × 60 claims).
The queue SQL is ordinary SQL — no manual prepare discipline needed
at POC scale; noting the engine crate should keep using
`prepare_cached` if it wants explicit caching (deadpool's
`ClientWrapper::prepare_cached` exists for exactly this).
## Sub-module W (wake-vs-poll parity) — findings
### LISTEN-driven wake latency (pgdiag-st-2)
300 notify-commits at 25 ms spacing, listener attached before first
commit, delivery = wake-on-channel after commit:
- **p50 = 1.10–1.15 ms, p99 = 1.42–1.45 ms, max = 1.49–2.21 ms,
300/300 delivered (0 missed).**
Cross-engine relative claim (contract input): this is *the same
sub-2.5 ms* order as POC #1's SQLite watcher at its 1 ms cadence (p50
1.40 ms arm-A / 2.15 ms arm-B) — the wake layer is not the Postgres
engine's weakness; it's *tighter* than poll-based wake and
structurally push (nothing polls: the server delivers).
- **Burst-stress (30 rapid commits, no spacing): 30/30 delivered**
(both release and debug runs; commit burst executes at ~200–520
commits/s, all 30 per-payload notifications delivered). LISTEN does
not coalesce the way a poller does — each committed NOTIFY is
delivered. The per-payload assert (not per-count) is what our
contract tests pin: `seen.contains(payload_i)` for every i.
- **Re-attach storm** (N listeners × M channels): the shape is one
listener connection with N broadcast subscribers — re-attach is a
broadcast re-subscribe, no server round-trips; the reconnect path
re-issues M LISTENs on the one connection (verified in the reconnect
test; the storm is O(M) SQL statements once per reconnect).
### LISTEN connection failure and the replay hole
`listener_reconnect_recovery_and_replay_hole`: listener connection
killed mid-subscription (`pg_terminate_backend` targeted at the
listener's application_name — the listener sets
`application_name('alkstore-pg-poc-listener')` precisely to be
kill-targetable and diagnosable) →
- the commit during the gap is **not re-delivered** (LISTEN has no
replay — the honest contract the spec predicted holds);
- the listener reconnects (exponential backoff 50 ms base) and
re-LISTENs;
- **a synthetic wake is broadcast on a reserved channel** (`__listener_
reconnected__`) when the connection re-establishes — this is the
mechanism that turns the no-replay hole into a *recoverable* event:
subscribers get `Disconnected`-ish signal via the wake and re-read
state. Verified: reconnect wake arrives; post-reconnect deliveries
work; state re-read is complete (all three commits — pre-kill,
in-gap, post-reconnect — visible to a fresh consumer read despite
the in-gap notification never being delivered);
- the assertion `!saw_replay` pins the no-replay honesty.
This is the "on reconnect, wake all subscribers once" recovery the
spec sketched, now implemented and pinned. The engine's subscriber
contract inherits it: wake = opaque hint + re-read; gaps surface as a
reconnect wake, not as lost silence (the SQLite side's missed-wake
stress tests the overtriggering twin of this contract).
### Poll-vs-listen claim latency (pgdiag-st-3) — the decisive measurement
Enqueue→claim latency, one job in flight, 1/8/32 claimants, both
postures, release build (debug numbers were consistent):
| posture | claimants | p50 ms | p90 | p99 | max |
|---|---|---|---|---|---|
| poll (50 ms interval) | 1 | 50.0 | 50.8 | 65.3 | 65.3 |
| poll | 8 | 32.5 | 36.7 | 46.5 | 46.5 |
| poll | 32 | 40.4 | 49.3 | 51.1 | 51.1 |
| **listen** | 1 | **6.0** | 6.4 | 8.6 | 8.6 |
| **listen** | 8 | **3.1** | 7.0 | 7.6 | 7.6 |
| **listen** | 32 | **5.7** | 6.6 | 10.6 | 10.6 |
LISTEN-driven claiming beats even a *50 ms* poll by **5–16× at p50**;
against pgboss-style 1 s default poll intervals the gap would be
~100–300×. The claimants-scaling is flat-to-better (multiple claimants
absorb wakeup jitter). **The LISTEN-driven claim path with re-poll
safety net is the engine's default consumption posture; poll-only
(interval-tunable) remains the fallback when a LISTEN connection is
unavailable/not wanted.** This decides the pg half of the
reactivity-vs-pgboss gap: the push channel pgboss-rs lacks, this
engine gets, measured.
## The seam/cost probe (pgdiag-st-1)
`begin + enqueue_tx + commit` per iteration through the pool, n=3000,
release, sequential (POC #1's seam workload's pg twin):
- **`synchronous_commit=on` (ship config): p50 = 2.40 ms, p90 = 2.73,
p99 = 3.00, max = 6.93 ms** (0 fails);
- **`synchronous_commit=off`: p50 = 2.26 ms, p90 = 2.66, p99 = 3.02,
max = 40.9 ms** (0 fails).
Reading (shape, not absolute — different machines/databases):
- The pg seam is flat and tight at p50–p99 (~2.3–3 ms) — the pool
checkout + BEGIN + INSERT + COMMIT round-trip cost with the
per-connection statement cache warm. The `=off` knob does not
*materially* change p50–p99 here (the local docker fsync floor is
small; the knob would matter more on replicated/farther storage).
The max-tail differences (~7 ms vs ~41 ms) reflect exactly that:
`=on` pays fsync per commit predictably; `=off` batches, occasionally
longer flush batches. **`synchronous_commit` remains a per-session
durability knob with honest trade shape — measured, both postures
reported; per-session SET mechanics verified (pool.rs sets it via
the connect options; `set_sync_commit_session` via SET).**
- Cross-engine: SQLite's seam (same workload shape) was 0.354/0.183 ms
(arm A) vs sqlx's 0.707 ms in POC #1 — i.e., Postgres' transactional
seam on the network stack costs ~3–10× the SQLite one here, which
is the honest expected order (network round-trips vs in-process
file writes). The unified trait absorbs this as "engine costs
differ by orders for the same op" — the per-engine performance
expectations must not be baked into the shared contract numbers.
- Raw floor re-measure (methodology cross-check): prepared SELECT
round-trip p50 = **284 µs** (p99 344 µs) — consistent with the
POC #5-era ~150–500 µs claim, same box, docker-hop counted (the
docker bridge adds overhead vs the earlier bare box; the *shape*
cross-checks). Fresh-session connect: p50 ≈ 23 ms — matches the
~19–25 ms B2-era figure and validates the pooling discipline's
value (~2.4 ms amortized vs 23 ms fresh).
## Property tests (the contract suite) — all green, sequentially
`tests/contract.rs` — 11 tests, pg twin of POC #1's suite, all shared
assertions reused. **Invocation note (verified 2026-10-04 in this
repo's review pass): the suite passes under `cargo test --test contract
-- --test-threads=1` in one process; default parallel execution fails
6–8 tests by cross-test interference, not by contract failure — each
test builds its own engine + listener over the same database and
channels (`poc:q/s/n`) and truncates the shared tables, so parallel
tests receive each other's notifications and race each other's
truncates ("delivered before commit", "jobs lost", incomplete
state re-reads). Every test passes in isolation; the harness lacks
per-test channel/table namespaces, which a Phase 1 test harness must
have as a first-class feature (the same shared-server parallel-suite
shape alkblobs' `--all-features` gates solve with engine-scoped
namespaces). Recorded as a harness caveat — none of the failure modes
are contract failures.**
| Property | Result |
|---|---|
| enqueue+business+notify in one caller tx; rollback drops all (no ghosts: queue, stream, notification) | ✅ |
| exactly-once claim under 4 concurrent producers + 4 concurrent claimants (SKIP LOCKED claim SQL) | ✅ |
| 40 jobs 4 producers → 40 unique claims + full ack + zero ghosts | ✅ |
| stream read/save offset through caller tx; rollback drops; offset advances | ✅ |
| lock acquire/release/renew with TTL; expiry re-acquirable | ✅ |
| fan-out: 4 subscribers × 1 channel, every one wakes | ✅ |
| burst 30 commits: per-payload 30/30 delivered; state re-read complete | ✅ |
| listen-wake queue claim path (commit → notify → wake → claim) | ✅ |
| **in-tx NOTIFY delivers only on commit** (not at statement time) | ✅ |
| pooled-LISTEN-discard (#360): registers but never delivers; dedicated control delivers | ✅ |
| listener-kill → reconnect-wake → no replay of gap notify → post-commit delivery → state re-read complete | ✅ |
| notify payload boundary (7700 ok; 8100 rejected client-side) | ✅ |
| read-your-writes under read committed (in-tx + post-commit) | ✅ |
## Decision gate — evaluated
- **Contract:** every shared property holds. The commit-atomicity
property via in-tx NOTIFY is verified natively (`notify_fires_only_
on_commit` + rollback-ghost tests); exactly-once claim holds under
concurrency via `FOR UPDATE SKIP LOCKED` claim SQL. ✅
- **Seam:** the caller-tx shape survives with measured costs (2.4 ms
p50 ship config). Preference for shape (a) recorded with evidence.
The `*_tx` seam is *cheaper to hold* on pg than on SQLite (Send+Sync
client; no spawn_blocking round-trips). ✅
- **Wake:** LISTEN plumbing is robust (reconnect honest, synthetic
reconnect-wake closes the replay hole operationally, fan-out
correct) and *decisively beats poll* (3.1–6 ms vs 32–50 ms at
comparable claimant counts; 300/300 isolated wakes at 1.1 ms p50).
✅
**Verdict: PASS on all three gate conditions. OQ-ST-03 closes with
"per-engine drivers: rusqlite+honker-core (SQLite) / tokio-postgres +
deadpool-postgres (Postgres)". The sqlx PgListener fallback posture is
retired — no condition requiring it fired.**
## What feeds where
- **OQ-ST-03 (Postgres half): resolved** — tokio-postgres + deadpool as
recorded; per-engine-crate split (OQ-ST-02) keeps binary drivers
single, eliminating the link-collision class of problems the SQLite
arm documented.
- **OQ-ST-04 (reactive contract, pg side): measured ground.**
- Wake contract numbers: LISTEN p50 ≈ 1.1 ms (push), claim-latency
p50 ≈ 3–6 ms end-to-end; poll-only fallback ≈ interval-bound.
- Listener semantics: no replay; recovery = synthetic reconnect-wake
+ re-read; subscriber contract stays opaque-wake + re-read
(identical shape to SQLite's overtriggering watcher — the two
engines now share the *same* wake contract).
- Delivery-guarantee split confirmed native: notify = fire-and-forget
(commit-atomic, at-most-once per listener session, no replay);
streams = durable with offsets. The trait must NOT promise replay
under `listen()` — both engines are honest only as opaque wake.
- Tx-seam: caller-held tx handle (`*_tx` on a handle) is the seam on
both engines; pg's handle is async-native (Send+Sync client) while
SQLite's is a bridged lease. Same shape, different bridging
mechanism — the core-crate `TxHandle` trait from POC #1's sketch
stands, with the pg handle as the trivial-instance case.
- **OQ-ST-05 (queue semantics): re-derive-on-tokio-postgres posture
strengthened** — the minimal queue table + `SKIP LOCKED` claim +
LISTEN wake is ~40 lines of SQL around the pool, the exactly-once
property rides pg's native serialization semantics, and the
pgboss-rs schema family remains design-reference (its *states*
matter only for retry/dead-letter depth, which is OQ-ST-05's design
work; the POC's minimal table sufficed for every property).
- **OQ-ST-08 (multi-host): the pg engine is natively multi-host** —
the property tests ran all-through-network (docker bridge), the
listener/wake machinery is connection-based (per-process), and
nothing assumes single-host. The engine-capability question remains
for the trait surface, but the pg side has no single-host assumption
to remove.
- **Dependency postures (OQ-ST-06-adjacent, per-subsystem votes):**
honker-core = published-library (POC #1); deadpool-postgres +
tokio-postgres = published-library (this POC — clean, zero
conflicts); postgres-notify = **derive-not-adopt** (the 0.3.8 quirks
+ single-maintainer posture + our forwarder being already-learned
territory; fallback if upstream improves).
## Honest caveats
- Same single-box posture as POC #1 (8-core shared dev box, dockerized
server over the docker bridge). Absolute numbers carry the docker-hop
(~284 µs raw floor here vs the ~150 µs-era bare-metal figure);
relative/shape claims are the deliverable and were consistent across
re-runs (debug and release).
- The LISTEN re-attach "storm" is tested as one-connection-N-channels
re-listen (our engine shape); a *per-listener-connection* storm (N
processes each × M channels at reconnect) is bounded by N × M
separate LISTEN statements — plausible but not measured (the engine
shape never exercises it; recording as assumption with the reasoning).
- The queue table is the minimal spec-out-of-scope shape (enqueue,
claim, ack, visibility column present but not scheduled/retried).
Retry/backoff/dead-letter depth remains OQ-ST-05's design work; the
*transactional and claim* properties this POC pins are the hard
driver-coupled part and they pass.
- `postgres-notify` verification was against 0.3.8's published source
(crate downloaded into the cargo registry cache); behavior quirks
(lazy reconnect, initial-connect script skip, identifier quoting)
are recorded from that source plus in-probe verification, cited as
of 2026-10-04.
- The synthetic reconnect-wake channel is a reserved name
(`__listener_reconnected__`) — engine consumers choosing their own
channel names must not collide; a prefix convention or a separate
meta-channel namespace is Phase 1 contract surface.
## Artifacts
- POC crate: `/workspace/alkstore-pg-posture-poc` (self-contained,
published-deps-only).
- Tests: `cargo test` → `tests/contract.rs` (11 tests; sequential
invocation: all pass; parallel default invocation interferes across
tests via the shared db/channels — see the invocation note above).
- Probe binary: `cargo run --release -- seam|watch|burst|claim|pollvlisten|pool|pnlisten|smoke`
per the binary's help (wake command prints the notify-latency table;
pollvlisten the consumption-posture table).
- Probe examples (`examples/pgdiag_st1.rs` raw floor + fresh session;
`pgdiag_st4.rs` pool-budget accounting; `raw_listen_*.rs`,
`pn_*.rs`, `shared_listener_*.rs`, `attach_control.rs` the
listener-substrate steps; each runs standalone against the harness
server).
- Harness: docker `pglo-poc` (`postgres:16-alpine`, :15432, db
`blobs`). Note for re-runs: the container also carries
`synchronous_commit=off` as a server default from its earlier POC
use; the POC's pools set `synchronous_commit` per connection via
connect options, so probes are self-consistent — but `SHOW
synchronous_commit` outside the POC reads `off` on the server.