ADR-014: transactional outbox enqueue — outbox_enqueue_tx on TxHandle (OQ-13 resolved)
This commit is contained in:
1 parent
d401908f13
commit
04a04651dc
8 files changed
+359
-122
No files matched your search
@@ -21,18 +21,19 @@ boundaries, fidelity posture, port deltas; its substrate-side residue
|
||||
is OQ-11); **the fork packaging folded** (2026-10-05,
|
||||
[ADR-013](decisions/013-fold-substrate-into-sqlite.md) — no
|
||||
`alkstore-substrate` crate; the forked machinery is a module subtree of
|
||||
`alkstore-sqlite`). Next: OQ-08 (rides the now-pinned trait shape and the
|
||||
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), 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).
|
||||
`alkstore-sqlite`); **OQ-13 resolved** (2026-10-05,
|
||||
[ADR-014](decisions/014-outbox-tx-enqueue.md) —
|
||||
`outbox_enqueue_tx` joins the `TxHandle` trait, the hole is closed).
|
||||
Next: **OQ-12** (streams depth — a Phase 1 review
|
||||
find: key semantics, `StreamEvent` shape, ordering row, retention;
|
||||
evidence gathered; check consumer rows for per-key-ordering need
|
||||
before choosing among its options), OQ-08 (rides the now-pinned trait
|
||||
shape and the 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),
|
||||
OQ-11 (fork follow-through items — substrate-side, non-consumer-facing).
|
||||
|
||||
Resolved questions stay listed with their resolution; they are not
|
||||
deleted.
|
||||
@@ -127,9 +128,10 @@ narrowed to the pinning work its own record already scoped.)*
|
||||
surface; the contract is the trait it returns. (7) Locks guarantee
|
||||
row added to ADR-006's table (TTL-bounded mutual exclusion, silent
|
||||
expiry); the scheduler row is explicitly transferred to OQ-09's
|
||||
resolution. A verification backlog (core-contract.md §Verification
|
||||
resolution. A verification backlog (core-contract.md §Verification
|
||||
backlog) tracks the one-engine-pinned properties.
|
||||
- **Cross-references**: OQ-01, OQ-10, OQ-09, OQ-08.
|
||||
- **Cross-references**: OQ-01, OQ-10, OQ-09, OQ-08; OQ-13 (the one
|
||||
gap the v1 pinning's own Phase 1 review found in it).
|
||||
|
||||
## Theme: Queues and scheduling
|
||||
|
||||
@@ -232,42 +234,6 @@ narrowed to the pinning work its own record already scoped.)*
|
||||
[ADR-011](decisions/011-sqlite-substrate-fork.md); ADR-005's posture
|
||||
calculus applied, not renegotiated.
|
||||
|
||||
## Theme: Deployment and capabilities
|
||||
|
||||
### OQ-08: Where does the honest single-host/multi-host boundary live in the trait surface? *(== OQ-ST-08)*
|
||||
|
||||
- **Origin**: [deployment.md](deployment.md)
|
||||
- **Status**: open
|
||||
- **Priority**: medium
|
||||
- **Resolution**: open. The pg engine is natively multi-host (POC #2
|
||||
verified — no single-host assumption to remove); SQLite is
|
||||
single-machine by nature (file-backed, NFS-two-writers unsupported —
|
||||
honker's honesty posture, inherited by the forked substrate). The
|
||||
unified surface must not pretend SQLite is multi-host. Options:
|
||||
per-engine capability flags (`Store::capabilities()`), a documented
|
||||
deployment matrix only ([deployment.md](deployment.md) carries the
|
||||
facts), or compile-time knowledge only (a consumer choosing the
|
||||
SQLite engine knows). Rides the now-pinned contract shape
|
||||
([ADR-008](decisions/008-contract-v1-pinning.md)): the trait
|
||||
constrains where capability differences can surface.
|
||||
- **Cross-references**: OQ-04, [ADR-006](decisions/006-wake-and-delivery-contract.md), [ADR-012](decisions/012-forked-substrate-design.md) (the substrate inherits the honesty posture).
|
||||
|
||||
### OQ-10: How do engine crates track core-contract version changes?
|
||||
|
||||
- **Origin**: [overview.md](overview.md),
|
||||
[ADR-001](decisions/001-crate-split.md)
|
||||
- **Status**: open
|
||||
- **Priority**: medium
|
||||
- **Resolution**: open. The core crate's trait surface is a contract
|
||||
the engine crates must track ([ADR-001](decisions/001-crate-split.md) negative consequence). What
|
||||
is the versioning/sync discipline — semver-bump-only-when-
|
||||
contract-changes, engines pin core ranges, a contract-compatibility
|
||||
test suite the engines run against the core's trait definitions?
|
||||
What happens to a released engine crate when core makes a contract
|
||||
breaking change?
|
||||
- **Cross-references**: OQ-04 (the contract being versioned — now
|
||||
including its first post-v1 extensions, ADR-009/ADR-010), OQ-02.
|
||||
|
||||
### OQ-11: Forked-substrate follow-through — scaffold, provenance register, and cherry-pick discipline
|
||||
|
||||
- **Origin**: [ADR-012](decisions/012-forked-substrate-design.md)
|
||||
@@ -302,6 +268,46 @@ 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-10: How do engine crates track core-contract version changes?
|
||||
|
||||
- **Origin**: [overview.md](overview.md),
|
||||
[ADR-001](decisions/001-crate-split.md)
|
||||
- **Status**: open
|
||||
- **Priority**: medium
|
||||
- **Resolution**: open. The core crate's trait surface is a contract
|
||||
the engine crates must track ([ADR-001](decisions/001-crate-split.md) negative consequence). What
|
||||
is the versioning/sync discipline — semver-bump-only-when-
|
||||
contract-changes, engines pin core ranges, a contract-compatibility
|
||||
test suite the engines run against the core's trait definitions?
|
||||
What happens to a released engine crate when core makes a contract
|
||||
breaking change?
|
||||
- **Cross-references**: OQ-04 (the contract being versioned — now
|
||||
including its first post-v1 extensions, ADR-009/ADR-010), OQ-02.
|
||||
|
||||
## Theme: Deployment and capabilities
|
||||
|
||||
### OQ-08: Where does the honest single-host/multi-host boundary live in the trait surface? *(== OQ-ST-08)*
|
||||
|
||||
- **Origin**: [deployment.md](deployment.md)
|
||||
- **Status**: open
|
||||
- **Priority**: medium
|
||||
- **Resolution**: open. The pg engine is natively multi-host (POC #2
|
||||
verified — no single-host assumption to remove); SQLite is
|
||||
single-machine by nature (file-backed, NFS-two-writers unsupported —
|
||||
honker's honesty posture, inherited by the forked substrate). The
|
||||
unified surface must not pretend SQLite is multi-host. Options:
|
||||
per-engine capability flags (`Store::capabilities()`), a documented
|
||||
deployment matrix only ([deployment.md](deployment.md) carries the
|
||||
facts), or compile-time knowledge only (a consumer choosing the
|
||||
SQLite engine knows). Rides the now-pinned contract shape
|
||||
([ADR-008](decisions/008-contract-v1-pinning.md)): the trait
|
||||
constrains where capability differences can surface.
|
||||
- **Cross-references**: OQ-04, [ADR-006](decisions/006-wake-and-delivery-contract.md), [ADR-012](decisions/012-forked-substrate-design.md) (the substrate inherits the honesty posture).
|
||||
|
||||
|
||||
|
||||
## Theme: Core contract (Phase 1 review finds)
|
||||
|
||||
### OQ-12: Streams depth — key semantics, `StreamEvent` shape, ordering row, retention
|
||||
|
||||
- **Origin**: [core-contract.md](core-contract.md) streams section,
|
||||
@@ -356,60 +362,47 @@ narrowed to the pinning work its own record already scoped.)*
|
||||
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
|
||||
### OQ-13: Transactional outbox enqueue shape — the `TxHandle` surface cannot express it — **RESOLVED**
|
||||
|
||||
- **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
|
||||
- **Status**: resolved (2026-10-05, Phase 1 —
|
||||
[ADR-014](decisions/014-outbox-tx-enqueue.md))
|
||||
- **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
|
||||
- **Resolution**: Pinned by
|
||||
[ADR-014](decisions/014-outbox-tx-enqueue.md): one method,
|
||||
`outbox_enqueue_tx(outbox, opts, payload) -> job_id`, added to the
|
||||
`TxHandle` trait. The method takes the **outbox name** (validated
|
||||
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` —
|
||||
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
|
||||
engine-internal naming (the same status `run_once`'s consumption of
|
||||
the backing queue already had). Option (b) (an `OutboxHandle` off
|
||||
the tx handle) is rejected — a second boxed handle to deliver one
|
||||
op; option (c) (non-transactional only) is rejected — it drops the
|
||||
property the mechanism exists for. Honker's raw-`Transaction` seam
|
||||
is not inherited (no engine-neutral `Transaction` type exists; it
|
||||
would force a core→engine dependency or a downcast seam — the two
|
||||
things ADR-008 §2 dissolved). Framed as completing contract v1 in
|
||||
place, pre-implementation — no versioning event, OQ-10 untouched.
|
||||
Contract-suite row added (commit-atomicity on both engines).
|
||||
- **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
|
||||
(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.
|
||||
(streams depth, capability-surface shape, versioning discipline,
|
||||
fork-scaffold follow-through) with its evidence base complete — no
|
||||
external arrivals are being waited on.
|
||||
Reference in new issue
Block a user