Files
alkstore/tasks/suite-tx-commit-atomicity-rows.md
T
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

159 lines
8.2 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.
---
id: suite-tx-commit-atomicity-rows
name: Contract-suite rows — outbox/keyed-publish tx commit-atomicity + with_tx panic probe
status: completed
depends_on: []
scope: moderate
risk: low
impact: phase
level: implementation
tags: [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
- [x] Three rows exist, version-stamped (ADR-014 §1, ADR-015 §2,
ADR-021 §4, ADR-007 as applicable)
- [x] Rows wired into both engines' `contract_suite.rs` targets
- [x] SQLite column green server-less; pg column green against the
harness server
- [x] 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
- [x] `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.