Postgres engine: transactional seam — begin_tx, PgTxHandle, all eleven *_tx methods with drop=rollback detached teardown, probe-pinned unknowable-state discard arms, engine-side resolution arithmetic (task pg-engine-seam-tx)

This commit is contained in:
glm-5.3-flash committed 2026-10-09 06:50:53 +00:00
1 parent c6a7eeaa45
commit cf5ceea70e
7 files changed
+2318 -29

No files matched your search

+144 -11
View File
@@ -1,7 +1,7 @@
---
id: pg-engine-seam-tx
name: Postgres engine — `begin_tx`, `TxHandle` (pooled-object handle), all eleven `*_tx` methods
status: pending
status: completed
depends_on: [pg-engine-open-opts]
scope: moderate
risk: high
@@ -82,28 +82,28 @@ curve, and the scheduler runner.
## Acceptance Criteria
- [ ] `begin_tx` checks out + `BEGIN`; concurrent `begin_tx` draws
- [x] `begin_tx` checks out + `BEGIN`; concurrent `begin_tx` draws
distinct pooled objects (pool-bounded); pool exhaustion blocks
(deadpool's acquire semantics) — tested
- [ ] All eleven `*_tx` methods implemented with entry-point
- [x] All eleven `*_tx` methods implemented with entry-point
validation; writes commit-atomic, reads see the tx's own writes
(read committed, both directions POC-pinned); opts resolution
(delay-over-run_at, relative-expires, 300/3/5/none and 60/5/5
stamp sets) pinned by tests
- [ ] `commit` commits + re-pools; drop (without commit) rolls back +
- [x] `commit` commits + re-pools; drop (without commit) rolls back +
re-pools — no-ghosts tested for job rows, stream events,
notifications (a listener hears nothing from a rolled-back
`notify_tx`), and offset saves
- [ ] Drop is panic-safe; failed COMMIT/ROLLBACK arms discard the
- [x] Drop is panic-safe; failed COMMIT/ROLLBACK arms discard the
object (pool accounting exact); ops-after-consume → `Closed`
- [ ] `notify_tx`/`notify`'s 8000-byte check: typed
- [x] `notify_tx`/`notify`'s 8000-byte check: typed
`PayloadTooLarge { limit: 8000 }` client-side, before any round
trip; ≤ 8000 crosses
- [ ] `with_tx` (core's provided method) works end-to-end on this
- [x] `with_tx` (core's provided method) works end-to-end on this
engine: Ok ⇒ commit, Err ⇒ rollback — tested
- [ ] `encode_payload`'s `Codec` error propagates; extent guard on the
- [x] `encode_payload`'s `Codec` error propagates; extent guard on the
tx stream reads; `get_job_tx` queue-scoped
- [ ] `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
## References
@@ -122,8 +122,141 @@ curve, and the scheduler runner.
## Notes
> To be filled by implementation agent
Decisions of record made while implementing (the description didn't
pin them):
- **The `Drop` = rollback is a detached-task rollback (the async-native
drop posture), not a synchronous await**: `Drop` cannot `.await`,
and the POC's `__private_api_rollback`-style sync submission is a
private tokio-postgres API. The handle captures the construction
runtime's `Handle`; `Drop` takes the held object and spawns the
teardown task — `ROLLBACK`, then re-pool by dropping the object
(re-pool = the `Object` drop arm; discard = `Object::take`,
deadpool's permanent-take, which reduces the pool size exactly).
Probes verified `Handle::spawn` is captured-handle-bound (never
panics on the captured handle's liveness — usable from any thread,
e.g. a sync-context drop) and the runtime-independent futures don't
panic either. The runtime-shutdown-before-task-runs arm drops the
object, whose connection closes server-side and **the server itself
aborts the open transaction** (probe-verified) — the no-ghosts
property holds even there. The `begin_concrete`/`pooled_client`/
`consume_shell` `#[cfg(test)]` accessors are the tests'
fault-injection surface (poisoning a tx needs raw in-tx SQL; the
boxed trait object cannot downcast, ADR-007).
- **The unknowable-state discard arm is probe-pinned, and the
failing-COMMIT arm is induced with a deferred FK**: an *aborted* tx
commits fine server-side by design (`COMMIT` on an aborted tx = a
no-op success — probe-verified both protocols), so the failing-COMMIT
arm is induced with a DEFERRABLE INITIALLY DEFERRED FK violation
(surfaced at commit only); the failed commit discards the object
(`Object::take`) — the wave-3 review's finding (c) pre-empted, the
pool accounting exact (fresh draw proceeds, usable). The failed
`BEGIN` arm in `begin_tx` discards too (same posture); a
`ROLLBACK`ed-then-failed-ROLLBACK residue was probe-verified not to
strand a session.
- **The 8000-byte check's predicate is `len >= limit`, not `len >
limit`** — probe-pinned against the harness server (7999-byte text
delivers; 8000 is server-rejected both protocols; the pg payload
budget is the wire's 8000 bytes *counting the NUL terminator*). The
variant's `limit: 8000` stays the contract's pinned number
(ADR-008 §5); the check's quantity is contract-pinned (the
serde_json serialization — the stored-bytes twin, ADR-020 §4), and
the typed arm mirrors the server exactly so no oversized payload
ever surfaces as an opaque server-side reject. Contract text says
"≤ 8000 bytes"; the server's answer (and now ours) is "< 8000
serialization bytes plus NUL ≤ 8000" — the largest *crossing*
serialization is 7999 bytes (a 7997-char bare string). Flagged here
as a core-text reconciliation note for wave 5's suite (the pg-side
rejection arm's row) — no variant number changes.
- **The clock is a `std::time` read, not a SQL-side clock function**
(the "pick one, document it" point): `resolution.rs`'s `now_unix()`
is the single read; one instant stamps `run_at`/`created_at`/
`expires_at` as plain `i64` binds (second-precision integer
arithmetic, schema.rs's engine-wide timestamp posture). The SQL
layer never reads a clock in the enqueue/publish paths the tx seam
carries.
- **Dead rows are `get_job_tx`-visible** (the auto-commit `get_job`
shape, ADR-010 §1's "dead rows included"): the tx read is
queue-scoped *and* dead-diagnosis-carrying (`last_error`/`died_at`)
— two typed selects (dead table first, then live with NULL-cast
diagnosis columns), one decode owner (`job_from_row`, the
`#[doc(hidden)]` constructor's 18-column uniform shape). The
18-column select's two `NULL::text`/`NULL::bigint` casts matter
(probe: untyped NULL carries no type info for `Option<T>` decode).
- **Extended-protocol param typing is deliberate at three sites**
(probe-pinned: the server infers param types from context):
`GREATEST($3::bigint, 0)` (the bare `$3` beside the literal `0`
infers int4 — an i64 bind errors), the `LIMIT $3` bind rides the
inferred int8 fine, and `pg_notify($1, $2)`'s payload rides as
UTF-8 text (the server infers Text; binding `Vec<u8>` errors
WrongType — the notify payload's contract carriage *is* the
serialization's UTF-8 text, so no cast is needed client-side).
- **`save_offset_tx` clamps a negative save at 0 in the INSERT seed**
(`GREATEST($3::bigint, 0)`): the offsets table's domain CHECK
(`offset >= 0`, schema.rs) would abort the tx on a negative
first-save; clamping preserves the checkpoint's non-negative
domain and the monotone rule (a negative save at-or-below the
stored checkpoint is the same silent no-op a 0-save is). The
trait docs do not pin a negative-offset save's disposition — the
clamp is this engine's honest answer (typed never, no
tx-poisoning from a caller's negative cursor).
- **`enqueue_row` (the shared INSERT) and the trait-stub
`notify/listen/stream/queue/outbox/try_lock/schedule/unschedule/
run_schedules` shapes** ride the open task's stub text unchanged;
only `begin_tx` is wired here (the seam task's scope). The open
task's `store_trait_methods_are_wiring_stubs` test updated:
`begin_tx` now boots + commits (the stub posture's replacement is
this task's landing), the stub-message assertion moved to the
`notify` stub (still `Database`, still "wiring lands with").
- **Test harness details**: the dead-row plant rides typed `execute`
binds (the simple protocol — `batch_execute` — carries no
parameters; probe-pinned E42P02 "there is no parameter $1" is
exactly the simple/extended boundary); the tx-reads test's
poison-induction and the no-ghosts listener test use
schema-qualified quoted DDL (identifier quoting at the test's
composition sites, matching production's `quote_identifier`
posture).
## Summary
> To be filled on completion
Landed alkstore-postgres's transactional seam: `resolution.rs` (the
engine-side arithmetic — the single std-time clock read, the
plain-queue 300/3/5/none and outbox 60/5/5 derived stamp sets, the
`__alkstore_outbox:{name}` reserved-prefix derivation,
`stamps_with_override`'s max-attempts-only override, and
`resolve_enqueue_opts`' delay-over-`run_at` + relative-`expires`
resolution, one clock read per enqueue — every formula unit-tested),
`tx.rs` (the `PgTxHandle` pooled-object handle: `begin` = pool
checkout + `BEGIN` with the exhaustion-blocking, fail-`Database`
arms; the shared `enqueue_row` INSERT both enqueue shapes ride; all
eleven `*_tx` methods implemented with entry-point validation before
any round trip, core's fallible `encode_payload` at the seam with the
typed `Codec` error `?`-ed, the client-side `PayloadTooLarge { limit:
8000 }` check at `len >= 8000` (the server-mirroring, NUL-counting
predicate — probe-pinned), the monotone `save_offset_tx` upsert
clamp-seeded at 0, queue-scoped dead-visible `get_job_tx`, the
extent guards (`limit <= 0` → empty `Vec`) on both tx stream reads,
`commit` = `COMMIT` + re-pool with the failed arm discarding
(`Object::take`), and drop = rollback via the detached-task teardown
(RAII paths roll back through a detached `ROLLBACK`+re-pool;
failed/`is_closed` arms discard; the runtime-gone arm's session
close aborts server-side — no ghosts on any arm) and
ops-after-consume failing closed `Error::Closed`), and the
`PgStore::begin_tx` wiring replacing its stub. Verified against the
harness server: 16 new tx-seam tests + 8 resolution unit tests (39
lib tests green ×2 runs) covering the acceptance rows — pool bounds
+ blocked begin, the stamp/opts resolutions, read-your-own-writes
both directions, keyed publish + monotone offsets + silent-no-op
saves, the uniform no-ghosts list (jobs/events/notifies/offsets — a
listener hears nothing from a rolled-back notify and delivers a
committed one, native commit-atomicity), the unknowable-state arms
(deferred-FK-induced failing COMMIT → discard + exact accounting),
the fail-closed shell (all eleven ops + double-commit `Closed`),
panic-through + mid-tx-cancellation rollbacks, `with_tx` Ok⇒commit /
Err⇒rollback, the typed 8000-byte boundary (crossing 7999 vs typed
rejects at 8000 and 8002), entry-point validation on every name
(shared kinds reserved+empty, consumer-local kinds empty — residue-
free rejects), and the 8-parallel-tx exactly-once fan-out;
workspace `cargo test` green server-less (236→260 tests, pg tests
skipping per convention); `cargo clippy --all-targets -- -D
warnings` and `cargo fmt --check` clean workspace-wide.