ADR-021: third review round — tx-read methods, Job.claimed_at, schedule() queue validation, drop=rollback, receiver arms
This commit is contained in:
1 parent
8323a7853e
commit
08dc1bf011
15 files changed
+499
-29
No files matched your search
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-06 (ADR-019/020 — handle surfaces, enqueue semantics, payload bridge)
|
||||
last_updated: 2026-10-07 (ADR-021 — third review round: tx reads, claimed_at, schedule queue validation, drop=rollback, receiver arms)
|
||||
---
|
||||
|
||||
# Core contract
|
||||
@@ -72,11 +72,29 @@ trait TxHandle {
|
||||
publish_with_key_tx(stream, key, payload) -> offset
|
||||
notify_tx(channel, payload)
|
||||
save_offset_tx(stream, consumer, offset)
|
||||
get_job_tx(queue, job_id) -> Option<Job> // ADR-021
|
||||
get_offset_tx(stream, consumer) -> i64 // ADR-021
|
||||
read_since_tx(stream, offset, limit) -> Vec<StreamEvent> // ADR-021
|
||||
read_from_consumer_tx(stream, consumer, limit) -> Vec<StreamEvent> // ADR-021
|
||||
outbox_enqueue_tx(outbox, opts, payload) -> job_id
|
||||
commit(self: Box<Self>) -> Result<()> // or rollback
|
||||
}
|
||||
```
|
||||
|
||||
The tx read methods (`get_job_tx` / `get_offset_tx` / `read_since_tx`
|
||||
/ `read_from_consumer_tx`,
|
||||
[ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §1) make
|
||||
read-your-own-writes possible inside the business transaction — they
|
||||
take the mechanism name (mirroring `enqueue_tx`/`publish_tx`'s
|
||||
name-taking shape), read the caller's own transaction's rows, and are
|
||||
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
|
||||
RAII paths, not only the explicit ones.
|
||||
|
||||
`outbox_enqueue_tx` takes the **outbox name** and derives the backing
|
||||
queue engine-side (the reserved prefix makes the derived name
|
||||
unreachable by `enqueue_tx` by design —
|
||||
@@ -223,7 +241,11 @@ business-tx shape). Depth pinned by
|
||||
(`EventReceiver::save_offset(&mut self)`, which saves the
|
||||
last-yielded event's offset through the same op,
|
||||
[ADR-019](decisions/019-mechanism-handle-surfaces.md) §6)
|
||||
interleave freely and cannot regress one another. A saved offset
|
||||
interleave freely and cannot regress one another. Both save
|
||||
forms' `Err` arm carries `Database` only
|
||||
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §5) —
|
||||
regression refusal is a silent no-op, so a save result errs only
|
||||
on storage failure. A saved offset
|
||||
below the trim horizon
|
||||
([ADR-015](decisions/015-streams-depth.md) §5) stays a valid
|
||||
position marker.
|
||||
@@ -241,6 +263,16 @@ business-tx shape). Depth pinned by
|
||||
consumption: attach, read to current tail, resume after restart
|
||||
from the stored offset; explicit `save_offset` on the receiver
|
||||
(shape in [ADR-008](decisions/008-contract-v1-pinning.md) §8).
|
||||
**Error and close arms pinned**
|
||||
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §5):
|
||||
`recv()`'s `Err` arm carries `Database` only (payloads cross raw;
|
||||
`Codec` lives at `payload_as<T>`; `Closed` is the `None` arm);
|
||||
close is terminal and its *cause* is the documented engine
|
||||
asymmetry — SQLite's receiver closes on watcher death (POC-pinned),
|
||||
the pg receiver stays open across forwarder reconnects (reconnect
|
||||
wakes arrive on it) and closes only at engine shutdown. A closed
|
||||
receiver never reopens; recovery is a fresh `subscribe(consumer)`
|
||||
resuming from the saved offset.
|
||||
Consumption is wake-driven with table re-read — the same
|
||||
mechanism split as queues (durable row, LISTEN/watcher wake,
|
||||
[ADR-006](decisions/006-wake-and-delivery-contract.md)); the
|
||||
@@ -375,7 +407,15 @@ Collapsed into queues per [ADR-009](decisions/009-scheduler-collapse.md):
|
||||
runner (`StopToken` is a core-crate cloneable cancel handle —
|
||||
[ADR-019](decisions/019-mechanism-handle-surfaces.md) §4; the
|
||||
leader-elected leadership lock is the reserved name
|
||||
`__alkstore_scheduler`). Schedules never fire
|
||||
`__alkstore_scheduler`). Both name-bearing arguments validate at the
|
||||
entry point: the schedule *name* like a queue name (non-empty,
|
||||
reserved-prefix rejected —
|
||||
[ADR-009](decisions/009-scheduler-collapse.md) §1) and, since
|
||||
[ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §3, the
|
||||
**queue argument** identically (`InvalidName`/`ReservedName`) — a
|
||||
schedule cannot target a reserved name, so the outbox backing queue
|
||||
stays reachable only through the outbox surface
|
||||
([ADR-014](decisions/014-outbox-tx-enqueue.md)). Schedules never fire
|
||||
without a runner (no ambient timers). v1 spec grammar:
|
||||
`@every <n><unit>` only (`s|m|h|d`); cron strings are rejected
|
||||
(`InvalidSpec`) pending a consumer-inventory row naming wall-clock
|
||||
@@ -570,6 +610,30 @@ before the engine specs are called `stable`:
|
||||
§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.
|
||||
- **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
|
||||
(`get_job_tx` a row the same tx enqueued; `read_since_tx` an event
|
||||
the same tx published); another engine connection sees none of it
|
||||
pre-commit; post-rollback the reads' subjects are gone (no-ghosts
|
||||
for reads).
|
||||
- **Drop = rollback on both engines**
|
||||
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §4) —
|
||||
a handle dropped before commit leaves no job/event/notify/offset
|
||||
residue (no-ghosts via RAII); afterward the SQLite writer slot is
|
||||
reusable and the pg client re-poolable.
|
||||
- **`schedule()` queue-argument validation on both engines**
|
||||
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §3) —
|
||||
reserved-prefix and empty queue arguments rejected at registration
|
||||
with `ReservedName`/`InvalidName`; stored schedules fire only into
|
||||
non-reserved queue names.
|
||||
- **Receiver error/close arms on both engines**
|
||||
([ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) §5) —
|
||||
`recv()`'s `Err` carries `Database` only; `save_offset`'s `Err`
|
||||
carries `Database` only; regression saves are silent no-ops;
|
||||
SQLite's receiver closes on watcher death (terminal `None`); the
|
||||
pg receiver stays open across forwarder reconnects (reconnect-wakes
|
||||
arrive; no close) and closes only at engine shutdown.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
@@ -590,6 +654,7 @@ before the engine specs are called `stable`:
|
||||
| [017](decisions/017-contract-versioning.md) | Contract versioning (governs this surface's changes) | core crate's semver *is* the contract version; four change classes; amend-in-place ends at first release; pairing = manifest pin + version-stamped contract suite + docs |
|
||||
| [019](decisions/019-mechanism-handle-surfaces.md) | Mechanism handles (amends 008 §1/§2/§8) | handle traits pinned (`Queue`/`StreamHandle`/`Outbox`/`Lock`/`JobHandle`); `worker_id` claimant identity; `Job`/`Schedule` struct shapes; core-owned `StopToken`; save-offset composition |
|
||||
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue options + bridges (amends 008 §1/§2, 009 §3) | `delay` wins over `run_at`; `expires` = relative seconds; scheduler stamps from derived queue defaults; serde_json byte-level payload encoding |
|
||||
| [021](decisions/021-tx-reads-and-value-shape-fixes.md) | Third review round (amends 008 §2/§8, 019 §1/§3, 009 §1) | tx-side read methods on `TxHandle` (read-your-own-writes); `Job.claimed_at` restored; `schedule()` queue argument validated; drop = rollback; receiver close/error arms pinned |
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -619,3 +684,8 @@ its findings resolved directly as
|
||||
surfaces, value shapes, stop token) and
|
||||
[ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md)
|
||||
(enqueue-option semantics, scheduler stamp source, payload encoding).
|
||||
The third review round (2026-10-07, pre-decomposition) likewise found
|
||||
no new open questions — its five findings resolved directly as
|
||||
[ADR-021](decisions/021-tx-reads-and-value-shape-fixes.md) (tx-side
|
||||
reads, `Job.claimed_at`, `schedule()` queue-validation, drop =
|
||||
rollback, receiver close/error arms).
|
||||
Reference in new issue
Block a user