Files
alkstore/tasks/suite-tx-commit-atomicity-rows.md
glm-5.3-flash 360e71e51e Contract-suite tx commit-atomicity rows (task suite-tx-commit-atomicity-rows): the N-5 panic-probe admission in properties.rs's module doc (spawned with_tx task joined via JoinError::is_panic — catch_unwind around an async closure cannot see the panic point across an await; post-panic assertions read-only until convergence, single-task drive, no race window; ADR-017 §2 class 4) and three version-stamped rows: outbox_enqueue_tx_commit_atomicity (rollback drops the backing-queue job with the business write — get_job_tx-gone + run_once claims nothing; commit makes the job claimable exactly when the business write commits, real delivery through the RecordingDelivery closure with job-id identity, derived __alkstore_outbox:mail queue, exact payload, 60/5/5 stamps, consumed-after-ack; ADR-014 §1, ADR-010 §3a, ADR-021 §4, ADR-007), publish_with_key_tx_commit_atomicity (rollback drops the keyed event with the business write, key round-tripping inside the tx; commit surfaces it to read_since and a post-commit subscriber attach with the key round-tripping; ADR-015 §2/§4, ADR-021 §4, ADR-007), and with_tx_panicking_closure_rolls_back (N-5's probe against real engines: the panicking closure writes all four kinds then panics mid-flight; panic surfaces via the join; no-ghost reads converge with begin_tx granting every iteration; pre-panic listener silence on the notified channel; ghost never claimable; fresh with_tx commits through the same seam — both engines green; ADR-007, ADR-021 §4) — wired into both engines' suite targets (SQLite tokio tests, pg harness_row!s). Dispositions recorded in Notes: the 60/5/5 stamp inspection rides the delivery handle's job() because get_job cannot target the reserved derived backing queue through the contract surface (exemplar row pins the rejection; engine-side raw-row probes stayed put), the panic row's notify leg is a windowed silence (SQLite's wake overtriggers on any commit — the fully cross-engine notify-ghost pin rides the engines' rollback-ghosts twins, the suite's drop-rollback row made the same call), the closure tail routes through a #[cold] mid_flight_panic -> Error helper (a bare Err(panic!()) tail trips unreachable_code under -D warnings), and the post-panic convergence loop is the suite-side bounded wait (state outcomes only, begin_tx doubling as the seam-still-grants probe). Verified: sqlite suite 19/19, pg suite 19/19 vs harness twice (postgres/poc@:15432) + solo re-runs of each new row per engine (determinism), workspace build/test green, clippy -D warnings, fmt clean
2026-10-10 06:17:18 +00:00

8.2 KiB
Raw Permalink Blame History

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
suite-tx-commit-atomicity-rows Contract-suite rows — outbox/keyed-publish tx commit-atomicity + with_tx panic probe completed
moderate low phase implementation
wave-5
contract-suite
tx-seam

Description

Add the tx-seam commit-atomicity rows to alkstore-contract-suite/src/properties.rs and wire them into both engines' suite targets, discharging three backlog rows (core-contract.md §Verification backlog): "outbox_enqueue_tx commit-atomicity on both engines" (ADR-014), "publish_with_key_tx commit-atomicity on both engines" (ADR-015 §2), and the N-5 with_tx panic-disposition probe (the waves-1–2 general review's ordered wave-5 add — the panic path "can only be fully pinned once a real engine's handle exists").

Rows to add:

  • outbox_enqueue_tx_commit_atomicity — rollback drops the backing-queue job row together with the business write (no ghost job; run_once afterwards claims nothing); commit makes the job claimable by run_once exactly when the business write commits (drive a real delivery through the Delivery closure); the stamped opts are the outbox's derived 60/5/5 set, get_job-visible and identical on both engines (ADR-014 §1, ADR-010 §3a). The reserved/empty outbox-name validation legs already pin in the exemplar row — do not duplicate them.
  • publish_with_key_tx_commit_atomicity — rollback drops the keyed event row with the business write (no ghost event); commit makes it visible to read_since (and a subscriber attach); the key round-trips on the committed event. The empty-Some-key InvalidName leg already pins in the exemplar — do not duplicate.
  • with_tx_panicking_closure_rolls_back — a closure that panics mid-flight ⇒ rollback with no residue (the uniform no-ghosts list: job/event/notify/offset — ADR-021 §4's drop = rollback through the panic path), and the store remains usable afterward (the SQLite writer slot replenished — the wave-3 stranding fix's property, now pinned at contract level). Mechanics hint: the panic crosses an await inside with_tx's own future, so drive it via tokio::spawn + JoinError::is_panic() (a catch_unwind around an async closure does not see the panic point); assert the panic surfaced and the post-panic state. This is N-5's probe against real engines for the first time — the SQLite handle-on-lease Drop impl and the pg pooled-object discard arm must both survive it.

Acceptance Criteria

  • Three rows exist, version-stamped (ADR-014 §1, ADR-015 §2, ADR-021 §4, ADR-007 as applicable)
  • Rows wired into both engines' contract_suite.rs targets
  • SQLite column green server-less; pg column green against the harness server
  • The panic probe passes on both engines (no writer-slot stranding, no pooled-object leak) — if an engine fails here, that is a real defect: stop, record, fix engine-side before completing the task
  • cargo test -p alkstore-sqlite -p alkstore-postgres green (pg rows skip cleanly server-less); clippy -D warnings; fmt clean

References

  • docs/architecture/core-contract.md §Verification backlog (outbox tx commit-atomicity; keyed-publish tx commit-atomicity)
  • docs/architecture/decisions/014-outbox-tx-enqueue.md §1
  • docs/architecture/decisions/015-streams-depth.md §2
  • docs/architecture/decisions/021-tx-reads-and-value-shape-fixes.md §4
  • docs/reviews/001-waves-1-2-general-review.md (N-5)
  • tasks/sqlite-engine-seam-tx.md (the Drop impl N-5's probe exercises)

Notes

Decisions of record beyond the task description:

  • The 60/5/5 stamp inspection rides the delivery handle's job(). ADR-014 §1's backlog wording says the stamped opts are "get_job-visible", but the derived backing queue is engine-internal: it is reserved-prefixed, so no get_job/get_job_tx call can target it through the contract surface (the exemplar row itself pins get_job_tx(reserved queue) → ReservedName; the engine-side stamp tests use raw row probes, which the suite may not). The row therefore asserts the 60/5/5 set on the Delivery closure's JobHandle (job() — visibility_timeout_s/max_attempts/backoff_base_s/ dead_letter_retention_s) — the only contract-visible inspection of backing-queue rows. The delivery payload-exactness and the __alkstore_outbox:{name} derived-name pin ride the same snapshot.
  • The panic row's notify leg is a windowed silence, not a full cross-engine pin. The row drives all four write kinds (job/event/notify/offset) through the panicking tx, but a rolled-back notify's no-residue is only observable as listener silence, and SQLite's wake overtriggers on any commit (the notify module's documented coalescing posture), so silence is asserted in a window that precedes every post-panic committed write — a delivery-shaped wake there is a genuine defect. The fully cross-engine notify-ghost pin rides the engines' own rollback-ghosts tests (SQLite's raw notify-count probe, pg's listener-silence twin); the suite's existing drop_rollback_leaves_no_ghosts row makes the same shape of call (drives the observable three kinds).
  • The panic-probe closure's tail routes through a #[cold] helper typed Error (mid_flight_panic) — a bare Err(panic!(...)) tail is unreachable-call-shaped and trips the unreachable_code lint under -D warnings; catch_unwind around an async closure cannot see the panic point anyway (the mechanics hint), so the typed diverger costs nothing.
  • The post-panic convergence loop is the suite-side bounded wait for the rollback to land (pg's detached rollback task; SQLite's rolls back synchronously in the unwind): state-outcome assertions only, each iteration's begin_tx doubling as the seam-still-grants probe (the writer-slot/pool replenish property), the deadline a safety bound like await_fires' — no timing-value assertion.

Summary

Landed the three tx-seam commit-atomicity rows in alkstore-contract-suite/src/properties.rs, each version-stamped per the suite convention:

  • outbox_enqueue_tx_commit_atomicity — rollback drops the backing-queue job with the business write (business id get_job_tx- gone in a fresh tx; run_once afterwards claims nothing); commit makes the job claimable by run_once exactly when the business write commits (real delivery through the RecordingDelivery closure: job id identity, derived __alkstore_outbox:mail queue, exact payload, 60/5/5 stamps, consumed-after-ack) (ADR-014 §1, ADR-010 §3a, ADR-021 §4, ADR-007).
  • publish_with_key_tx_commit_atomicity — rollback drops the keyed event with the business write (both streams empty on fresh reads, key round-tripping inside the tx pre-drop); commit makes it visible to read_since and to a post-commit subscriber attach with the key round-tripping on both (ADR-015 §2/§4, ADR-021 §4, ADR-007).
  • with_tx_panicking_closure_rolls_back — N-5's probe against real engines for the first time: spawned-task with_tx closure writes enqueue/publish/notify/offset then panics mid-flight; the panic surfaces via JoinError::is_panic(); the no-ghost reads converge with begin_tx granting every iteration; the pre-panic listener hears nothing on the notified channel; the ghost never becomes claimable work; a fresh with_tx commits through the same seam — both engines green (no writer-slot stranding, no pooled-object leak) (ADR-007, ADR-021 §4).

Also: the module-doc determinism/orchestration note gained the panic probe's admission sentence (spawned-task mechanics, catch_unwind asymmetry — the runner-driving extension's sibling; ADR-017 §2 class 4), RecordingDelivery + the #[cold] diverger are suite-local helpers, and the rows are exported from the suite lib and wired into alkstore-sqlite/tests/contract_suite.rs (three direct tokio tests) and alkstore-postgres/tests/contract_suite.rs (three harness_row!s).

Verified: sqlite suite 19/19; pg suite 19/19 vs the harness server (pglo-poc :15432, postgres/poc/blobs) twice consecutively, plus solo re-runs of each new row per engine (determinism); workspace build/test green; clippy -D warnings clean; fmt clean.