From 83767e880b90904cc96639d2099ef76cf091c375 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Wed, 7 Oct 2026 14:01:36 +0000 Subject: [PATCH] Third review round follow-through: enqueue_tx stamp source, sweep_expired handle form, with_tx signature, and B-block ambiguity pins --- docs/architecture/README.md | 4 +- docs/architecture/core-contract.md | 109 +++++++++++++----- .../decisions/007-transactional-seam.md | 18 +++ .../decisions/008-contract-v1-pinning.md | 30 ++++- .../decisions/010-queue-semantics-depth.md | 12 +- .../decisions/015-streams-depth.md | 13 ++- .../019-mechanism-handle-surfaces.md | 28 ++++- .../020-enqueue-opt-semantics-and-bridges.md | 11 ++ .../021-tx-reads-and-value-shape-fixes.md | 16 +++ docs/architecture/open-questions.md | 105 +++++++++-------- docs/architecture/queues.md | 13 ++- 11 files changed, 258 insertions(+), 101 deletions(-) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index fb2f0ee..1773964 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-07 (ADR-021 — third review round resolved) +last_updated: 2026-10-07 (ADR-021 + third-round follow-through resolved) --- # alkstore — Architecture @@ -27,7 +27,7 @@ pending architecture review and OQ resolution. | [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-10 (resolved) | | [engine-sqlite.md](engine-sqlite.md) | draft | SQLite engine: forked-substrate/rusqlite mapping | OQ-06 (resolved), OQ-12 (resolved), OQ-13 (resolved) | | [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-08 (resolved), OQ-12 (resolved), OQ-13 (resolved) | -| [queues.md](queues.md) | draft | Queue/scheduler/outbox semantics depth (resolved: ADR-009/ADR-010) | OQ-06 (resolved) | +| [queues.md](queues.md) | draft | Queue/scheduler/outbox semantics depth (resolved: ADR-009/ADR-010) | OQ-05 (resolved), OQ-09 (resolved), OQ-06 (resolved) | | [deployment.md](deployment.md) | draft | Host semantics, connection budgets, knobs, matrix | OQ-08 (resolved) | | [open-questions.md](open-questions.md) | draft | OQ tracker (promoted from OQ-ST register) | — | diff --git a/docs/architecture/core-contract.md b/docs/architecture/core-contract.md index 971c787..371464a 100644 --- a/docs/architecture/core-contract.md +++ b/docs/architecture/core-contract.md @@ -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` 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>` — `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` (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` 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`, `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` - 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 diff --git a/docs/architecture/decisions/007-transactional-seam.md b/docs/architecture/decisions/007-transactional-seam.md index 2627221..4eef389 100644 --- a/docs/architecture/decisions/007-transactional-seam.md +++ b/docs/architecture/decisions/007-transactional-seam.md @@ -56,6 +56,24 @@ handle.commit() / handle.rollback() wrapper over* the handle shape, not instead of it (the POC verified the reverse composition doesn't work: a closure cannot outlive itself, so caching-subscriber state can't escape it). + *(Signature pinned 2026-10-07, third review round follow-through — + the last "shape at implementation" deferral this ADR carried:* + ```text + store.with_tx(f) -> Result<()> // on the Store trait + f: FnOnce(&mut dyn TxHandle) -> BoxedFuture> + ``` + *The trait method is deliberately **non-generic** (a `Store` trait + object must stay object-safe, ADR-008 §2's posture) and returns + `Result<()>`: the closure signals commit/rollback by its own result + (`Ok` ⇒ commit, `Err` ⇒ rollback; drop/panic across the closure ⇒ + rollback, ADR-021 §4's disposition) and returns values through the + caller's captured state (the handle never escapes the closure — + the same box-in/box-out composition the POC's closure-scoped arm + (b) verified works, with (a)'s preference unchanged: `with_tx` is + the convenience, `begin_tx` the seam). The closure's future boxes + for object safety. An engine may additionally offer a generic + value-returning `with_tx` convenience on its *concrete* store type; + that ergonomics layer is engine surface, not contract.)* - Both engines deliver the property **natively** — no emulation: in-tx notify/Notify delivers only at commit; rollback drops job rows, business rows, and notifications together (the POC property tests on diff --git a/docs/architecture/decisions/008-contract-v1-pinning.md b/docs/architecture/decisions/008-contract-v1-pinning.md index 88ab136..1881373 100644 --- a/docs/architecture/decisions/008-contract-v1-pinning.md +++ b/docs/architecture/decisions/008-contract-v1-pinning.md @@ -212,7 +212,11 @@ struct Wake { channel: String } the receiver's forms: `recv() -> None` means closed (source death); `try_recv`/`recv_timeout` return `Err(Closed)` when the source is closed, versus `Ok(None)` when merely no wake is pending right now - — closed and idle are distinguishable states. + — closed and idle are distinguishable states. *(Timeout arm pinned + 2026-10-07, third review round follow-through: `recv_timeout(d)`'s + expiry without a wake returns `Ok(None)` — idle is the documented + `Ok(None)` arm; `Err` on that method carries `Database` (storage + failure) or `Closed` (source death), never a timeout signal.)* ### 4. Reserved namespace — exact strings @@ -238,7 +242,14 @@ struct Wake { channel: String } consumer arguments, `subscribe(consumer)`) are *not* a reserved- namespace kind — they are consumer-local identifiers, not shared engine namespaces; no prefix rule applies (an engine may still quote - them). + them). *(Annotated 2026-10-07, third review round follow-through: + the consumer-local identifier classes take the **non-empty rule + only** — an empty stream-consumer name or empty `try_lock` owner is + `InvalidName`, mirroring `worker_id`'s rule + ([ADR-019](019-mechanism-handle-surfaces.md) §2) — with no + reserved-prefix rejection (they are not a shared namespace, so a + leading `__alkstore_` is the caller's own local name, no engine + namespace to collide with).)* - Per-engine internal names: - SQLite: honker's machinery owns two categories of internal names, and the contract treats them differently. Its `_honker_*` *table* @@ -283,7 +294,11 @@ v1 variants (guaranteed-matchable on every engine): - `PayloadTooLarge { limit }` — **universal variant**, produced by the Postgres engine's client-side check (8000 bytes, POC #2 verified-before-round-trip) and by contract never on SQLite (no - limit — the documented engine asymmetry). The variant lives in the + limit — the documented engine asymmetry). The measured quantity + *(pinned 2026-10-07, third review round follow-through)* is the + **serde_json serialization of the payload `Value`** — the same + byte string ADR-020 §4 pins as the stored row bytes — the check and + the storage measure one quantity. The variant lives in the shared taxonomy so the limit asymmetry is visible to engine-agnostic code — a caller can match it without knowing which engine is behind the store, and the error itself documents the boundary. The @@ -397,7 +412,14 @@ trait EventReceiver { No `save_every`, no save-on-drop — checkpointing is the consumer's explicit call (the §1 obligation). The consumer's *name* is fixed at `subscribe(consumer)`; it is a consumer-local identifier (no reserved -prefix, §4). +prefix, §4). *(Annotated 2026-10-07, third review round follow-through: +`EventReceiver` deliberately carries **no `recv_timeout`** — unlike +`WakeReceiver` (§3) — because a stream consumer's steady state is +wake-driven re-read (the queues split applies: the durable row is the +truth, the wake is the hint) and its polling-shaped needs are already +served by `try_recv` and the offset-anchored `read_since`; a +bounded-wait convenience has no row naming it. Stated so the absence +is read as design, not oversight.)* ## Consequences diff --git a/docs/architecture/decisions/010-queue-semantics-depth.md b/docs/architecture/decisions/010-queue-semantics-depth.md index ca19090..41eed1c 100644 --- a/docs/architecture/decisions/010-queue-semantics-depth.md +++ b/docs/architecture/decisions/010-queue-semantics-depth.md @@ -66,6 +66,12 @@ Contract job states: **`pending` → `processing` → `dead`** (+ absence: it. Not an interrupt — the holder's next ack/heartbeat returns false, the same shape as expiry. `ack_batch` is the batch form of ack (count returned; non-claimed ids silently not counted). + *(Acknowledged 2026-10-07, third review round follow-through: the + batch form applies §2's full validity predicate — processing state + **and** unexpired claim deadline — per id, not claim-state alone; + an id whose deadline lapsed is silently not counted, identically to + an acked-elsewhere or cancelled id. Per-id outcomes are + independent; partial success is ordinary.)* - Claim ordering (same queue): `priority DESC`, then ready-time (`run_at`) ascending, then enqueue order — FIFO under equal priority, both references agree; pinned. @@ -224,8 +230,10 @@ budget, exhaustion-by-reclaim, and `expires` lapses (§5). ### 5. `sweep_expired`: the no-stranded-rows property -Contract semantics for `sweep_expired(queue)` (the v1 skeleton's -maintenance entry point): +Contract semantics for `sweep_expired` (the v1 skeleton's +maintenance entry point; the queue-scoped handle form — name on the +handle — is pinned by [ADR-019](019-mechanism-handle-surfaces.md) §1, +2026-10-06): - It moves to dead (`last_error = 'expired'`) **every row of the queue past its `expires_at` in any state** — pending rows (honker diff --git a/docs/architecture/decisions/015-streams-depth.md b/docs/architecture/decisions/015-streams-depth.md index 45d6f75..f1b4da9 100644 --- a/docs/architecture/decisions/015-streams-depth.md +++ b/docs/architecture/decisions/015-streams-depth.md @@ -188,9 +188,16 @@ as already pinned): > offsets immutable and never renumbered.* This is the row the contract suite pins cross-engine stream -equivalence against: two engines, same publish sequence → same offset -sequence → same `read_since` output order, for direct and subscriber -reads alike. It is deliberately *not* stronger than both engines can +equivalence against: two engines, same publish sequence → same +*(per-stream)* offset sequence → same `read_since` output order, for +direct and subscriber reads alike. *(Per-stream stated 2026-10-07, +third review round follow-through: both engines' offsets are global +counter values — pg bigserial, SQLite AUTOINCREMENT — so a fresh +stream's first event does not carry offset 1; equivalence means the +same publish sequence into one stream produces the same *relative* +offset sequence per that stream, a property safe under either +counter scheme; cross-stream offset values are engine-internal and +never compared.)* It is deliberately *not* stronger than both engines can implement by a shared SQL shape (`ORDER BY offset ASC` is already both engines' claim path) and not weaker than the mechanism's purpose (replay demands monotone positions). diff --git a/docs/architecture/decisions/019-mechanism-handle-surfaces.md b/docs/architecture/decisions/019-mechanism-handle-surfaces.md index 82a445f..dae62c5 100644 --- a/docs/architecture/decisions/019-mechanism-handle-surfaces.md +++ b/docs/architecture/decisions/019-mechanism-handle-surfaces.md @@ -189,12 +189,23 @@ trait JobHandle { job(&self) -> &Job // the row, as a value ack(self: Box) -> bool retry(self: Box, err: Option, delay: Option) - // None delay = queue curve + -> bool // None delay = queue curve fail(self: Box, err: Option) -> bool heartbeat(self, extend: i64) -> bool // absolute reset } ``` +*(Annotated 2026-10-07, third review round follow-through: the +original sketch elided `retry`'s return type — pinned `-> bool`, +the same validity predicate as its siblings. A refused retry — +deadline lapsed, row reclaimed/cancelled/acked elsewhere — leaves +the handle's op **not applied**: the handle was consumed by +`retry(self: Box, …)` regardless (caller-side), but the row +keeps its pre-call state (still `processing`, or gone if acked/cancelled) +with its original stamps; no dead-letter, no schedule-side delay is +recorded. `false` = the caller's window closed, at-least-once +redelivery governs.)* + — and **`get_job` returns `Option`** (pure data). The *same uniform validity predicate* ([ADR-010](010-queue-semantics-depth.md) §2 — `processing` + unexpired caller deadline) governs a @@ -205,9 +216,18 @@ trait JobHandle { because renewal is inherently repeated within one claim (the same one-shot/repeatable split the receiver shapes carry; do not "fix" the asymmetry at implementation). - `err: Option`: `None` = the engine's - documented-default string (taxonomy-consistent: these strings are - row content, `get_job` data — not matched error variants). + `err: Option`: `None` = the engine's + documented-default string (taxonomy-consistent: these strings are + row content, `get_job` data — not matched error variants). + *(Annotated 2026-10-07, third review round follow-through: the + default string is pinned — **`fail(None)` records `"failed"`**; + `retry(None, …)` that exhausts the budget records + `"max attempts exceeded"`, per + [ADR-010](010-queue-semantics-depth.md) §4's path-based rule — + exhaustion is engine-observed state, so the path's pinned string + wins over the caller-None case; a successful `retry(None, …)` + records nothing (the row returns to pending, `last_error` exists + only on dead rows).)* *(Struct corrected 2026-10-07 by [ADR-021](021-tx-reads-and-value-shape-fixes.md) §2: `claimed_at` was missing from the original pinning — diff --git a/docs/architecture/decisions/020-enqueue-opt-semantics-and-bridges.md b/docs/architecture/decisions/020-enqueue-opt-semantics-and-bridges.md index e1abc65..eb0a36d 100644 --- a/docs/architecture/decisions/020-enqueue-opt-semantics-and-bridges.md +++ b/docs/architecture/decisions/020-enqueue-opt-semantics-and-bridges.md @@ -105,6 +105,17 @@ parities: 300 s / 3 / 5 s / none), not a handle's opts and not - **`ScheduleOpts` field growth post-release is class 3** (opts struct, [ADR-017](017-contract-versioning.md) §3's exemption) — stated so the pricing is visible now. +- *(Annotated 2026-10-07, third review round follow-through: §3a's + "resolved over the queue handle's `QueueOpts`" reading applies only + to the auto-commit `Queue::enqueue` — the one call shape with a + queue handle in scope. Plain + `enqueue_tx(queue_name, opts, payload)` is a no-handle-open shape + too (a `TxHandle` never carries queue opts), so it stamps the + engine's plain-queue defaults (300 s / 3 / 5 s / none) exactly as + boundary fires do; the outbox's `outbox_enqueue_tx` keeps its + 60 s/5/5 derived set ([ADR-014](014-outbox-tx-enqueue.md) §1). + Without this note the SQLite and pg implementers could legitimately + diverge on the tx path's stamp source.)* ### 4. The payload encoding: serde_json serialization of the trait `Value`, byte-exactly diff --git a/docs/architecture/decisions/021-tx-reads-and-value-shape-fixes.md b/docs/architecture/decisions/021-tx-reads-and-value-shape-fixes.md index 20ab91d..a454b3d 100644 --- a/docs/architecture/decisions/021-tx-reads-and-value-shape-fixes.md +++ b/docs/architecture/decisions/021-tx-reads-and-value-shape-fixes.md @@ -12,6 +12,22 @@ corrections to accepted decisions found by the review — ADR-019 §1's tx-methods claim and ADR-019 §3's `Job` field list — corrected inline there, per this repo's convention that wrong statements in decisions are fixed where they stand, not left for readers to discover.) +*(Follow-through 2026-10-07, same round, second session: the review's +remaining ambiguities — all decidable from decided material — were +closed by single-sentence/annotation edits against the already-decided +ADRs and specs, recorded in the annotations' own text rather than a +new ADR: A4 (`enqueue_tx` stamps the derived engine defaults — +annotated onto ADR-020 §3), A5 (stale `sweep_expired(queue)` shapes +aligned to ADR-019 §1's handle form in core-contract.md and queues.md), +`retry -> bool` + the refused-retry residue and the `fail(None)` +default string (ADR-019 §3), the one-shot/repeatable split restated +(core-contract), `ack_batch`'s per-id predicate (ADR-010 §1), opts +field types/defaults (core-contract, honker-rs parity), the +consumer-local non-empty rules (ADR-008 §4), the `with_tx` signature +(ADR-007), the `PayloadTooLarge` measurement basis (ADR-008 §5), +per-stream offset-sequence equivalence (ADR-015 §4), the +`recv_timeout` timeout arm and `EventReceiver`'s no-`recv_timeout` +posture (ADR-008 §3/§8).)* ## Context diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 74abcd3..f96cd9e 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -377,57 +377,6 @@ narrowed to the pinning work its own record already scoped.)* that removed the substrate's versioning surface), OQ-11 (the cherry-pick procedure governing the substrate-side non-events). -## Theme: Deployment and capabilities - -### OQ-08: Where does the honest single-host/multi-host boundary live in the trait surface? *(== OQ-ST-08)* — **RESOLVED** - -- **Origin**: [deployment.md](deployment.md) -- **Status**: resolved (2026-10-06, Phase 1 — - [ADR-016](decisions/016-deployment-honesty.md)) -- **Priority**: medium -- **Resolution**: Pinned by - [ADR-016](decisions/016-deployment-honesty.md): **no runtime - capability surface — in v1 and by default ever**. The honest - single-host/multi-host boundary lives in the two places it is - already true, which compose rather than rival: (1) - **compile-time engine identity** — the engine crate a binary - depends on *is* the deployment statement (single-driver binaries, - [ADR-001](decisions/001-crate-split.md); constructors in engine - crates, [ADR-008](decisions/008-contract-v1-pinning.md) §6 — - "the engine choice is a dependency-graph fact, not a runtime - branch"); (2) **deployment.md's documented matrix** — the - ops-facing facts of record, unchanged. Option 2 - (`Store::capabilities()`) rejected field-by-field under ADR-008 - §5's act-differently rule generalized to surface: host semantics - admit no in-process action (a flag would invite the engine-type - branch principle 4 bans); payload limits already have their runtime - carriage — the universal, contract-wide matchable - `PayloadTooLarge` variant (a `capabilities()` field would be a - second normative home); wake cadence and knobs are engine-crate - config (§6's split); and no consumer-inventory row names any - runtime-adapt need. No `engine_name()`, no `#[cfg]` capability - axes. The "must not pretend SQLite is multi-host" obligation - resolves into three standing statements (contract text carries - asymmetries via the taxonomy, engine-crate docs carry posture, - deployment.md carries ops facts); the misconfiguration case - (SQLite as shared network storage) follows the family's - deployment-asserts-truth posture (alkblobs precedent) — documented - boundary, no fabricated detection. Contract-suite row added: - `PayloadTooLarge` occurrence asymmetry (pg client-side pre-round- - trip, SQLite never) — with no capabilities API, the variant is the - one runtime carriage of an engine asymmetry, so its matchability - is pinned. Re-entry gate: a consumer-inventory row naming a - runtime-adapt need. -- **Cross-references**: OQ-04 (the pinning that parked this), - OQ-10 (narrowed by this — no capability struct to govern), OQ-11, - [ADR-001](decisions/001-crate-split.md), - [ADR-006](decisions/006-wake-and-delivery-contract.md), - [ADR-008](decisions/008-contract-v1-pinning.md) §1/§5/§6, - [ADR-012](decisions/012-forked-substrate-design.md), - [deployment.md](deployment.md). - - - ## Theme: Core contract (Phase 1 review finds) ### OQ-12: Streams depth — key semantics, `StreamEvent` shape, ordering row, retention — **RESOLVED** @@ -512,8 +461,9 @@ narrowed to the pinning work its own record already scoped.)* like `store.outbox(name)` — `InvalidName`/`ReservedName`), derives the backing queue name `__alkstore_outbox:{outbox}` engine-side, and lands the job row in the caller's transaction with - `EnqueueOpts` stamped per [ADR-010](decisions/010-queue-semantics- - depth.md) §3a over the backing queue's derived `QueueOpts` — + `EnqueueOpts` stamped per + [ADR-010](decisions/010-queue-semantics-depth.md) §3a over the + backing queue's derived `QueueOpts` — commit-atomic with the business write, rollback drops both (the no-ghosts property). The reserved-prefix rejection is unchanged: it governs directly-supplied names; the derivation is legitimate @@ -533,6 +483,55 @@ narrowed to the pinning work its own record already scoped.)* (outbox scope row), ADR-007/ADR-008 (the seam design), ADR-010 §3 (the outbox's derived QueueOpts — unchanged by this). +## Theme: Deployment and capabilities + +### OQ-08: Where does the honest single-host/multi-host boundary live in the trait surface? *(== OQ-ST-08)* — **RESOLVED** + +- **Origin**: [deployment.md](deployment.md) +- **Status**: resolved (2026-10-06, Phase 1 — + [ADR-016](decisions/016-deployment-honesty.md)) +- **Priority**: medium +- **Resolution**: Pinned by + [ADR-016](decisions/016-deployment-honesty.md): **no runtime + capability surface — in v1 and by default ever**. The honest + single-host/multi-host boundary lives in the two places it is + already true, which compose rather than rival: (1) + **compile-time engine identity** — the engine crate a binary + depends on *is* the deployment statement (single-driver binaries, + [ADR-001](decisions/001-crate-split.md); constructors in engine + crates, [ADR-008](decisions/008-contract-v1-pinning.md) §6 — + "the engine choice is a dependency-graph fact, not a runtime + branch"); (2) **deployment.md's documented matrix** — the + ops-facing facts of record, unchanged. Option 2 + (`Store::capabilities()`) rejected field-by-field under ADR-008 + §5's act-differently rule generalized to surface: host semantics + admit no in-process action (a flag would invite the engine-type + branch principle 4 bans); payload limits already have their runtime + carriage — the universal, contract-wide matchable + `PayloadTooLarge` variant (a `capabilities()` field would be a + second normative home); wake cadence and knobs are engine-crate + config (§6's split); and no consumer-inventory row names any + runtime-adapt need. No `engine_name()`, no `#[cfg]` capability + axes. The "must not pretend SQLite is multi-host" obligation + resolves into three standing statements (contract text carries + asymmetries via the taxonomy, engine-crate docs carry posture, + deployment.md carries ops facts); the misconfiguration case + (SQLite as shared network storage) follows the family's + deployment-asserts-truth posture (alkblobs precedent) — documented + boundary, no fabricated detection. Contract-suite row added: + `PayloadTooLarge` occurrence asymmetry (pg client-side pre-round- + trip, SQLite never) — with no capabilities API, the variant is the + one runtime carriage of an engine asymmetry, so its matchability + is pinned. Re-entry gate: a consumer-inventory row naming a + runtime-adapt need. +- **Cross-references**: OQ-04 (the pinning that parked this), + OQ-10 (narrowed by this — no capability struct to govern), OQ-11, + [ADR-001](decisions/001-crate-split.md), + [ADR-006](decisions/006-wake-and-delivery-contract.md), + [ADR-008](decisions/008-contract-v1-pinning.md) §1/§5/§6, + [ADR-012](decisions/012-forked-substrate-design.md), + [deployment.md](deployment.md). + ## Deferred / Blocked None. **The open question set is empty** — every OQ above is resolved diff --git a/docs/architecture/queues.md b/docs/architecture/queues.md index c71995d..5b13900 100644 --- a/docs/architecture/queues.md +++ b/docs/architecture/queues.md @@ -1,6 +1,6 @@ --- status: draft -last_updated: 2026-10-07 (ADR-021 — third review round: tx reads, claimed_at, schedule queue validation) +last_updated: 2026-10-07 (ADR-021 + third-round follow-through: handle-form sweep_expired, ack_batch predicate, opts types) --- # Queues, scheduler, outbox — semantics depth @@ -88,9 +88,10 @@ made under; the ADRs carry the WHY. ▼ (gone) - expires lapse (any state) — sweep_expired(q) moves the row to - dead with last_error='expired'; also enforces dead-letter - retention. ack/cancel leave no row; get_job sees dead rows. + expires lapse (any state) — sweep_expired() on the queue handle + moves the row to dead with last_error='expired'; also enforces + dead-letter retention. ack/cancel leave no row; get_job sees + dead rows. ``` - **States**: `pending` → `processing` → `dead` (+ absence: @@ -151,7 +152,9 @@ made under; the ADRs carry the WHY. ## Sweep / maintenance (ADR-010 §5–§6) -- `sweep_expired(queue)` moves *every* past-`expires_at` row (pending +- `sweep_expired()` on the queue handle (the queue-scoped form, + [ADR-019](decisions/019-mechanism-handle-surfaces.md) §1) moves + *every* past-`expires_at` row (pending **and** processing) to dead, and enforces dead-letter retention — the **no-stranded-rows property** ([ADR-010](decisions/010-queue-semantics-depth.md) §5): a job with