docs: Phase 1 review round — wake wording honesty, job-handle validity predicate, outbox/streams depth OQs
- 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.
This commit is contained in:
1 parent
8e68b44194
commit
d401908f13
9 files changed
+250
-35
No files matched your search
+14
-10
@@ -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.
|
||||
|
||||
@@ -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<Lock>` —
|
||||
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 <n><unit>` 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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Lock>` 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<dyn EventReceiver>` — the durable
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String>, payload, created_at: i64 }` with
|
||||
`payload_as<T>` (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.
|
||||
(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.
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user