From d401908f13a8814cecdbaf0a361d1c9185c726ae Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Mon, 5 Oct 2026 12:50:33 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20Phase=201=20review=20round=20=E2=80=94?= =?UTF-8?q?=20wake=20wording=20honesty,=20job-handle=20validity=20predicat?= =?UTF-8?q?e,=20outbox/streams=20depth=20OQs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C2: 'at-least-once wake delivery' collapsed to 'best-effort hints' (ADR-006 §1 + core-contract) — coalescing and the pg no-replay hole contradict per-commit wake promises; the guarantee table's row was already correct. - W2: uniform job-handle validity predicate pinned (ADR-010 §2, core-contract, queues.md; verification-backlog row): processing state + unexpired deadline; D-12's missing-check not inherited. - W1: outbox run_once worker semantics pinned in core-contract (pull op, ack/retry-on-curve, no heartbeat in delivery — honker parity, dual-execution window documented). - W3: named-locks tx-seam posture stated (no lock_tx; acquisition is auto-commit; TTL discipline governs). - C1 -> OQ-12: streams depth (key semantics w/ honker ground, event shape, ordering row, retention); method names pinned, depth open. - New find -> OQ-13: the v1 TxHandle surface cannot express the transactional outbox enqueue (derived backing-queue name is reserved-prefix-rejected; honker's raw-Transaction seam unavailable); option set + decision rule sketched. - README resolution order refreshed (OQ-06 resolved); ScheduleOpts + stream consumption-trigger lines added to core-contract. --- docs/architecture/README.md | 24 ++-- docs/architecture/core-contract.md | 75 +++++++++-- .../006-wake-and-delivery-contract.md | 10 +- .../decisions/008-contract-v1-pinning.md | 12 +- .../decisions/010-queue-semantics-depth.md | 14 ++ docs/architecture/engine-postgres.md | 5 + docs/architecture/engine-sqlite.md | 4 + docs/architecture/open-questions.md | 120 +++++++++++++++++- docs/architecture/queues.md | 21 ++- 9 files changed, 250 insertions(+), 35 deletions(-) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 012e4e8..ada1c59 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -24,9 +24,9 @@ pending architecture review and OQ resolution. | Doc | Status | Purpose | Key OQs | |---|---|---|---| | [overview.md](overview.md) | draft | Crate family, feature surface, non-goals, evidence base | — | -| [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-08, OQ-10 | -| [engine-sqlite.md](engine-sqlite.md) | draft | SQLite engine: forked-substrate/rusqlite mapping | OQ-05, OQ-06, OQ-09 (all resolved) | -| [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-05 (resolved), OQ-08, OQ-09 (resolved) | +| [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-08, OQ-10, OQ-12, OQ-13 | +| [engine-sqlite.md](engine-sqlite.md) | draft | SQLite engine: forked-substrate/rusqlite mapping | OQ-06 (resolved), OQ-12, OQ-13 | +| [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-05 (resolved), OQ-08, OQ-12, OQ-13 | | [queues.md](queues.md) | draft | Queue/scheduler/outbox semantics depth (resolved: ADR-009/ADR-010) | OQ-06 (resolved) | | [deployment.md](deployment.md) | draft | Host semantics, connection budgets, knobs, matrix | OQ-08 | | [open-questions.md](open-questions.md) | draft | OQ tracker (promoted from OQ-ST register) | — | @@ -53,13 +53,15 @@ pending architecture review and OQ resolution. Tracked in [open-questions.md](open-questions.md) (OQ-01..NN; the Phase 0 register's OQ-ST-01..08 promote one-to-one — OQ-NN mirrors -OQ-ST-NN — with new Phase 1 questions appended after). Highlights, -in suggested resolution order (OQ-04, OQ-09, OQ-05, OQ-06 resolved): +OQ-ST-NN — with new Phase 1 questions appended after). Open, in +suggested resolution order: -- **OQ-06** (high): honker-core quality read — **resolved** - (2026-10-05, [ADR-011](decisions/011-sqlite-substrate-fork.md)): the - fork trigger fired; SQLite substrate is owned code forked from - honker-core's lineage, queue ops re-derived on contract v1. +- **OQ-12** (high): streams depth — key semantics, `StreamEvent` + shape, ordering guarantee row, retention (Phase 1 review find; + evidence gathered in the OQ). +- **OQ-13** (high): transactional outbox enqueue shape — the v1 + `TxHandle` surface cannot express the outbox's one load-bearing + operation (Phase 1 review find; option set sketched in the OQ). - **OQ-08** (medium): capability-surface shape. - **OQ-10** (medium): contract versioning across engine crates. - **OQ-11** (medium): forked-substrate follow-through (provenance @@ -72,7 +74,9 @@ Resolved (kept with resolutions): OQ-01 (feature scope), OQ-02 [ADR-008](decisions/008-contract-v1-pinning.md)), ~~OQ-09~~ (scheduler collapse — [ADR-009](decisions/009-scheduler-collapse.md); scheduler guarantee row pinned), ~~OQ-05~~ (queue semantics depth — -[ADR-010](decisions/010-queue-semantics-depth.md)). +[ADR-010](decisions/010-queue-semantics-depth.md)), ~~OQ-06~~ +(honker-core quality read — fork fired, +[ADR-011](decisions/011-sqlite-substrate-fork.md)). No deferred OQs: all open questions are actionable Phase 1 work with complete evidence bases. diff --git a/docs/architecture/core-contract.md b/docs/architecture/core-contract.md index 42f2c64..cb782fd 100644 --- a/docs/architecture/core-contract.md +++ b/docs/architecture/core-contract.md @@ -108,9 +108,12 @@ never sees it. delivers opaque `Wake { channel }` signals ([ADR-006](decisions/006-wake-and-delivery-contract.md)), never payloads or ids ([ADR-008](decisions/008-contract-v1-pinning.md) §3). - Wakes are at-least-once, possibly coalesced (SQLite) or per-notify - (Postgres), possibly repeated after reconnect. **Consumers must be - idempotent on wake.** + Wakes are **best-effort hints, not per-commit delivery promises**: + possibly coalesced (SQLite) or per-notify (Postgres), possibly + repeated after reconnect, possibly absent entirely (SQLite burst + coalescing; the pg no-replay hole) — recovery is the consumer's + re-read plus the engine's reconnect surface, never a guaranteed + deliver. **Consumers must be idempotent on wake.** - Failure surfaces as channel events, never silence: watcher death closes the receiver (`recv() -> None`, SQLite); the synthetic reconnect-wake (a reserved channel @@ -130,7 +133,11 @@ transaction-aware (`save_offset_tx` gives exactly-once-within-a- business-tx shape). - `publish` / `publish_tx` / `publish_with_key` — append to the - stream's durable log. + stream's durable log. *(Depth annotation: the key's semantics, the + `StreamEvent` shape, the ordering row, and log retention are + OQ-12's — method names pinned by + [ADR-008](decisions/008-contract-v1-pinning.md) §1, depth deferred + there; see [open-questions.md](open-questions.md).)* - `read_since` / `read_from_consumer(offset)` — cursor-based reads; `get_offset(consumer)` — checkpoint inspection. - `save_offset` / `save_offset_tx` — consumer checkpoint, explicit; @@ -142,6 +149,13 @@ business-tx shape). 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). + 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 + receiver's engine-side trigger is engine-internal, but events + never require polling to become visible (the queues row's pg + re-poll safety net is an admission of LISTEN loss, not the + posture). ### queues @@ -165,7 +179,14 @@ obligations: attempt**; `retry(err, None)` computes the queue's equal-jitter exponential delay (range pinned by [ADR-010](decisions/010-queue-semantics-depth.md) §3), `retry(err, - Some(d))` overrides; `fail` = immediate dead-letter. Dead letters: + Some(d))` overrides; `fail` = immediate dead-letter. Handle-op + validity (uniform predicate): an op succeeds only while the row is + `processing` and the caller's claim deadline is unexpired — deadline + lapse refuses all handle ops (heartbeats, acks included) and a + reclaim does too; the dual-execution window's worker may still + complete its work but its ack will not land (at-least-once, as + documented); false, not error + ([ADR-010](decisions/010-queue-semantics-depth.md) §2). Dead letters: move-to-dead storage, `get_job` sees dead rows (with `last_error`/`died_at`), retention via `dead_letter_retention_s` (default forever), no redrive API @@ -181,7 +202,13 @@ obligations: ### named locks -TTL-bounded coordination locks, transactional-friendly: +TTL-bounded coordination locks. Lock rows live in the same database +as the caller's business data — coordination and data co-locate — +but lock ops are **not** part of the tx seam: there is no `lock_tx`; +a lock held across a business transaction is held by explicit +acquisition before/inside it, not transaction-scoped (acquisition is +a separate auto-commit op; rollback of the business tx does not +release it — the TTL discipline governs). - `try_lock(name, owner, ttl)` — acquire or fail (`Option` — no-work is a value, not an error); `renew` and `release` on the @@ -201,7 +228,19 @@ TTL-bounded coordination locks, transactional-friendly: A helper over queues, not a separate mechanism: enqueue inside the business transaction + the delivery/consumption worker entry points. Same evidence base and guarantee as queues -([ADR-002](decisions/002-feature-scope.md)). +([ADR-002](decisions/002-feature-scope.md)). Worker semantics +(honker's `run_once` shape, inherited as the pinned posture): +`run_once(worker_id, delivery)` is a *pull* op — claim one 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): +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; +idempotent delivery is the consumer's obligation, same as queues. +The transactional *enqueue* side's shape is OQ-13's (the v1 surface +has a real hole there — see +[open-questions.md](open-questions.md)). ### scheduler @@ -213,9 +252,12 @@ the reserved name `__alkstore_scheduler`). Schedules never fire without a runner (no ambient timers). v1 spec grammar: `@every ` only (`s|m|h|d`); cron strings are rejected (`InvalidSpec`) pending a consumer-inventory row naming wall-clock -cron. Boundary guarantee: -[ADR-009](decisions/009-scheduler-collapse.md) §4 (at-least-once per -elapsed boundary, bounded catch-up with skip-forward past the cap). +cron. Each boundary fire enqueues ordinary work stamped with +`ScheduleOpts { priority, max_attempts, expires }` +([ADR-009](decisions/009-scheduler-collapse.md) §3). Boundary +guarantee: [ADR-009](decisions/009-scheduler-collapse.md) §4 +(at-least-once per elapsed boundary, bounded catch-up with +skip-forward past the cap). ## Cross-cutting contracts @@ -228,7 +270,7 @@ of record for notify/streams/queues; the locks row is row is [ADR-009](decisions/009-scheduler-collapse.md) §4's. This spec inherits them and adds the consumer obligations: -- wake idempotence (notify's at-least-once, coalescing behavior); +- wake idempotence (notify's best-effort, coalescing behavior); - explicit offset saves (streams); - visibility-timeout budgeting — heartbeat inside the deadline for long work; the dual-execution window and reclaim-eats-attempt rules @@ -335,6 +377,13 @@ before the engine specs are called `stable`: per [ADR-012](decisions/012-forked-substrate-design.md) §2's contract-blind boundary; the contract suite must pin both engines' arithmetic to identical outputs. +- **Job-handle validity predicate on both engines** — the uniform + processing-state + unexpired-deadline rule + ([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. ## Design Decisions @@ -359,6 +408,10 @@ questions affecting this document: - **OQ-10**: contract versioning discipline across engine crates ([open](open-questions.md)) - **OQ-08**: capability-surface shape ([open](open-questions.md)) +- **OQ-12**: streams depth — key semantics, `StreamEvent` shape, + ordering row, retention ([open](open-questions.md)) +- **OQ-13**: transactional outbox enqueue shape + ([open](open-questions.md)) Resolved on this document's surface: **OQ-09** (scheduler collapse — [ADR-009](decisions/009-scheduler-collapse.md)) and **OQ-05** (queue diff --git a/docs/architecture/decisions/006-wake-and-delivery-contract.md b/docs/architecture/decisions/006-wake-and-delivery-contract.md index 0189b2b..3f2195d 100644 --- a/docs/architecture/decisions/006-wake-and-delivery-contract.md +++ b/docs/architecture/decisions/006-wake-and-delivery-contract.md @@ -42,9 +42,13 @@ a cache invalidates on wake and re-reads, instead of re-polling). - Postgres: LISTEN delivers per-notification (no coalescing), and the forwarder's synthetic reconnect-wake covers connection gaps. - Both: consumer code must be correct if wakes repeat, coalesce, or - arrive in any order, with at-least-once wake delivery. Exactly-once - *processing* semantics belong to queues/streams (below), never to - the wake layer. + arrive in any order. **Wakes are best-effort hints, not a per-commit + delivery guarantee** — SQLite coalescing and Postgres's no-replay hole + (a commit during a connection gap is never re-delivered) both mean + individual changes can get *no* wake; recovery is consumers' re-read, + the reconnect-wake, and idempotence — never a promised per-commit + deliver. Exactly-once *processing* semantics belong to queues/streams + (below), never to the wake layer. Measured ground: wake latency p50 ≈ 1.1–2.2 ms on both engines at default cadence; per-payload burst delivery verified (30/30 on pg, diff --git a/docs/architecture/decisions/008-contract-v1-pinning.md b/docs/architecture/decisions/008-contract-v1-pinning.md index aa30349..f5b87f8 100644 --- a/docs/architecture/decisions/008-contract-v1-pinning.md +++ b/docs/architecture/decisions/008-contract-v1-pinning.md @@ -49,6 +49,11 @@ implement identically): `read_from_consumer`, `save_offset`, `get_offset`, `subscribe` (durable log, explicit offsets — the auto-checkpoint ambiguity is not inherited, per [ADR-006](006-wake-and-delivery-contract.md)). + *(Depth annotation 2026-10-05: the key's semantics, the `StreamEvent` + shape, the ordering guarantee row, and log retention are OQ-12's — + the method *names* are pinned here, their depth was left + un-pinned, mirrored in [core-contract.md](../core-contract.md) + streams.)* - queues v1 skeleton — `enqueue`, `claim_one`, `claim_batch`, `ack_batch`, `cancel`, `get_job`, `sweep_expired`; job handle `ack / retry / fail / heartbeat`; `EnqueueOpts { delay, run_at, @@ -58,7 +63,12 @@ implement identically): - locks — `try_lock(name, owner, ttl) -> Option` with `renew` and `release`. - outbox helper — `outbox(name)` with `enqueue` + `run_once` delivery - worker. + worker. *(Depth annotation 2026-10-05: the transactional *enqueue* + shape on the `TxHandle` seam is OQ-13's — the v1 method list has no + outbox enqueue method and the derived backing queue's reserved + prefix makes plain `enqueue_tx` illegal; see + [open-questions.md](../open-questions.md). `run_once` worker + semantics are pinned in core-contract.md's outbox section.)* - the tx seam — `begin_tx` / commit / rollback and the `*_tx` methods ([ADR-007](007-transactional-seam.md)). - `subscribe(consumer) -> Box` — the durable diff --git a/docs/architecture/decisions/010-queue-semantics-depth.md b/docs/architecture/decisions/010-queue-semantics-depth.md index cdf10fc..1485f43 100644 --- a/docs/architecture/decisions/010-queue-semantics-depth.md +++ b/docs/architecture/decisions/010-queue-semantics-depth.md @@ -94,6 +94,20 @@ no-renewal/expire-sweep model is not inherited): claimable by ordinary claim (no transition fires on lapse); there is no separate reaper for expired claims, only the budget rules below and the no-stranded-rows sweep (§5). +- **Job-handle op validity predicate** (the D-12 disposition, stated + uniformly): every handle op (`ack`/`heartbeat`/`retry`/`fail`) + succeeds only when the row is in `processing` **and** the caller's + claim deadline has not lapsed — ADR-008 §5's false-case list + ("expired, acked elsewhere, cancelled") is the same rule one + predicate-wide: deadline lapse refuses *all* ops, not just + heartbeat. The dual-execution window stays honest under this: the + original worker between lapse and reclaim may still *complete its + work* (the side effects happen), but its ack will not land and the + row is reprocessed at reclaim — at-least-once, as documented. The + reclaim wins atomically when it races the ack (one statement, same + predicate). This state check is engine-pinned in both engines' + implementations (upstream's missing-check defect class D-12 is + *not* inherited). ### 3. Retry and backoff: explicit delay or the queue's curve diff --git a/docs/architecture/engine-postgres.md b/docs/architecture/engine-postgres.md index 33afdf3..aca60f1 100644 --- a/docs/architecture/engine-postgres.md +++ b/docs/architecture/engine-postgres.md @@ -118,6 +118,11 @@ questions affecting this document: - **OQ-08**: capability surface (shared with [deployment.md](deployment.md)) (open) +- **OQ-12**: streams depth (affects this engine's stream realization + — event-shape/ordering/retention equivalents on the pg side) + ([open](open-questions.md)) +- **OQ-13**: transactional outbox enqueue shape (affects this + engine's `TxHandle` impl) ([open](open-questions.md)) Resolved: **OQ-09** (scheduler collapse — [ADR-009](decisions/009-scheduler-collapse.md)) and **OQ-05** (queue diff --git a/docs/architecture/engine-sqlite.md b/docs/architecture/engine-sqlite.md index be4c6d4..8537526 100644 --- a/docs/architecture/engine-sqlite.md +++ b/docs/architecture/engine-sqlite.md @@ -135,6 +135,10 @@ questions affecting this document: - **OQ-06**: honker-core quality read — fork-trigger assessment — **resolved** (2026-10-05, [ADR-011](decisions/011-sqlite-substrate-fork.md); trigger fired). +- **OQ-12**: streams depth (affects this engine's stream realization + — key column, event shape, retention) ([open](open-questions.md)) +- **OQ-13**: transactional outbox enqueue shape (affects this + engine's `TxHandle` impl) ([open](open-questions.md)) Resolved: **OQ-09** (scheduler collapse — [ADR-009](decisions/009-scheduler-collapse.md)) and **OQ-05** (queue diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 1ca7e00..bbb832a 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -26,8 +26,13 @@ ADR-011 substrate fork), OQ-10 (versioning discipline for contract extensions; note ADR-011 changes its substrate-side facts for SQLite — the forked machinery lives in-tree inside the engine crate per [ADR-013](decisions/013-fold-substrate-into-sqlite.md), so the -engine/core contract pairing is what the discipline must track), and -OQ-11 (fork follow-through items — substrate-side, non-consumer-facing). +engine/core contract pairing is what the discipline must track), OQ-11 +(fork follow-through items — substrate-side, non-consumer-facing), +**OQ-12** (streams depth — a Phase 1 review find: key semantics, +`StreamEvent` shape, ordering row, retention; evidence gathered), and +**OQ-13** (the transactional outbox enqueue shape — a Phase 1 review +find: the v1 `TxHandle` surface cannot express the outbox's one +load-bearing operation). Resolved questions stay listed with their resolution; they are not deleted. @@ -297,9 +302,114 @@ narrowed to the pinning work its own record already scoped.)* carries the fold's provenance duties in-tree), ADR-011, ADR-012, ADR-013. +### OQ-12: Streams depth — key semantics, `StreamEvent` shape, ordering row, retention + +- **Origin**: [core-contract.md](core-contract.md) streams section, + [ADR-008](decisions/008-contract-v1-pinning.md) §1 (Phase 1 + architecture review, 2026-10-05) +- **Status**: open +- **Priority**: high (streams is a first-class + [ADR-002](decisions/002-feature-scope.md) mechanism; ADR-008's + "no placeholder semantics remain" positive consequence is false for + `publish_with_key` until this resolves — the contract suite also + cannot pin cross-engine stream equivalence without an ordering row) +- **Resolution**: open. The method *names* are pinned (ADR-008 §1); + the depth is not. Sub-questions, with the evidence already gathered: + - **Key semantics** — honker's realization is a stored `key` column + (`_honker_stream(topic, key, payload)`, NULLable) with no + per-key read path or ordering enforcement anywhere; the honker-rs + doc comment says "used for per-key ordering downstream" + (`packages/honker-rs/src/lib.rs:894`), i.e. per-key global-FIFO + is an *emergent* property (a consumer reading `offset ASC` + filtered by key), not server-enforced. Options: (a) pin exactly + honker's shape — key is carried metadata, ordering stays global + FIFO (document the emergent per-key pattern), cheapest and + honest; (b) pin *server-enforced* per-key ordering (a read or + delivery guarantee keyed on `key`) — real machinery on both + engines, needs a consumer row naming the need; (c) cut + `publish_with_key` from v1 (no consumer row names keys — the + honker-rs doc comment is upstream's intent, not ours). Decision + rule: consumer-inventory row first if (b). + - **`StreamEvent` shape** — honker's: `{ offset: i64, topic: String, + key: Option, payload, created_at: i64 }` with + `payload_as` (upstream lib.rs:1008). The contract needs its own + pinned field list (stream name vs topic token, key in or out per + the key decision, timestamp semantics) — probably inherited + near-verbatim. + - **Ordering guarantee row** — ADR-006's table has no streams + *ordering* cell content beyond "readable"; pin FIFO-by-offset as + the read ordering (`ORDER BY offset ASC` is already both + engines' claim path) and whether any per-key strengthening rides + the key decision. + - **Retention / bounded growth** — the reference read flags + `_honker_stream` as unbounded-growth unless the user sweeps + (honker-machinery §8.5); no consumer row names stream retention + (replay-forever is the *purpose* for the named need — + subscription surfaces). Options: consumer-side trim API + (`trim_to(offset)` — a new contract method), consumer-recipe + only (new stream + advance consumers + unschedule/nothing — + needs a deletion path that doesn't exist yet, so this option is + really "document the gap"), or document nothing (honest but + repeats the notifications-table mistake ADR-010 §6 just fixed). + Rides the no-ambient-sweeper posture either way. +- **Cross-references**: OQ-04 (the pinning that left this depth + open), OQ-01 (scope row), ADR-002, ADR-006 (the guarantee table + the ordering row extends), ADR-008 §1. + +### OQ-13: Transactional outbox enqueue shape — the `TxHandle` surface cannot express it + +- **Origin**: [core-contract.md](core-contract.md) (outbox section), + [ADR-008](decisions/008-contract-v1-pinning.md) §1 (Phase 1 + architecture review, 2026-10-05) +- **Status**: open +- **Priority**: high (the outbox's *only reason to exist* is the + commit-atomic enqueue; as pinned, the v1 surface cannot perform it + — a decomposition off the current text would produce an + unimplementable task) +- **Resolution**: open. The hole, stated precisely: the outbox is + "enqueue inside the business transaction + delivery workers" + ([ADR-002](decisions/002-feature-scope.md)); the delivery side is + pinned (`run_once`, plus plain queue consumption of the backing + queue), but the enqueue side has no method. The derived backing + queue name `__alkstore_outbox:{name}` + ([ADR-008](decisions/008-contract-v1-pinning.md) §4) is + reserved-prefix — every entry point **rejects** it, so + `handle.enqueue_tx("__alkstore_outbox:x", …)` is contract-illegal + by construction; and `Outbox` (the handle `store.outbox(name)` + returns) is not in the `TxHandle` method set + ([ADR-008](decisions/008-contract-v1-pinning.md) §1, §2), so there + is no `outbox_enqueue_tx` either. Honker solves it with a + different seam: `outbox.enqueue_tx(&tx, …)` routes through the + caller's raw `Transaction` + (`packages/honker-rs/src/lib.rs:506-513`) — a seam our handle + design ([ADR-007](decisions/007-transactional-seam.md), + [ADR-008](decisions/008-contract-v1-pinning.md) §2 — the handle + *is* the tx, no downcast) deliberately does not have. Options + (starter set, not exhaustive): + (a) `outbox_enqueue_tx(name, payload)` on the `TxHandle` trait — + symmetric with the other `*_tx` methods, one method, engine resolves + the derived name on the engine side (bypasses the entry-point + rejection legitimately, since the *method* takes the outbox name, + not the derived queue name); + (b) an `OutboxHandle` obtained from the `TxHandle` + (`handle.outbox(name) -> OutboxHandle` with enqueue) — more + symmetrical with the store-side handle, two hops; + (c) non-transactional only — `outbox.enqueue` auto-commit, dropping + the commit-atomic property (defeats the purpose; listed to be + rejected explicitly). + Decision rule: smallest surface that restores the + transactional-local-adjacency property (guiding principle 2) for + the outbox without opening the reserved namespace; (a) reads + strongest against ADR-008 §2's rationale and needs one contract + suite row. +- **Cross-references**: OQ-04 (the pinning that missed this), + OQ-12 (the sibling depth gap found by the same review), ADR-002 + (outbox scope row), ADR-007/ADR-008 (the seam design), ADR-010 §3 + (the outbox's derived QueueOpts — unchanged by this). + ## Deferred / Blocked None currently. Every open OQ above is actionable Phase 1 work -(capability-surface shape, versioning discipline, fork-scaffold -follow-through) with its evidence base complete — no external arrivals -are being waited on. \ No newline at end of file +(streams depth, outbox enqueue shape, capability-surface shape, +versioning discipline, fork-scaffold follow-through) with its evidence +base complete — no external arrivals are being waited on. diff --git a/docs/architecture/queues.md b/docs/architecture/queues.md index 9e98f04..3d14a10 100644 --- a/docs/architecture/queues.md +++ b/docs/architecture/queues.md @@ -65,8 +65,15 @@ made under; the ADRs carry the WHY. ┌────────┐ fail(err) / budget │ │ dead │ ◄────── exhausted ───────────┘ └────────┘ - │ ack deletes the row - │ (from processing, claim still valid) + │ ack deletes the row + │ (from processing, deadline unexpired) + + Handle-op validity (ADR-010 §2, uniform): ack/heartbeat/retry/ + fail succeed only while the row is processing and the claim's + deadline is unexpired — lapse refuses all handle ops; a worker + whose deadline lapsed before reclaim may still complete its + work, but its ack does not land (the job reprocesses at + reclaim — at-least-once). │ ▼ (gone) @@ -84,9 +91,13 @@ made under; the ADRs carry the WHY. [ADR-010](decisions/010-queue-semantics-depth.md) §3a). `heartbeat(extend)` is renewal — an absolute reset from now (the new full deadline, not additive); a **late heartbeat is refused** (never - steals the job back from a reclaimer). Between deadline lapse and - another worker's reclaim, the original worker may still complete — - the dual-execution window is the contract's honest at-least-once + steals the job back from a reclaimer). Deadline lapse refuses all + handle ops uniformly (the validity predicate, + [ADR-010](decisions/010-queue-semantics-depth.md) §2). Between + deadline lapse and another worker's reclaim, the original worker may + still complete its *work* — but its ack refuses and the row + reprocesses at reclaim — the dual-execution window is the + contract's honest at-least-once posture; downstream idempotence is the consumer's job. - **Reclaim consumes an attempt**: a handler that forgets to heartbeat looks like a repeatedly-failing job and dead-letters on