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:
1 parent
08dc1bf011
commit
83767e880b
11 files changed
+258
-101
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user