Third review round follow-through: enqueue_tx stamp source, sweep_expired handle form, with_tx signature, and B-block ambiguity pins

This commit is contained in:
glm-5.3-flash committed 2026-10-07 14:01:36 +00:00
1 parent 08dc1bf011
commit 83767e880b
11 files changed
+258 -101

No files matched your search

+81 -28
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-10-07 (ADR-021 — third review round: tx reads, claimed_at, schedule queue validation, drop=rollback, receiver arms)
last_updated: 2026-10-07 (ADR-021 + third-round follow-through: enqueue_tx stamp source, sweep_expired handle form, handle-op/ack_batch/opts-type pins, with_tx signature, payload-limit basis, per-stream offsets)
---
# Core contract
@@ -91,8 +91,9 @@ pure reads (no ops ride them). Reads and saves are tx-shaped;
claim/maintenance ops never are (no `claim_tx` — a claim's visibility
deadline must not be tied to a business transaction's lifetime;
deliberate absence, not an omission). **Dropping a handle without
`commit`/`rollback` rolls it back** ([ADR-021](decisions/021-tx-reads-
and-value-shape-fixes.md) §4) — the no-ghosts property holds through
`commit`/`rollback` rolls it back**
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §4) — the
no-ghosts property holds through
RAII paths, not only the explicit ones.
`outbox_enqueue_tx` takes the **outbox name** and derives the backing
@@ -141,7 +142,11 @@ methods — object safety of the boxed handles,
[ADR-008](decisions/008-contract-v1-pinning.md) §2); `payload_as<T>`
decodes on concrete returned values (`Job`, `StreamEvent`). The
`with_tx` closure wrapper ships over the handle shape (the POC-verified
composition direction, [ADR-007](decisions/007-transactional-seam.md)).
composition direction, [ADR-007](decisions/007-transactional-seam.md);
signature pinned there 2026-10-07: `with_tx(f) -> Result<()>` with
`f: FnOnce(&mut dyn TxHandle) -> BoxedFuture<Result<()>>` — `Ok` ⇒
commit, `Err`/drop/panic ⇒ rollback; non-generic for object safety,
values return through captured state, no handle escapes the closure).
Mechanism handles (`Queue`, `StreamHandle`, `Outbox`, `Lock`) are
core-owned trait objects like the tx handle — engine types never appear
in consumer signatures. Their trait surfaces are pinned by
@@ -164,7 +169,10 @@ never sees it.
- `notify(channel, payload)` — payload ≤ 8000 bytes on Postgres
(client-side checked, typed `PayloadTooLarge` before the round
trip, verified by POC #2); no limit on SQLite. The error variant is
trip, verified by POC #2; the measured quantity is the serde_json
serialization of the payload `Value` — ADR-020 §4's stored-bytes
form, so check and storage measure one thing); no limit on SQLite.
The error variant is
contract-wide (callers match it identically on both engines), the
*occurrence* is the documented engine asymmetry
([ADR-008](decisions/008-contract-v1-pinning.md) §5) — with no
@@ -289,19 +297,38 @@ Durable at-least-once work
obligations:
- `enqueue` / `enqueue_tx` — commit-atomic; `EnqueueOpts { delay,
run_at, priority, max_attempts, expires }`; **option semantics
run_at, priority, max_attempts, expires }`; **field types pinned
(2026-10-07, third review round follow-through)**: `delay`,
`run_at`, `expires` are `Option<i64>` (honker-rs parity,
`packages/honker-rs/src/lib.rs:439-447` — unset is distinguishable
from zero, and the resolution rules below are the unset behavior),
`priority` is a plain `i64` with default **0** (the claim-order
key; the row schema's `DEFAULT 0`), and `max_attempts` is
`Option<i64>` with `None` = the queue-level stamp decides (the
§3a override; no honker counterpart — our field). Every field has
a default-resolved meaning, so all are constructor-settable with
`..Default::default()`. Queue-level
`QueueOpts { visibility_timeout_s, max_attempts, backoff_base_s,
dead_letter_retention_s }` ([ADR-010](decisions/010-queue-semantics-depth.md)
§3/§3a): plain `i64` fields (`dead_letter_retention_s:
Option<i64>`, `None` = forever) with a `Default` providing the
pinned defaults 300/3/5/none (honker parities except retention,
which is ours); `ScheduleOpts { priority, max_attempts, expires }`
types identical to their `EnqueueOpts` twins. **Option semantics
([ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md))**:
ready time resolves `delay` → `now + delay` (wins) over `run_at` →
literal absolute over neither → now; `expires` is relative seconds
from enqueue resolved to an absolute row `expires_at` (`None` =
never); the resolved values — not the raw fields — are what
`get_job` shows. Queue-level
`QueueOpts { visibility_timeout_s, max_attempts, backoff_base_s,
dead_letter_retention_s }` ([ADR-010](decisions/010-queue-semantics-depth.md)
§3/§3a) stamp onto each job row at enqueue, resolved over the
handle's opts (auto-commit/tx paths) or the engine's derived
defaults (outbox backing queues, scheduler boundary fires — the
no-handle-open shapes, ADR-014/ADR-020 §3).
`get_job` shows. The `QueueOpts` stamps land on each job row at
enqueue (§3a), resolved per
[ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md) §3 by
call shape: the auto-commit `Queue::enqueue` (the one handle-carrying
shape) resolves over that handle's opts; `enqueue_tx` (a
`TxHandle` carries no queue opts) and every other no-handle-open
path — outbox backing queues, scheduler boundary fires — stamp the
engine's derived defaults (plain 300/3/5/none; the outbox's
60/5/5 set).
- `claim_one` / `claim_batch` — exactly-once handout under concurrency
(POC-pinned on both engines); take a caller-supplied `worker_id`
(claimant identity — consumer-local, no reserved rule, non-empty;
@@ -312,7 +339,13 @@ obligations:
- Job handle: `ack / retry / fail / heartbeat` on the boxed
`JobHandle` a claim returns ([ADR-019](decisions/019-mechanism-handle-surfaces.md)
§3 — `job(&self) -> &Job` beside the ops; one-shot `self: Box<Self>`
ops like the tx commit). `ack` deletes the row;
ops like the tx commit). The one-shot/repeatable split is
deliberate: `ack`/`retry`/`fail` consume the handle (each is a
terminal transition of the claim); **`heartbeat(self, extend)`
takes `&self` and is repeatable** — renewal is inherently repeated
within one claim; do not "fix" this asymmetry at implementation
([ADR-019](decisions/019-mechanism-handle-surfaces.md) §3). `ack`
deletes the row;
`heartbeat(extend)` is *renewal* — an absolute reset of the claim
deadline (extend is the new full deadline from now, **not additive**
to elapsed time); late heartbeat refused; **a reclaim consumes an
@@ -332,13 +365,17 @@ obligations:
(default forever), no redrive API
([ADR-010](decisions/010-queue-semantics-depth.md) §1–§4).
`QueueOpts` stamp onto the job row at enqueue (§3a) — queues are
names, not config owners. The remaining v1-skeleton ops (`ack_batch`,
`cancel` — unconditional delete, not an interrupt) pin in
names, not config owners. The remaining v1-skeleton ops (`ack_batch`
— the batch ack applies the uniform predicate per id, lapsed or
non-claimed ids silently not counted; `cancel` — unconditional
delete, not an interrupt) pin in
[ADR-010](decisions/010-queue-semantics-depth.md) §1.
- `sweep_expired(queue)` — moves *every* past-expiry row (any state)
to dead + enforces dead-letter retention — the no-stranded-rows
- `sweep_expired()` — moves *every* past-expiry row (any state) to
dead + enforces dead-letter retention — the no-stranded-rows
property ([ADR-010](decisions/010-queue-semantics-depth.md) §5);
cadence recipe in [queues.md](queues.md) (no ambient sweeper).
the queue-scoped handle form ([ADR-019](decisions/019-mechanism-handle-surfaces.md)
§1 — the name rides the handle); cadence recipe in
[queues.md](queues.md) (no ambient sweeper).
### named locks
@@ -376,7 +413,11 @@ delivery callable's shape pinned by
job, run
the delivery closure, `ack` on `Ok`, `retry(err, None)` (the queue's
curve) on `Err`; the consumer calls it in its own loop. **No
heartbeat inside delivery** (honker parity, inherited deliberately):
engine-issued heartbeat inside delivery** (honker parity, inherited
deliberately — the helper does not auto-heartbeat on the caller's
behalf; the boxed `JobHandle`'s own `heartbeat` remains available to
the delivery closure, and renewal inside the deadline is the
consumer's obligation):
delivery slower than the backing queue's stamped visibility timeout
(default 60 s for outbox-backed queues) can have its claim expire
mid-delivery and be redelivered — the dual-execution window;
@@ -563,8 +604,12 @@ before the engine specs are called `stable`:
([ADR-010](decisions/010-queue-semantics-depth.md) §2): the
late-heartbeat boundary (refused exactly when the deadline has
lapsed), the post-lapse-ack-refusal (at-least-once), and the
ack-vs-reclaim race (one wins atomically); both engines' SQL pin
identical outcomes in the contract suite.
ack-vs-reclaim race (one wins atomically); `ack_batch` applies the
predicate per id (lapsed or non-claimed ids silently not counted);
`retry`'s refusal is `false` with the row untouched; the
engine-default strings land identically (`fail(None)` →
`"failed"`, exhaustion → `"max attempts exceeded"`); both engines'
SQL pin identical outcomes in the contract suite.
- **`outbox_enqueue_tx` commit-atomicity on both engines**
([ADR-014](decisions/014-outbox-tx-enqueue.md)) — rollback drops
the backing-queue job row together with the business write (no
@@ -573,11 +618,15 @@ before the engine specs are called `stable`:
(`ReservedName`/`InvalidName`); stamped opts visible via `get_job`
equal for both engines.
- **Cross-engine stream equivalence** ([ADR-015](decisions/015-streams-depth.md)
§3/§4) — publish sequence → identical offset sequence → identical
`read_since` output order on both engines, direct and subscriber
reads alike; keyed/unkeyed interleavings preserve global FIFO;
`key` round-trips exactly (`None` vs `Some`), `stream`/`created_at`
fields equal.
§3/§4) — publish sequence → identical *(per-stream)* offset
sequence → identical
`read_since` output order on both engines, direct and subscriber
reads alike (offsets are global counters engine-side — pg
bigserial, SQLite AUTOINCREMENT — so equivalence is per-stream
relative order, never absolute offset values); keyed/unkeyed
interleavings preserve global FIFO;
`key` round-trips exactly (`None` vs `Some`), `stream`/`created_at`
fields equal.
- **`publish_with_key_tx` commit-atomicity on both engines**
([ADR-015](decisions/015-streams-depth.md) §2) — rollback drops the
keyed event row with the business write (no ghost event); commit
@@ -609,7 +658,11 @@ before the engine specs are called `stable`:
- **Enqueue-opts resolution equivalence** ([ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md)
§1/§2/§3) — delay-over-run_at precedence, relative-expires
resolution, and the scheduler-fired jobs' derived-default stamps
(`get_job`-visible via the row) are identical on both engines.
(`get_job`-visible via the row) are identical on both engines;
plain `enqueue_tx` stamps the same plain-queue derived defaults
(300/3/5/none) on both engines — auto-commit-handle enqueues stamp
the handle's opts, no-handle-open shapes stamp derived defaults
(the outbox's 60/5/5 set being the one exception, ADR-014 §1).
- **In-tx read-your-own-writes on both engines**
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §1) —
reads issued on the tx handle see the transaction's own writes