Files
alkstore/tasks/pg-fix-forwarder-hardening.md
T

3.9 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
pg-fix-forwarder-hardening Forwarder TOCTOU restructure + hang-shaped arms (review 004 items 4+6) pending
broad high component implementation
review-004-fixes
postgres-engine
forwarder

Description

Forwarder surgery that should ride its own session, per docs/reviews/004-pre-release-general-review.md §"Findings recorded, not fixed" items 4 and 6 (the review bundles them as the forwarder-hardening session). Line refs are as of the review; the inline fixes may have shifted them slightly — re-locate on pickup.

(a) Register/unregister TOCTOU (item 4). The registry refcount and the LISTEN/UNLISTEN command emission are not one critical section (forwarder.rs:131-176 vs 438-482): interleaved register/unregister across tasks can leave a live refcount with the server-side LISTEN cleared (both queued commands share a generation, so no stale check saves it) until the next reconnect re-issue. Fix shape: a decide-and-queue restructure inside the forwarder — the register/unregister decision and the derived command enqueue must be atomic with respect to each other (decide under the registry guard; queue the command with the refcount state it was derived from).

(b) Generation-verify/LISTEN failure parks forever (item 6, arm 1). A failed generation-verify/LISTEN batch is treated as connection death and parks on poll_task.await forever even though the session may be alive (forwarder.rs:698-702/803-808).

(c) Shutdown can leak the listener session (item 6, arm 2). close()/Drop can leak the listener session past shutdown when the serve loop is awaiting a command on a black-holed connection (the run_command await sits outside the shutdown select, :781; no timeout on this connection).

Treatment family for (b)+(c): timeouts/bounded waits on the listener path.

Preserve while restructuring (the gate will check these): the two POC-pinned deadlock pitfalls stay structurally excluded; the reconnect invariants from pg-fix-forwarder-reconnect (the retry core, the fanout release guard, receivers-terminal-iff-loop-gone) stay green; the register-failure unregister fix from review 004 (inline fix 3) stays; the poison-recovery lock idiom from review 004 (inline fix 4) stays.

Use the existing test-seam vocabulary (the reconnect-config slot, the connector injection, the generation tests) for the new pins.

Acceptance Criteria

  • The TOCTOU is closed: register/unregister interleavings across tasks cannot leave a live refcount with the server-side LISTEN cleared — pinned by a test that interleaves the two and asserts the server-side LISTEN state matches the refcount
  • A failed generation-verify/LISTEN batch no longer parks forever: bounded wait with an honest error or reconnect — pinned by test
  • close()/Drop reclaims the listener session even when the serve loop is stuck awaiting on a dead connection — pinned by test (black-holed connection seam)
  • The preserved postures listed above re-verified green (existing forwarder pins: reconnect survival, no-replay, shutdown close, stale-UNLISTEN generation tests, the register-failure unregister pin)
  • cargo test -p alkstore-postgres (harness server), clippy -D warnings, fmt clean; workspace gates green server-less

References

  • docs/reviews/004-pre-release-general-review.md §"Findings recorded, not fixed" items 4 and 6 (the findings, the fix shapes, the own-session scoping)
  • docs/architecture/decisions/004-postgres-driver.md (the forwarder's owned failure modes, the two POC-pinned deadlock pitfalls)
  • tasks/pg-fix-forwarder-reconnect.md (the retry core, the fanout guard, the test-seam vocabulary)
  • tasks/pg-fix-stale-unlisten.md (the generation/staleness machinery the TOCTOU restructure must compose with)

Notes

To be filled by implementation agent

Summary

To be filled on completion