Postgres engine: open constructor, PgOpts, pool + listener wiring — forwarder skeleton with the POC-pinned pitfalls structurally excluded, seam error mappings, wave-3 stub surface (task pg-engine-open-opts)
This commit is contained in:
1 parent
2f1353bd41
commit
c6a7eeaa45
9 files changed
+1815
-13
No files matched your search
+133
-10
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: pg-engine-open-opts
|
||||
name: Postgres engine — `open` constructor, `PgOpts`, pool + listener wiring
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: [pg-engine-schema]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
@@ -77,20 +77,20 @@ name; `synchronous_commit` observable via `SHOW`).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `PgOpts` with connection config, `schema` (default
|
||||
- [x] `PgOpts` with connection config, `schema` (default
|
||||
`alkstore`), `max_size`, `synchronous_commit` (default on);
|
||||
documented; not `#[non_exhaustive]`
|
||||
- [ ] `open` boots pool + bootstrap + forwarder skeleton; failure at
|
||||
- [x] `open` boots pool + bootstrap + forwarder skeleton; failure at
|
||||
any step is a typed `Database` error (source chain preserved)
|
||||
- [ ] Forwarder skeleton: dedicated non-pooled connection, poll loop
|
||||
- [x] Forwarder skeleton: dedicated non-pooled connection, poll loop
|
||||
before first query, bounded broadcast fanout, dynamic channel
|
||||
set; the two POC deadlock pitfalls structurally excluded
|
||||
- [ ] `close()`/`Drop` teardown clean (listener connection dropped,
|
||||
- [x] `close()`/`Drop` teardown clean (listener connection dropped,
|
||||
pool closed); post-close ops fail closed
|
||||
- [ ] Error-mapping helpers exist and are the documented reuse point
|
||||
- [ ] Crate docs carry the `# Posture` statements (multi-host,
|
||||
- [x] Error-mapping helpers exist and are the documented reuse point
|
||||
- [x] Crate docs carry the `# Posture` statements (multi-host,
|
||||
listener budget, no spawn_blocking)
|
||||
- [ ] `cargo test -p alkstore-postgres` (harness server), clippy
|
||||
- [x] `cargo test -p alkstore-postgres` (harness server), clippy
|
||||
`-D warnings`, fmt clean; gates green server-less (tests skip)
|
||||
|
||||
## References
|
||||
@@ -106,8 +106,131 @@ name; `synchronous_commit` observable via `SHOW`).
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
Decisions of record made while implementing (the description didn't
|
||||
pin them):
|
||||
|
||||
- **The connection config rides `open`'s first argument, not `PgOpts`**
|
||||
(the description's bullet list carried it under both; the signature
|
||||
line `open(config, opts)` and ADR-008 §6's
|
||||
`alkstore_postgres::open(url, PgOpts)` pin decide it): `PgOpts`
|
||||
carries schema/max_size/synchronous_commit only. `open` is **async**
|
||||
(`connect` is; the SQLite twin's sync `open` was file-only). Fail
|
||||
posture: unparseable config → `pg_error` (a
|
||||
`tokio_postgres::Error::ConfigParse`); refused connection →
|
||||
`pg_error` — both typed `Database`, source chain preserved, pinned
|
||||
by test.
|
||||
- **`open` connects the listener inline, not inside the task** — an
|
||||
unreachable server must fail `open` synchronously (the
|
||||
unreachable-server acceptance row); the forwarder task takes over
|
||||
the established connection and owns reconnects from there.
|
||||
- **Forwarder skeleton shape** (`forwarder.rs`, the POC's
|
||||
~90-line shape restructured): a `LoopCtx` struct carries the loop's
|
||||
state (clippy's too-many-arguments); per connection generation the
|
||||
loop spawns a **poll task owning the `Connection`** (`poll_message`
|
||||
→ bounded broadcast 1024, lag surfaced) before any client query —
|
||||
pitfall 1's structural exclusion held permanently (the command
|
||||
select keeps the poll task co-resident with LISTEN/UNLISTEN client
|
||||
queries every generation); the `Client` is owned by the loop
|
||||
(pitfall 2). Connection generations carry an `Option<ListenerConnection>`
|
||||
slot; `ListenerConnection` is the `NoTls`
|
||||
`Connection<Socket, NoTlsStream>` alias. Commands (LISTEN/UNLISTEN)
|
||||
ride an mpsc queue the loop issues between polls (reconnect replays
|
||||
them harmlessly alongside the registry re-issue); the shutdown
|
||||
watch flips at `close`/`Drop`.
|
||||
- **Dynamic channel set**: a `BTreeSet`-backed registry
|
||||
(`Forwarder::{register,unregister}`), registry-write-first ordering
|
||||
(a registration survives a connection death between write and
|
||||
LISTEN — the reconnect re-issues from the snapshot).
|
||||
`register` fails `Database` only mid-reconnect (transient, honest
|
||||
state surfaced). `subscribe()` on the fanout exposes the raw
|
||||
notification broadcast — the notify-listen task bridges receivers
|
||||
onto it. `RawNotification { channel }` is the skeleton's broadcast
|
||||
content (the `Wake` mapping is that task's).
|
||||
- **The synthetic reconnect-wake is broadcast by the skeleton** (the
|
||||
reserved channel `__alkstore_listener_reconnected__` after every
|
||||
successful re-LISTEN except the first) — the notify-listen task owns
|
||||
the wake-content/receiver semantics; the skeleton's fanout already
|
||||
carries it (pinned by the backend-kill test).
|
||||
- **Listener `application_name` is per-instance**:
|
||||
`alkstore-pg-listener-{pid}-{seq}` — the deployment.md ops
|
||||
kill-targetability with a unique suffix so parallel
|
||||
stores/tests are distinguishable in `pg_stat_activity` (the
|
||||
outside-the-pool and teardown assertions target the store's own
|
||||
instance). Exposed at `PgStore::listener_application_name()` for
|
||||
the notify-listen task's backend-kill tests.
|
||||
- **Liveness is verified, not assumed**: `connected` starts true (the
|
||||
handed-in connection is fresh) and a no-channel generation probes
|
||||
with `SELECT 1` (a dead connection would otherwise report live).
|
||||
- **Empty `PgOpts::schema` falls back to `DEFAULT_SCHEMA`** (defensive;
|
||||
the documented default is `.default()`-driven).
|
||||
- **Error mappings** (`seam.rs`, the mechanism tasks' reuse surface):
|
||||
`pg_error(tokio_postgres::Error)`, `pool_error(PoolError)`,
|
||||
`build_error(BuildError)` (the pool-build failure arm —
|
||||
`PoolBuilder::build` returns `BuildError`, not
|
||||
`CreatePoolError`), `database_error(msg)` (the SQLite twin's
|
||||
io-Error string helper).
|
||||
- **No runtime dep additions beyond the existing set** (tokio-postgres,
|
||||
deadpool-postgres, tokio, thiserror already carried by the schema
|
||||
task; `serde_json` added — core's payload type appears in the trait
|
||||
stubs' signatures). TLS: `NoTls` on both paths (the consumer's
|
||||
sslmode/TLS story is a deployment concern; noted in the forwarder
|
||||
module docs, the notify-listen task revisits listener TLS if a task
|
||||
or ADR ever demands it).
|
||||
- **Trait stubs** carry the closed-store check *ahead of* the stub
|
||||
error (`close()`/`Drop` make every arm fail closed immediately —
|
||||
the post-close fails-closed row is pinned from open onward). Stub
|
||||
messages name their landing task; their text rides the source
|
||||
chain (`Database` Display is opaque — asserted via the chain).
|
||||
- **Machinery accessors are `pub(crate)`** (`pool`, `forwarder`,
|
||||
`schema`, `listener_application_name`) — the mechanism tasks reach
|
||||
them; tests exercise them and the lib-build dead-code gates are
|
||||
`#[cfg_attr(not(test), allow(dead_code))]`-carried until the wiring
|
||||
tasks land (the wave-3 intermediate posture).
|
||||
- **Harness reuse**: the schema task's env-carried DSN convention
|
||||
(`ALKSTORE_PG_HOST/PORT/USER/PASSWORD/DB`), schema-per-test
|
||||
isolation, skip-clean server-less; the shared default schema is
|
||||
never dropped by tests; the POC's fanout shape (broadcast 1024,
|
||||
lag-surfaced) and backoff constants (50 ms → 2 s) are re-owned as
|
||||
named constants.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Stood up `alkstore-postgres`'s public surface: `PgOpts` (schema —
|
||||
default `alkstore` per ADR-010 §8; `max_size` — default
|
||||
`DEFAULT_MAX_SIZE = 8`, the deployment budget line with the +1
|
||||
listener outside the pool; `synchronous_commit` — default on, wired as
|
||||
connect-options `-c` per-session SET per the POC's verified mechanics;
|
||||
not `#[non_exhaustive]`, `Debug`+`Clone`+`Default` per ADR-017 §3's
|
||||
opts exemption), the `async open(config: &str, opts: PgOpts) ->
|
||||
Result<Box<dyn Store>>` constructor (parse → pool build with
|
||||
`RecyclingMethod::Fast` → pool-checkout schema bootstrap → dedicated
|
||||
non-pooled listener connect + forwarder spawn; every failure a typed
|
||||
`Error::Database` with the source chain preserved), the concrete
|
||||
`PgStore` re-exported (pool + forwarder handle + schema name held
|
||||
behind the `Store` trait; `close()`/`Drop` shut the forwarder down
|
||||
and close the pool — post-close ops fail closed), the LISTEN
|
||||
forwarder skeleton (`forwarder.rs`: dedicated non-pooled connection,
|
||||
poll-first structure excluding both POC-pinned deadlock pitfalls,
|
||||
bounded 1024 broadcast fanout with lag surfaced, dynamic channel set
|
||||
with registry-write-first recovery ordering, reconnect loop with the
|
||||
50 ms → 2 s exponential backoff, synthetic reconnect-wake broadcast on
|
||||
the reserved channel), the seam error mappings (`pg_error`,
|
||||
`pool_error`, `build_error`, `database_error` — the mechanism tasks'
|
||||
documented reuse point), and the full `Store` trait as wave-3-style
|
||||
`Database("… wiring lands with the … task")` stubs with closed-store
|
||||
fail-closed checks. Crate docs carry the `# Posture` statements
|
||||
(multi-host, the listener budget line, natively async / no
|
||||
spawn_blocking). Verified: 12 new open/opts tests
|
||||
(`src/store/open_tests.rs`) + the 9 schema tests green against the
|
||||
harness server (boot round-trip, pool bounds + pool saturation leaving
|
||||
the listener untouched, opts flow: schema name in the created schema +
|
||||
`synchronous_commit` observable via `SHOW` on both settings, pitfall
|
||||
pins — immediate post-open LISTEN + long-after-open delivery — and the
|
||||
outside-the-pool accounting via per-instance `pg_stat_activity`,
|
||||
backend-kill reconnect with reserved-wake + post-reconnect delivery,
|
||||
close/drop teardown with server-side session termination + closed
|
||||
pool + fail-closed ops, unparseable-config and unreachable-server
|
||||
`Database` failures, the stub surface, the seam mappings' source
|
||||
chains); workspace `cargo test` green server-less (236 tests, pg tests
|
||||
skipping per convention); `cargo clippy --all-targets -- -D warnings`
|
||||
and `cargo fmt --check` clean workspace-wide.
|
||||
Reference in new issue
Block a user