Files
alkstore/tasks/pg-fix-open-path.md
T
glm-5.3-flash 5abd351c86 Wave-6 decomposition (mem engine): eleven tasks per the pg-wave rhythm — foundations (crate scaffold, MemStore::new isolation, guarded state core, wake routing, wasm32 gate from birth), seam-tx (the load-bearing tx overlay: eleven staged _tx methods, commit-merge + wake-at-commit — pg-fix-tx-wake's discipline structural from the start, drop = discard trivial, with_tx panic posture), mechanisms mutually parallel (notify-listen close-at-engine-drop/no-replay/never-coalesced; queues = ADR-010 machine driver-free with the equal-jitter curve and opts resolution as ADR-012 §2's third owner; streams log+offsets+trim; locks TTL registry, no busy-path analog), scheduler-outbox (tick/catch-up-64/leadership through the __alkstore_scheduler lock, engine-uniform per ADR-009 §4; outbox over queues with the 60/5/5 seam stamp), integration (suite third StoreFactory column green on host under a current-thread flavor, wasm32 acceptance items, engine-mem.md to implementation-complete), and the review-wave-6 gate (born-pinned audit: conformance read, discharge-map wiring, wasm items, three-way equivalence green, class-1 window discipline). Window riders: suite-harness-current-thread — the wave's one suite-side pre-task, past_stamp_sleep's blocking sleep goes async per review 003's properties.rs:1683 posture note, enabling the current-thread flavor; pg-fix-scheduler-tick-seam — review 003 Finding 1, the cfg(test) transient-fault seam driving the real tick_with_retries through retry-then-success and exhaustion arms (the last uncovered wave-4 hardening arm, sqlite-commit-error-arm pattern). Task hygiene: pg-fix-open-path frontmatter fixed (unquoted 'max_size: 0' colon-space inside the name scalar broke YAML parsing — taskgraph validate had been red one-error since the wave-4 fix-batch decomposition; also restores review-wave-4-fixes' dependency resolution). implementation.md: wave-6 table row → decomposed, task-set listing with the parallelism note (rider + pre-task independent of the mem chain; rider closes before the gate), wave-6 decomposition bullet in the review-rounds record. Docs-only change; verified taskgraph validate (60 tasks, no cycles), cargo fmt --check, cross-reference read
2026-10-10 09:57:01 +00:00

7.1 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
pg-fix-open-path Fix `max_size: 0` open hang + DSN options override (review 002 Finding 3 + options note) completed
single low component implementation
wave-4-fixes
postgres-engine
open

Description

Two open-path fixes in alkstore-postgres/src/store.rs (open_store), from the wave-4 general review:

Finding 3 (MEDIUM, live-proven): PgOpts { max_size: 0 } hangs open forever. Site: store.rs:162-165 + the bootstrap pool.get() at store.rs:177 — deadpool (0.13/0.14) does not validate max_size and defaults to no timeouts, so a max_size: 0 pool can never grant a checkout: open blocks forever at the bootstrap checkout (no BuildError, no default pool timeouts) instead of failing typed. Probe (removed): open with max_size: 0 timed out at 5 s, never returned.

Fix shape (the review's): validate max_size > 0 in open_store — typed Error::Database before any round trip (the engine-wide validate-at-entry posture; the source-chain detail should say what was rejected, e.g. "max_size must be positive"). The alternative shape (configure deadpool timeouts so the hang surfaces as an error) is weaker — a timeout-configured hang still burns the consumer's time; prefer the guard. Do both only if trivial.

Minor note (same file, same surface): store.rs:152-156 — pool_cfg.options(...) replaces any options= present in the consumer's DSN (tokio-postgres's setter semantics) — a silent config override. Preferred fix: append to the DSN's existing options string (tokio-postgres Config::get_options() reads what the parse carried) so the engine's synchronous_commit SET rides alongside whatever the consumer configured. If appending proves impractical, document at the site and in the crate docs that engine-configured options win — but append is the honest default (the consumer's DSN options were there first).

Tests (against the harness server): max_size: 0 fails typed and promptly (assert the error; the test's own structure bounds the hang — a regression re-hangs and fails the test by timeout); a DSN carrying pre-existing options= plus open succeeds and synchronous_commit is still observable via SHOW on both settings (the append path doesn't break the engine's SET); the existing opts-flow tests stay green.

Acceptance Criteria

  • max_size: 0 fails open with a typed Database error before any round trip — pinned by test (prompt failure, no hang)
  • The consumer DSN's options= survive: appended, not replaced — pinned by test (DSN with options + engine SET coexist, SHOW observable)
  • If append proved impractical instead: the engine-wins semantics documented at the site and in crate docs (record the decision in Notes) — n/a: append landed
  • Existing open/opts tests stay green
  • cargo test -p alkstore-postgres (harness server), clippy -D warnings, fmt clean; gates green server-less

References

  • docs/reviews/002-wave-4-general-review.md §Finding 3 + §Minor notes (the options bullet)
  • docs/architecture/engine-postgres.md (Connection architecture)
  • docs/architecture/decisions/008-contract-v1-pinning.md §6 (config split)
  • tasks/pg-engine-open-opts.md (the open task of record; Notes carry the connect-options SET mechanics this task extends)

Notes

Decisions of record made while implementing (the description didn't pin them):

  • Both fix shapes' primary option landed (the guard; no deadpool timeout config added): open_store rejects opts.max_size == 0 at entry — database_error("max_size must be positive, got 0") (the value rides the message; the source chain says what was rejected) — before the config parse and any round trip. The timeout-configured-hang alternative was not taken (the review ranked it weaker; not trivially worth doing both — deadpool 0.13/0.14 has no default pool timeouts to lean on and the guard is total).
  • Append is practical and landed (the task's preferred fix; the n/a acceptance row): tokio-postgres 0.7.18's Config::get_options() -> Option<&str> returns the raw options string the parse (or a prior programmatic options()) carried, so open_store reads it, appends -c synchronous_commit={on|off}, and calls the setter — the consumer's options= ride every pooled connection alongside the engine's SET. Postgres parses repeated -c flags in order, so if the consumer's own options set synchronous_commit, the engine's later -c wins for that knob (it is the knob PgOpts owns) and every other consumer setting lives untouched. The listener path (cfg.clone() + application_name only) is unchanged — the SET is a pool-connection concern. Documented at the site and on PgOpts::max_size (0 rejected immediately) in opts.rs.
  • Tests: open_fails_database_promptly_on_zero_max_size pins the guard — typed Database, source chain containing "max_size" + "positive", bounded by an inner tokio::time::timeout(2s) so a regression re-hangs and fails here by timeout; it runs server-less too (unreachable-DSN fallback — the guard fires before any connection attempt, which is exactly the "before any round trip" property being pinned). consumer_dsn_options_survive_alongside_the_engine_set pins the append: DSN options='-c statement_timeout=30000' → open succeeds, SHOW statement_timeout = 30s (consumer's survived) and SHOW synchronous_commit = on/off per PgOpts (engine's SET rides) — both knob values asserted for both settings, against the harness server.
  • The PgOpts::max_size doc on opts.rs now states the 0 rejection (the constructor's validation is part of the option's contract now that it is typed-failed at open).

Summary

Both open-path fixes landed in alkstore-postgres/src/store.rs (open_store). Finding 3: a max_size > 0 entry guard fires before the config parse and any round trip — max_size: 0 now fails open with the typed Error::Database ("max_size must be positive, got 0") instead of hanging forever at the bootstrap checkout (deadpool 0.13/0.14 neither validates max_size nor defaults timeouts). Minor note: the engine's connect-options SET (-c synchronous_commit=…) is now appended to the parse-carried options= string (read back via tokio_postgres::Config::get_options()) rather than replacing it — the consumer's DSN options survive on every pooled connection alongside the engine's knob; repeated -c flags are order-processed by postgres so the engine still owns synchronous_commit if it collides. Pinned by two new tests in src/store/open_tests.rs (the zero-guard test with a bounded-hang timeout structure, server-less-capable; the append test asserting both SHOW statement_timeout and SHOW synchronous_commit across both knob settings against the harness server). Verified: 118/118 pg lib tests + contract-suite/schema suites green against the harness (pglo-poc :15432), workspace cargo test green server-less (283 tests, pg skipping per convention), cargo clippy --all-targets -- -D warnings and cargo fmt --check clean workspace-wide.