docs: resolve OQ-04 — contract v1 pinned (ADR-008): surface partition, TxHandle-on-handle-trait, Wake/WakeReceiver, reserved __alkstore_ namespace, error taxonomy, engine-crate constructors, locks guarantee row
This commit is contained in:
1 parent
4391f6e879
commit
7ad8ac56bc
11 files changed
+650
-133
No files matched your search
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-04
|
||||
last_updated: 2026-10-05
|
||||
---
|
||||
|
||||
# alkstore — Architecture
|
||||
@@ -24,11 +24,11 @@ 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-04, OQ-08, OQ-09, OQ-10 |
|
||||
| [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-05, OQ-08, OQ-09, OQ-10 |
|
||||
| [engine-sqlite.md](engine-sqlite.md) | draft | SQLite engine: honker-core/rusqlite mapping | OQ-05, OQ-06, OQ-09 |
|
||||
| [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-05, OQ-08, OQ-09 |
|
||||
| [queues.md](queues.md) | draft | Queue/scheduler/outbox semantics depth frame | OQ-05, OQ-09 |
|
||||
| [deployment.md](deployment.md) | draft | Host semantics, connection budgets, knobs, matrix | OQ-04, OQ-08 |
|
||||
| [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) | — |
|
||||
|
||||
## Architecture Decision Records
|
||||
@@ -42,22 +42,25 @@ pending architecture review and OQ resolution.
|
||||
| [005](decisions/005-dependency-ownership.md) | Published libraries by default, named fork triggers | Accepted |
|
||||
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract — opaque wake + re-read; notify-vs-streams split | Accepted |
|
||||
| [007](decisions/007-transactional-seam.md) | Transactional seam — caller-held `TxHandle`, `*_tx` methods | Accepted |
|
||||
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 surface pinning — surface partition, `TxHandle` shape, wake type, reserved strings, error taxonomy | Accepted |
|
||||
|
||||
## Open Questions
|
||||
|
||||
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 first):
|
||||
in suggested resolution order (OQ-04 resolved):
|
||||
|
||||
- **OQ-04** (high): contract pinning — exact trait shape, handle
|
||||
- ~~**OQ-04** (high): contract pinning — exact trait shape, handle
|
||||
representation, error taxonomy, reserved strings, guarantee rows
|
||||
for locks/scheduler.
|
||||
for locks/scheduler~~ — **resolved** (2026-10-05,
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md); the scheduler
|
||||
guarantee row transferred to OQ-09).
|
||||
- **OQ-05** (high): queue semantics depth — retry/backoff/dead-letter/
|
||||
sweep design.
|
||||
- **OQ-06** (high): honker-core quality read — fork-trigger gate.
|
||||
- **OQ-09** (medium): scheduler as first-class mechanism vs queues +
|
||||
`schedule()`.
|
||||
`schedule()`; owns the scheduler guarantee row.
|
||||
- **OQ-06** (high): honker-core quality read — fork-trigger gate.
|
||||
- **OQ-08** (medium): capability-surface shape.
|
||||
- **OQ-10** (medium): contract versioning across engine crates.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-04
|
||||
last_updated: 2026-10-05
|
||||
---
|
||||
|
||||
# Core contract
|
||||
@@ -10,8 +10,10 @@ specifies WHAT the contract is; the per-engine specs map it onto their
|
||||
machinery; ADRs carry the WHY. The starting artifact is the honker-rs
|
||||
surface (the Phase 0 §Interface finding) scoped to the
|
||||
inventory-confirmed features — [ADR-002](decisions/002-feature-scope.md)
|
||||
— and pinned against it. Exact Rust shapes are OQ-04's pinning work;
|
||||
the *obligations* are this document.
|
||||
— and pinned against it. The exact surface is pinned by
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) (contract v1 partition,
|
||||
`TxHandle` representation, wake type, reserved strings, error
|
||||
taxonomy, config split); the *obligations* are this document.
|
||||
|
||||
## Concepts
|
||||
|
||||
@@ -22,9 +24,9 @@ the *obligations* are this document.
|
||||
- **Mechanism** — one of the surface's coordination families: notify,
|
||||
streams, queues (+outbox), locks, scheduler. Delivery guarantees are
|
||||
pinned per mechanism in [ADR-006](decisions/006-wake-and-delivery-contract.md)'s
|
||||
table (notify/streams/queues); lock and scheduler guarantee rows are
|
||||
OQ-04/OQ-09 resolution items, not assumed.
|
||||
|
||||
table (notify/streams/queues), extended with the locks row by
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md); the scheduler row
|
||||
rides OQ-09's collapse decision.
|
||||
*(Guiding principles are defined in `docs/research/phase-0.md`
|
||||
§Vision and cited by number throughout this directory.)*
|
||||
- **Wake** — the opaque "something changed, re-read" signal the wake
|
||||
@@ -32,13 +34,28 @@ the *obligations* are this document.
|
||||
The contract's central abstraction.
|
||||
- **TxHandle** — the caller-held transaction object whose `*_tx`
|
||||
methods make side effects commit-atomic with a business write
|
||||
([ADR-007](decisions/007-transactional-seam.md)).
|
||||
([ADR-007](decisions/007-transactional-seam.md)). Contract shape:
|
||||
the `*_tx` methods live on the `TxHandle` trait itself — engines
|
||||
implement it for their concrete handle, no downcast
|
||||
([ADR-008](decisions/008-contract-v1-pinning.md) §2).
|
||||
|
||||
## The seam
|
||||
|
||||
Per [ADR-008](decisions/008-contract-v1-pinning.md) §6, the trait is
|
||||
constructed by engine crates (durability knobs, pool sizing, watcher
|
||||
cadence are engine options — [deployment.md](deployment.md) carries
|
||||
the facts); the contract is the trait surface it returns:
|
||||
|
||||
```text
|
||||
Store::open(config) -> Store
|
||||
Store::begin_tx() -> TxHandle
|
||||
Store::begin_tx() -> Box<dyn TxHandle + Send>
|
||||
|
||||
trait TxHandle {
|
||||
enqueue_tx(name, opts, payload) -> job_id
|
||||
publish_tx(stream, payload) -> event_id
|
||||
notify_tx(channel, payload)
|
||||
save_offset_tx(stream, consumer, offset)
|
||||
commit(self: Box<Self>) -> Result<()> // or rollback
|
||||
}
|
||||
```
|
||||
|
||||
Mechanism handles come off the store (or, for transactional variants,
|
||||
@@ -47,12 +64,23 @@ off the handle):
|
||||
```text
|
||||
store.notify(channel, payload) handle.notify_tx(channel, payload)
|
||||
store.stream(name) -> Stream handle.publish_tx / save_offset_tx
|
||||
store.queue(name) -> Queue handle.enqueue_tx
|
||||
store.try_lock(name, ttl) -> Lock
|
||||
store.queue(name, opts) -> Queue handle.enqueue_tx
|
||||
store.try_lock(name, owner, ttl) -> Option<Lock>
|
||||
store.listen(channel) -> Box<dyn WakeReceiver>
|
||||
store.outbox(name) -> Outbox
|
||||
```
|
||||
|
||||
The exact shape of these handles (traits vs concrete types, `dyn` vs
|
||||
generic `TxHandle`) is OQ-04's pinning work.
|
||||
Payloads cross the trait as core value types (non-generic trait
|
||||
methods — object safety of the boxed handles,
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) §2); `payload_as<T>`
|
||||
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)).
|
||||
Mechanism handles (`Stream`, `Queue`, `Outbox`, `Lock`) are core-owned
|
||||
trait objects like the tx handle — engine types never appear in
|
||||
consumer signatures; their trait methods pin at implementation,
|
||||
mirroring the `TxHandle` pattern ([ADR-008](decisions/008-contract-v1-pinning.md)
|
||||
§2's rationale applies identically).
|
||||
|
||||
## Mechanism contracts
|
||||
|
||||
@@ -64,20 +92,24 @@ replay, no per-listener retry; a listener attached after a commit
|
||||
never sees it.
|
||||
|
||||
- `notify(channel, payload)` — payload ≤ 8000 bytes on Postgres
|
||||
(client-side checked, typed error, verified by POC #2); no limit on
|
||||
SQLite. The asymmetry is a documented engine capability, see
|
||||
OQ-04/OQ-08.
|
||||
- `listen(channel)` — starts from "now"; delivers
|
||||
opaque wakes ([ADR-006](decisions/006-wake-and-delivery-contract.md)).
|
||||
(client-side checked, typed `PayloadTooLarge` before the round
|
||||
trip, verified by POC #2); 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) — see also
|
||||
OQ-08.
|
||||
- `listen(channel) -> Box<dyn WakeReceiver>` — starts from "now";
|
||||
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.** (The sketched return type — a wake receiver vs
|
||||
a subscription handle — is an OQ-04 pinning item, like all exact
|
||||
shapes here.)
|
||||
idempotent on wake.**
|
||||
- Failure surfaces as channel events, never silence: watcher death
|
||||
closes the receiver (SQLite); the synthetic reconnect-wake (a
|
||||
reserved channel, [ADR-004](decisions/004-postgres-driver.md)) covers
|
||||
Postgres connection gaps.
|
||||
closes the receiver (`recv() -> None`, SQLite); the synthetic
|
||||
reconnect-wake (a reserved channel
|
||||
([ADR-004](decisions/004-postgres-driver.md))) covers Postgres
|
||||
connection gaps.
|
||||
- Channel name is the one piece of semantic content a wake carries —
|
||||
the invalidation key for caching subscribers.
|
||||
|
||||
@@ -93,12 +125,17 @@ business-tx shape).
|
||||
|
||||
- `publish` / `publish_tx` / `publish_with_key` — append to the
|
||||
stream's durable log.
|
||||
- `read_since` / `read_from_consumer(offset)` — cursor-based reads.
|
||||
- `read_since` / `read_from_consumer(offset)` — cursor-based reads;
|
||||
`get_offset(consumer)` — checkpoint inspection.
|
||||
- `save_offset` / `save_offset_tx` — consumer checkpoint, explicit;
|
||||
**the contract's save is always explicit** (no auto-checkpoint
|
||||
cadence — honker's per-binding ambiguity is not inherited).
|
||||
- `subscribe(consumer)` — durable consumption: attach, read to
|
||||
current tail, resume after restart from the stored offset.
|
||||
cadence — honker's per-binding ambiguity is not inherited). The
|
||||
subscription handle exposes no `save_every` / auto-save-on-drop
|
||||
([ADR-008](decisions/008-contract-v1-pinning.md) §8).
|
||||
- `subscribe(consumer) -> Box<dyn EventReceiver>` — durable
|
||||
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).
|
||||
|
||||
### queues
|
||||
|
||||
@@ -108,9 +145,12 @@ is [queues.md](queues.md)'s (OQ-05); the contract-level obligations
|
||||
here:
|
||||
|
||||
- `enqueue` / `enqueue_tx` — commit-atomic; `EnqueueOpts { delay,
|
||||
priority, max_attempts, expires, ... }`.
|
||||
- `claim` / `claim_batch` — exactly-once handout under concurrency
|
||||
(POC-pinned on both engines).
|
||||
run_at, priority, max_attempts, expires }` (the v1 skeleton,
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) §1; deeper
|
||||
`QueueOpts`/retry surface rides OQ-05).
|
||||
- `claim_one` / `claim_batch` — exactly-once handout under concurrency
|
||||
(POC-pinned on both engines); `cancel`, `get_job` — job management
|
||||
and inspection.
|
||||
- Job handle: `ack / retry / fail / heartbeat`; visibility timeouts
|
||||
make at-least-once *safe* (work re-appears if a claimant dies).
|
||||
- `sweep_expired` — maintenance entry point (design in
|
||||
@@ -120,12 +160,18 @@ here:
|
||||
|
||||
TTL-bounded coordination locks, transactional-friendly:
|
||||
|
||||
- `try_lock(name, ttl)` — acquire or fail; `renew`; release on
|
||||
explicit unlock or TTL expiry. Re-acquirable after expiry
|
||||
(POC-pinned on Postgres; on SQLite it rests on honker's machinery —
|
||||
the SQLite-side pin is in OQ-04's verification backlog).
|
||||
- Lock names are shared-namespace (contract text pins collision
|
||||
rules with the reserved namespace, OQ-04).
|
||||
- `try_lock(name, owner, ttl)` — acquire or fail (`Option<Lock>` —
|
||||
no-work is a value, not an error); `renew` and `release` on the
|
||||
lock handle. Release on explicit unlock or TTL expiry.
|
||||
Re-acquirable after expiry (POC-pinned on Postgres; on SQLite it
|
||||
rests on honker's machinery — the SQLite-side pin is in the
|
||||
verification backlog below).
|
||||
- **Guarantee row** ([ADR-008](decisions/008-contract-v1-pinning.md)
|
||||
§7): mutual exclusion bounded by TTL + renewal — after TTL expiry
|
||||
exclusion lapses *silently* (no revocation event); holders must
|
||||
`renew` within TTL; expiry is a loss of exclusivity, not an error.
|
||||
- Lock names are shared-namespace (reserved-prefix rules below,
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) §4).
|
||||
|
||||
### outbox
|
||||
|
||||
@@ -159,26 +205,69 @@ the consumer obligations:
|
||||
|
||||
### Errors
|
||||
|
||||
`thiserror`-typed, per the family standard. The taxonomy (what
|
||||
callers match on per mechanism; engine-capability errors like
|
||||
`PayloadTooLarge` on pg vs no-limit on SQLite) is OQ-04's pinning
|
||||
work.
|
||||
`thiserror`-typed, per the family standard. Pinned by
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) §5: one top-level
|
||||
`Error`, with v1 variants `PayloadTooLarge` (universal; produced
|
||||
pg-side, contract-wide matchable), `ReservedName`, `InvalidName`,
|
||||
`Closed`, `Codec`, and the opaque `Database` fallback (engine detail
|
||||
preserved via the source chain). Pinning rule: a variant exists only
|
||||
when callers can act differently on it. Queue claim "no work" and
|
||||
job/lock boolean results are values, not errors.
|
||||
|
||||
### Capability surface
|
||||
|
||||
Whether the `Store` exposes engine capabilities at all — and if so,
|
||||
which (payload limits, host semantics, wake-cadence knobs) — is
|
||||
OQ-08's decision ([deployment.md](deployment.md)).
|
||||
None in contract v1. Whether the `Store` exposes engine capabilities
|
||||
at all — and if so, which (payload limits, host semantics,
|
||||
wake-cadence knobs) — is OQ-08's decision
|
||||
([deployment.md](deployment.md)).
|
||||
|
||||
### Naming / reserved namespace
|
||||
|
||||
Consumer-visible names (channels, streams, queues, locks) share
|
||||
engine-visible namespaces on Postgres (LISTEN channel names are
|
||||
server-global per database). Exact rules: OQ-04. Decided already: a
|
||||
**reserved/meta namespace exists** for engine-internal names — the
|
||||
Postgres reconnect-wake channel, any internal bookkeeping — and
|
||||
consumer names must not collide with it
|
||||
([ADR-006](decisions/006-wake-and-delivery-contract.md)).
|
||||
server-global per database). Pinned by
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) §4:
|
||||
|
||||
- Reserved prefix **`__alkstore_`**, engine-independent, applies
|
||||
across all name kinds; the one v1-reserved string is
|
||||
`__alkstore_listener_reconnected__` (the Postgres reconnect-wake
|
||||
channel, [ADR-004](decisions/004-postgres-driver.md)).
|
||||
- Reserved-prefix names are rejected at every entry point — the
|
||||
name-bearing methods *and their `*_tx` counterparts* — with the
|
||||
typed `ReservedName` error; empty names with `InvalidName`
|
||||
([ADR-008](decisions/008-contract-v1-pinning.md) §4). Stream-consumer
|
||||
names are consumer-local identifiers, not a reserved-namespace kind.
|
||||
- Engine-derived names in consumer namespaces carry the reserved
|
||||
prefix (the outbox's backing queue is derived under the prefix —
|
||||
honker's `_outbox:{name}` scheme is not inherited verbatim).
|
||||
- SQLite: honker's `_honker_*` internal table family is storage-
|
||||
internal (not consumer namespace); Postgres: queue/stream tables are
|
||||
schema-scoped (layout rides OQ-05) — the *channel* namespace is
|
||||
this section's.
|
||||
|
||||
## Verification backlog
|
||||
|
||||
Contract properties POC-pinned on one engine only (or sketched rather
|
||||
than surface-verified) — the contract test suite must pin both engines
|
||||
before the engine specs are called `stable`:
|
||||
|
||||
- **Lock TTL/expiry re-acquisition on SQLite** — pinned on Postgres
|
||||
(pg POC's lock probe); on SQLite it rests on honker's machinery
|
||||
unverified.
|
||||
- **Wake semantics under `WakeReceiver` shapes** — POCs verified
|
||||
wake delivery/coalescing through their own probe types; the pinned
|
||||
`Wake { channel }` / recv forms (§3 of
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md)) need the contract
|
||||
suite's own property tests on both engines.
|
||||
- **`save_offset_tx` exactly-once-within-a-business-tx shape** — both
|
||||
POCs verified it through their *sketch* implementations (SQLite's
|
||||
offset-save was a plain SQL upsert, not the full honker surface;
|
||||
the pg side likewise through its probe), so the property must be
|
||||
re-pinned against the real engines' `save_offset_tx` in the
|
||||
contract suite.
|
||||
- **Concurrent `try_lock` loser/error behavior** on SQLite (the pg
|
||||
side returns cleanly; honker's busy-path under lock contention is
|
||||
the thing to pin).
|
||||
|
||||
## Design Decisions
|
||||
|
||||
@@ -188,6 +277,7 @@ consumer names must not collide with it
|
||||
| [002](decisions/002-feature-scope.md) | Feature scope | inventory-confirmed features only; cut-flags explicit |
|
||||
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | opaque wake + re-read; notify-vs-streams guarantee split |
|
||||
| [007](decisions/007-transactional-seam.md) | Tx seam | caller-held handle, `*_tx` methods, native commit-atomicity |
|
||||
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 pinning | surface partition, `TxHandle` trait shape, `Wake` type, reserved strings, error taxonomy, config split, locks guarantee row |
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -195,10 +285,10 @@ Open questions are tracked in
|
||||
[open-questions.md](open-questions.md). Key
|
||||
questions affecting this document:
|
||||
|
||||
- **OQ-04**: exact trait-surface pinning — handle shape, error
|
||||
taxonomy, reserved strings, rename/grouping decisions (open,
|
||||
high priority)
|
||||
- **OQ-10**: contract versioning discipline across engine crates (open)
|
||||
- **OQ-08**: capability-surface shape (open)
|
||||
- **OQ-09**: scheduler as first-class mechanism vs queues + schedule
|
||||
(open)
|
||||
- **OQ-09**: scheduler as first-class mechanism vs queues + schedule —
|
||||
also owns the scheduler guarantee row ([open](open-questions.md))
|
||||
- **OQ-10**: contract versioning discipline across engine crates
|
||||
([open](open-questions.md))
|
||||
- **OQ-08**: capability-surface shape ([open](open-questions.md))
|
||||
- **OQ-05**: queue semantics depth — the v1 skeleton's extension path
|
||||
([open](open-questions.md))
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (the contract skeleton the evidence supports); remaining
|
||||
surface pinning is OQ-04's contract work against this ADR
|
||||
Accepted (the contract skeleton the evidence supports); the surface
|
||||
pinning is done — [ADR-008](008-contract-v1-pinning.md) §3/§4 pin the
|
||||
wake type and the reserved strings
|
||||
|
||||
## Context
|
||||
|
||||
@@ -74,9 +75,9 @@ coalesced-but-complete re-read on SQLite).
|
||||
The Postgres forwarder's synthetic reconnect-wake needs a reserved
|
||||
channel namespace no consumer channel may collide with. The naming
|
||||
convention for reserved/meta channels (and any internal queue/stream
|
||||
names) is pinned in the core contract — exact strings are OQ-04
|
||||
contract work; the *existence of a reserved namespace* is decided
|
||||
here.
|
||||
names) is pinned in the core contract — exact strings pinned by
|
||||
[ADR-008](008-contract-v1-pinning.md) §4; the *existence of a reserved
|
||||
namespace* is decided here.
|
||||
|
||||
### The caching-subscriber pattern rides the same contract
|
||||
|
||||
@@ -116,8 +117,8 @@ consumer-inventory-row-gated extension — not assumed now.
|
||||
§"What feeds where"; `docs/research/poc-pg-posture-findings.md`
|
||||
§Sub-module W.
|
||||
- OQ-ST-04 (`docs/research/phase-0.md`) — de-risked by these
|
||||
measurements; the contract remainder is OQ-04 +
|
||||
`core-contract.md`.
|
||||
measurements; the contract remainder resolved by
|
||||
[ADR-008](008-contract-v1-pinning.md) over `core-contract.md`.
|
||||
- [ADR-002](002-feature-scope.md) — notify/listen and streams are
|
||||
first-class; the guarantee split is why both exist.
|
||||
- [ADR-004](004-postgres-driver.md) — the forwarder owning the
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
## Status
|
||||
|
||||
Accepted (the seam shape both POCs verified); handle-type ergonomics
|
||||
(generic vs downcast) is OQ-04 contract work
|
||||
decided by [ADR-008](008-contract-v1-pinning.md) §2 — the `*_tx`
|
||||
methods live on the handle trait, no downcast
|
||||
|
||||
## Context
|
||||
|
||||
@@ -58,7 +59,8 @@ contract-pinning input rather than open risk:
|
||||
|
||||
1. The `as_any_mut` downcast per engine for `*_tx` methods on a
|
||||
`dyn TxHandle` — small, but a generic or enum-favored handle may be
|
||||
cleaner. OQ-04 decides with the full contract.
|
||||
cleaner. Decided by [ADR-008](008-contract-v1-pinning.md) §2
|
||||
(with the full contract).
|
||||
2. Thread-affinity of rusqlite tx ops (SQLite handle ops must
|
||||
round-trip the same blocking thread) — inherent to [ADR-003]'s
|
||||
bridge, documented as a contract note ("SQLite tx ops are
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
# ADR-008: Contract v1 surface pinning
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-10-05, Phase 1 — OQ-04's resolution; binds
|
||||
[core-contract.md](../core-contract.md); engine specs map it)
|
||||
|
||||
## Context
|
||||
|
||||
OQ-04 (promoted from OQ-ST-04) scoped the contract work: with both
|
||||
engines POC-verified ([ADR-003](003-sqlite-driver.md),
|
||||
[ADR-004](004-postgres-driver.md)) and the load-bearing decisions made
|
||||
([ADR-006](006-wake-and-delivery-contract.md) wake contract + guarantee
|
||||
table, [ADR-007](007-transactional-seam.md) caller-held tx seam), what
|
||||
remained was pinning the exact trait surface against the honker-rs
|
||||
starting artifact (`/workspace/honker`
|
||||
`packages/honker-rs/src/lib.rs` v0.5.0, the §Interface finding in
|
||||
phase-0.md). The unpinned items:
|
||||
|
||||
- Which surface parts are contract v1 vs engine-extension.
|
||||
- `TxHandle` representation — the POC sketch used `dyn` + per-engine
|
||||
`as_any_mut` downcast (two recorded frictions,
|
||||
[ADR-007](007-transactional-seam.md)).
|
||||
- The reserved namespace's exact strings (POC used
|
||||
`__listener_reconnected__` ad hoc).
|
||||
- The error taxonomy (e.g. is `PayloadTooLarge` universal when SQLite
|
||||
has no 8000-byte limit?).
|
||||
- What `listen()` returns and where the honker-rs surface gets renamed.
|
||||
- The `Store::open` config split (contract vs engine-crate option).
|
||||
- Delivery-guarantee rows for locks and scheduler (ADR-006's table
|
||||
covered notify/streams/queues only).
|
||||
- A verification backlog for properties POC-pinned on one engine only.
|
||||
|
||||
All of this is paper over a complete evidence base: the honker-rs
|
||||
surface listing, both POC findings (including the trait sketches and
|
||||
their recorded frictions), the consumer inventory rows
|
||||
([ADR-002](002-feature-scope.md)), and the guiding principles
|
||||
(phase-0.md — engine-agnostic consumer API, no engine-type branching).
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Contract v1 partition
|
||||
|
||||
**Contract v1** (the core crate's trait surface, both engines must
|
||||
implement identically):
|
||||
|
||||
- notify / listen — `notify(channel, payload)`; `listen(channel)`.
|
||||
- streams — `publish`, `publish_with_key`, `read_since`,
|
||||
`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)).
|
||||
- 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,
|
||||
priority, max_attempts, expires }` — the honker-rs field set,
|
||||
carried whole (the POC sketch's shape; `run_at` is the absolute-time
|
||||
counterpart of `delay`, not dropped).
|
||||
- locks — `try_lock(name, owner, ttl) -> Option<Lock>` with `renew`
|
||||
and `release`.
|
||||
- outbox helper — `outbox(name)` with `enqueue` + `run_once` delivery
|
||||
worker.
|
||||
- the tx seam — `begin_tx` / commit / rollback and the `*_tx` methods
|
||||
([ADR-007](007-transactional-seam.md)).
|
||||
- `subscribe(consumer) -> Box<dyn EventReceiver>` — the durable
|
||||
stream subscription handle, delivering `StreamEvent`s with an
|
||||
explicit `save_offset` on the receiver (no auto-checkpoint;
|
||||
§8 defines the shape).
|
||||
|
||||
**Not in v1 — deliberately owned elsewhere:**
|
||||
|
||||
- **Queue semantics depth** (retry/backoff curve, dead-letter
|
||||
move-vs-flag, visibility-renewal mechanics, sweep cadence, `QueueOpts`
|
||||
fields beyond the v1 skeleton) — OQ-05's design surface. v1 pins the
|
||||
skeleton above; depth additions extend the contract later.
|
||||
- **Scheduler surface shape** (first-class mechanism vs queues +
|
||||
`schedule()`) — OQ-09 decides; the scheduler surface (if any) is not
|
||||
part of contract v1.
|
||||
- **Capability flags** — OQ-08 decides whether `Store` exposes them at
|
||||
all; v1 has no capability surface.
|
||||
- **`claim_waker`** — dropped from the contract surface; wake-driven
|
||||
claim is the engine's consumption posture
|
||||
([engine-postgres.md](../engine-postgres.md)'s LISTEN-driven claim),
|
||||
engine-internal.
|
||||
- **Rate limits, result storage** — cut-flag rows
|
||||
([ADR-002](002-feature-scope.md)); re-enter only via a
|
||||
consumer-inventory row.
|
||||
- **Notification payloads on wakes** — wake is opaque
|
||||
([ADR-006](006-wake-and-delivery-contract.md)); the payload carrying
|
||||
honker's `Notification` type happens to provide is *not* surfaced in
|
||||
the contract (see §3).
|
||||
|
||||
### 2. `TxHandle` representation: the `*_tx` methods live on the handle trait
|
||||
|
||||
The downcast friction dissolves by moving the ops, not by choosing a
|
||||
handle encoding. The POC sketches put `enqueue_tx` etc. on the
|
||||
engine/store trait taking `&mut dyn TxHandle`, forcing each impl to
|
||||
downcast the handle to its concrete type. The contract instead defines
|
||||
a **`TxHandle` trait carrying the `*_tx` methods directly**:
|
||||
|
||||
```text
|
||||
trait TxHandle {
|
||||
enqueue_tx(name, opts, payload) -> job_id
|
||||
publish_tx(stream, payload) -> event_id
|
||||
notify_tx(channel, payload)
|
||||
save_offset_tx(stream, consumer, offset)
|
||||
commit(self: Box<Self>) -> Result<()> // or rollback
|
||||
}
|
||||
```
|
||||
|
||||
*(Sketches elide `Result<>` wrappers on `*_tx` returns for brevity —
|
||||
every fallible op returns the taxonomy of §5; `notify_tx` can fail
|
||||
with `PayloadTooLarge`/`ReservedName` exactly like its auto-commit
|
||||
counterpart.)*
|
||||
|
||||
- `Store::begin_tx() -> Box<dyn TxHandle + Send>` — callers hold the
|
||||
boxed handle across await points.
|
||||
- Each engine implements `TxHandle` for its own concrete handle —
|
||||
**no downcast, no enum, no generic parameters**. The friction the
|
||||
POCs recorded disappears because there is no engine-store dispatch
|
||||
of a handle argument; the handle *is* the object.
|
||||
- Object safety constraints that follow: no generic methods on
|
||||
`TxHandle`, payload serialization is value-typed in core
|
||||
(`serde_json::Value` — the type both POCs used; not generic over
|
||||
`T: Serialize`); `commit`/`rollback` take `self: Box<Self>`
|
||||
(dispatchable on a boxed trait object).
|
||||
- **Why not the enum** (one variant per engine): the enum's payload
|
||||
types would make core depend on engine types — inverting ADR-001's
|
||||
dependency direction (engines depend on core, never the reverse) —
|
||||
or make the variants engine-opaque, which re-introduces downcast one
|
||||
step further away. A new engine (a stated ADR-001 goal) must not be
|
||||
a core-crate enum edit.
|
||||
- The generic-vs-dyn alternative buys static dispatch for a hot-path
|
||||
cost the POCs measured as negligible against the seam itself (SQLite
|
||||
hop 0.35 ms p50, pg 2.4 ms p50 — both dominated by commit costs);
|
||||
it is the recorded optimization if ever needed (same posture as
|
||||
ADR-003's dedicated-bridge optimization).
|
||||
- Async dispatch over a boxed handle uses the desugared boxed-future
|
||||
form (the family-standard async trait posture); its per-op cost is
|
||||
bounded by the same measurements.
|
||||
- Thread-affinity note retained verbatim: SQLite tx ops serialize
|
||||
through the writer slot ([ADR-007](007-transactional-seam.md)) — a
|
||||
documented engine note, not a contract item.
|
||||
|
||||
### 3. Wake type and the `listen()` return
|
||||
|
||||
`listen(channel)` returns a **wake receiver**, not honker's
|
||||
subscription:
|
||||
|
||||
```text
|
||||
listen(channel) -> Box<dyn WakeReceiver>
|
||||
trait WakeReceiver {
|
||||
recv() -> Option<Wake> // None = source closed (watcher death)
|
||||
try_recv() -> Result<Option<Wake>>
|
||||
recv_timeout(d) -> Result<Option<Wake>>
|
||||
}
|
||||
|
||||
struct Wake { channel: String }
|
||||
```
|
||||
|
||||
- **`Wake` carries only the channel name** — the one piece of semantic
|
||||
content [ADR-006](006-wake-and-delivery-contract.md) allows. Honker's
|
||||
`Notification { id, channel, payload }` is not surfaced: the pg
|
||||
engine would have to discard payloads to honor the opaque contract,
|
||||
and honoring the contract uniformly is what makes consumer code
|
||||
engine-agnostic. Consumers needing content use streams (the
|
||||
guarantee split, ADR-006). The SQLite engine's payload transport is
|
||||
a non-contract implementation detail.
|
||||
- Receiver close is the failure surface: SQLite watcher death closes
|
||||
the receiver (`recv() -> None`,
|
||||
[ADR-006](006-wake-and-delivery-contract.md)); the Postgres
|
||||
reconnect-wake arrives as `Wake { channel:
|
||||
Reserved::LISTENER_RECONNECTED }` on every subscriber's receiver
|
||||
(a broadcast-to-all-subscribers fanout — engine behavior recorded in
|
||||
[engine-postgres.md](../engine-postgres.md)).
|
||||
- `WakeReceiver` methods have no wake-count or id: wakes are hints,
|
||||
their multiplicity is not part of the contract (coalescing on
|
||||
SQLite, per-notify on Postgres — ADR-006). Close semantics across
|
||||
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.
|
||||
|
||||
### 4. Reserved namespace — exact strings
|
||||
|
||||
- **Reserved prefix: `__alkstore_`** (leading double underscore). The
|
||||
namespace exists per [ADR-006](006-wake-and-delivery-contract.md);
|
||||
this pins its string. Engine-independent — applies to channel,
|
||||
stream, queue, and lock names on every engine.
|
||||
- **The one v1-reserved string:**
|
||||
`__alkstore_listener_reconnected__` — the Postgres forwarder's
|
||||
synthetic reconnect-wake channel ([ADR-004](004-postgres-driver.md)).
|
||||
The POC's ad-hoc `__listener_reconnected__` string is renamed into
|
||||
the namespace at implementation; nothing (consumer or contract text)
|
||||
depended on the ad-hoc string.
|
||||
- Consumer names with the reserved prefix are **rejected at the entry
|
||||
points** — the name-bearing surface methods (`notify`, `listen`,
|
||||
`stream`, `queue`, `outbox`, `try_lock`) *and their `*_tx`
|
||||
counterparts* (`notify_tx` at minimum; any `*_tx` op taking a name) —
|
||||
with the typed `ReservedName` error (taxonomy, §5). Engine-side
|
||||
validation obligation: reject before any engine round trip, on both
|
||||
the auto-commit and tx paths (validation must not be bypassable by
|
||||
the commit-atomic path). Stream-consumer names (`save_offset(_tx)`
|
||||
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).
|
||||
- Per-engine internal names:
|
||||
- SQLite: honker's machinery owns two categories of internal names,
|
||||
and the contract treats them differently. Its `_honker_*` *table*
|
||||
family (`_honker_dead`, `_honker_locks`, …) is storage-internal —
|
||||
not part of any consumer namespace; its fate rides the quality
|
||||
read (OQ-06, [ADR-005](005-dependency-ownership.md)). Its
|
||||
**consumer-namespace derived names** are a real collision surface:
|
||||
honker-rs materializes an outbox's backing queue as
|
||||
`_outbox:{name}` inside the queue-name namespace — a consumer
|
||||
calling `queue("_outbox:foo")` directly could collide with
|
||||
`outbox("foo")`. The contract's rule: **engine-derived names in
|
||||
consumer namespaces carry the reserved prefix** — the SQLite
|
||||
engine's outbox backing queue is `__alkstore_outbox:{name}`,
|
||||
derived from the consumer's outbox name, and every entry point
|
||||
rejects the reserved prefix for directly-supplied names (so a
|
||||
consumer can never create or collide with it). Honker's
|
||||
`_outbox:` scheme is not inherited verbatim.
|
||||
- Postgres: queue/stream tables are schema-scoped to avoid
|
||||
colliding with consumer tables (the co-tenancy precedent; layout
|
||||
decision rides OQ-05's namespace bullet,
|
||||
[queues.md](../queues.md)). The reserved *channel* namespace is
|
||||
this section's; the outbox backing queue obeys the same
|
||||
reserved-prefix rule as SQLite's.
|
||||
- Channel-name charset: the contract requires non-empty names and the
|
||||
reserved-prefix rejection; identifier quoting for Postgres LISTEN /
|
||||
`pg_notify` is the engine's obligation (POC #2 owned the quoting
|
||||
pitfall — test-pinned).
|
||||
|
||||
### 5. Error taxonomy
|
||||
|
||||
One top-level `Error` in the core crate, `thiserror`-typed (family
|
||||
standard). **Pinning rule: an error gets its own matchable variant
|
||||
only when the caller can act differently on it** — engine-specific
|
||||
conditions that a caller cannot act on stay inside the opaque
|
||||
fallback with their detail in the source chain. This is the line that
|
||||
keeps the taxonomy small and keeps it from inheriting honker's
|
||||
per-binding sprawl.
|
||||
|
||||
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
|
||||
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
|
||||
large-payload alternative (a table row with the id in the
|
||||
notification — the outbox shape) is the documented workaround.
|
||||
- `ReservedName { name }` — rejected reserved-prefix names (§4).
|
||||
- `InvalidName { name }` — empty names and other entry-point
|
||||
validation failures (§4).
|
||||
- `Closed` — an operation against a closed receiver/source (also the
|
||||
`try_recv` / `recv_timeout` form of the wake close in §3).
|
||||
- `Codec` — payload serialization/deserialization failures
|
||||
(`payload_as` on stream events and job payloads; wakes carry no
|
||||
payload per §3).
|
||||
- `Database` — everything else: driver/connection/SQL errors, opaque
|
||||
to the contract, engine detail preserved via the error source chain.
|
||||
No engine-specific variants are minted for its contents.
|
||||
|
||||
Explicitly *not* taxonomy items (callers act via value results, not
|
||||
errors): queue claim returning none (no work); job-handle ops
|
||||
(`ack`/`retry`/`fail`/`heartbeat`) returning "did the op land" booleans
|
||||
(`false` = the job was no longer claimable/owned — expired, acked
|
||||
elsewhere, cancelled); `try_lock` returning `Option<Lock>`; lock
|
||||
`release`'s boolean.
|
||||
|
||||
### 6. `Store::open` config shape — engine options, not contract surface
|
||||
|
||||
- The core crate defines the `Store` trait (the §1 surface plus
|
||||
`begin_tx`). **Constructors and their option structs live in the
|
||||
engine crates** (`alkstore_sqlite::open(path, SqliteOpts)` /
|
||||
`alkstore_postgres::open(url, PgOpts)`): durability knobs, pool
|
||||
sizing, pragmas, watcher cadence, listener posture are
|
||||
engine-configuration concerns —
|
||||
[deployment.md](../deployment.md) carries the knob
|
||||
facts, engine-crate docs carry the options.
|
||||
- What is contract: the trait the constructor returns, and the
|
||||
constructor's own obligation — a `Store` handed out by an engine
|
||||
crate honors the full v1 surface.
|
||||
- No capability surface in v1 (OQ-08 owns whether one ever exists).
|
||||
- Consumer code stays engine-agnostic by depending on core and being
|
||||
constructed by exactly one engine crate (ADR-001's single-driver
|
||||
binaries) — the engine choice is a dependency-graph fact, not a
|
||||
runtime branch.
|
||||
|
||||
### 7. Delivery-guarantee rows — locks pinned, scheduler owned by OQ-09
|
||||
|
||||
[ADR-006](006-wake-and-delivery-contract.md)'s table is extended by
|
||||
this decision:
|
||||
|
||||
| Mechanism | Durability | Replay | Atomicity | Guarantee |
|
||||
|---|---|---|---|---|
|
||||
| **locks** | row-backed (machinery-defined) | n/a | acquire/release atomic | **mutual exclusion bounded by TTL + renewal discipline**: a lock excludes other holders while held (explicit release or within TTL); after TTL expiry exclusion lapses *silently* — no revocation event, exclusivity resumes with re-acquisition; a holder renewing inside its TTL keeps exclusion |
|
||||
|
||||
- The consumer obligation this row carries: long held-lock work must
|
||||
`renew` within TTL; expiry is not an error but a *loss of exclusivity*
|
||||
(the honest TTL semantics the alkblobs fleet-sweeper ADR already
|
||||
treats as ground).
|
||||
- The **scheduler guarantee row is not pinned here** — it is defined
|
||||
by OQ-09's collapse decision (under collapse, the scheduler inherits
|
||||
the queues row; the scheduler-tick/leader-election guarantees become
|
||||
part of OQ-09's and OQ-05's resolution text). This is an explicit
|
||||
transfer to OQ-09's scope, not an open residue of this ADR.
|
||||
|
||||
### 8. Renames and grouping deltas from the honker-rs surface
|
||||
|
||||
Pinned deltas (the starting artifact's names, where the contract
|
||||
differs):
|
||||
|
||||
| honker-rs | contract v1 | Reason |
|
||||
|---|---|---|
|
||||
| `Subscription` (listen) | `WakeReceiver` / `Wake` | wake is opaque — no payload, no id; name must not suggest subscription semantics (no replay, ADR-006) |
|
||||
| `Notification { id, channel, payload }` | `Wake { channel }` | §3; payload is non-contract |
|
||||
| `Lock::heartbeat(ttl)` | `Lock::renew(ttl)` | renew describes the TTL discipline the guarantee row pins; heartbeat is the queue-side renewal term |
|
||||
| `Queue::claim_waker()` | (not surfaced) | engine-internal consumption wake (§1) |
|
||||
| `StreamSubscription` (`save_every`, auto-save on drop) | `subscribe(consumer) -> Box<dyn EventReceiver>`, explicit saves only | the explicit-offset obligation (core-contract.md streams; the auto-checkpoint ambiguity is not inherited, [ADR-006](006-wake-and-delivery-contract.md)) |
|
||||
| `update_events()` | (not surfaced) | superseded by the wake subscription — the pinned `WakeReceiver` *is* the reactive-event surface; honker's separate raw update-event stream has no contract role |
|
||||
| `prune_notifications` / `prune_notifications_keep_latest` | (not surfaced) | notifications-table maintenance tooling on the SQLite engine; disposition rides OQ-05's sweep/maintenance design (consumer-visible only if that design surfaces it) |
|
||||
| `Scheduler` surface | (out of v1, OQ-09 decides) | §1 |
|
||||
| `try_rate_limit` / `save_result` / `get_result` / `sweep_results` | (cut-flag rows) | [ADR-002](002-feature-scope.md) |
|
||||
|
||||
Grouping stays honker-shaped otherwise: mechanism handles off the
|
||||
store (`stream(name)`, `queue(name, opts)`, `outbox(name)`), the tx
|
||||
handle off `begin_tx`, payloads serialized through core value types
|
||||
(non-generic trait methods, §2), `payload_as<T>` as the decode
|
||||
convenience on concrete returned values (`Job`, `StreamEvent`) — the
|
||||
deserialization error is `Codec`, not a per-type variant.
|
||||
|
||||
The stream subscription handle's shape (defined here, since §1 pins
|
||||
the return type):
|
||||
|
||||
```text
|
||||
trait EventReceiver {
|
||||
recv() -> Option<Result<StreamEvent>> // None = stream closed
|
||||
try_recv() -> Result<Option<StreamEvent>>
|
||||
read_since(offset, limit) -> Result<Vec<StreamEvent>>
|
||||
save_offset(&mut self) -> Result<()> // explicit only — the
|
||||
// offset advances per read;
|
||||
// the save is the consumer's
|
||||
// checkpoint call
|
||||
offset() -> i64 // the receiver's current offset
|
||||
}
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive**
|
||||
|
||||
- The contract v1 is a complete, implementable surface: every open
|
||||
shape question from the OQ-04 list is pinned; no placeholder
|
||||
semantics remain in [core-contract.md](../core-contract.md).
|
||||
- The tx-seam downcast friction is *dissolved*, not tolerated — the
|
||||
trait shape makes each engine's handle self-contained, and a future
|
||||
engine is purely additive (implement the traits; no core edits).
|
||||
- The taxonomy rule (variants only where callers act) is a durable
|
||||
principle for later surface additions, keeping queue depth (OQ-05)
|
||||
from accreting error variants.
|
||||
- The reserved namespace is exact (`__alkstore_`), testable, and
|
||||
engine-independent.
|
||||
|
||||
**Negative**
|
||||
|
||||
- Dyn dispatch on `begin_tx`/mechanism returns means boxed handles and
|
||||
boxed futures per op — bounded by POC measurement as negligible
|
||||
against commit costs, but real; monomorphized fast paths are the
|
||||
recorded optimization if profiles ever demand them.
|
||||
- Wake payloads are dropped from what the SQLite engine *could* carry
|
||||
through — engine-side, honker's transport capability is unused by
|
||||
the contract surface (the cost of an honest uniform contract).
|
||||
- `commit(self: Box<Self>)` shape means a consumed handle can't be
|
||||
reused after commit (callers re-`begin_tx`) — matches the
|
||||
caller-owned lifetime the POCs verified, but is stricter than
|
||||
honker's re-usable `&Transaction` style.
|
||||
- OQ-05/OQ-09/OQ-08 remain open: this ADR deliberately does not pin
|
||||
queue depth, the scheduler shape, or capability flags; the v1
|
||||
additions to those surfaces will be later contract extensions.
|
||||
|
||||
## References
|
||||
|
||||
- OQ-04 (`docs/architecture/open-questions.md`) — this ADR's
|
||||
resolution.
|
||||
- The honker-rs surface (`/workspace/honker`
|
||||
`packages/honker-rs/src/lib.rs` v0.5.0) — the starting artifact;
|
||||
`/workspace/honker` is a reference checkout
|
||||
([ADR-005](005-dependency-ownership.md) posture).
|
||||
- `docs/research/poc-sqlite-posture-findings.md` §Probe 1 (trait
|
||||
friction), §"What feeds where"; `docs/research/poc-pg-posture-findings.md`
|
||||
§Sub-module T (tx-seam shapes), §Sub-module W (wake/reconnect),
|
||||
§Payload boundary (`PayloadTooLarge`).
|
||||
- [core-contract.md](../core-contract.md) — the spec this ADR's
|
||||
§1–§5 pin into place.
|
||||
- [ADR-002](002-feature-scope.md) (scope),
|
||||
[ADR-006](006-wake-and-delivery-contract.md) (wake contract,
|
||||
namespace existence, guarantee table),
|
||||
[ADR-007](007-transactional-seam.md) (tx seam mechanics).
|
||||
- OQ-09 (scheduler row transfer), OQ-08 (capability surface), OQ-10
|
||||
(versioning discipline for future contract extensions).
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-04
|
||||
last_updated: 2026-10-05
|
||||
---
|
||||
|
||||
# Deployment
|
||||
@@ -68,10 +68,11 @@ namespace bullet).
|
||||
| SQLite | `synchronous` | WAL + `NORMAL` shipped ([ADR-003](decisions/003-sqlite-driver.md)); FULL is available consumer-side for stricter durability; commit fsyncs land at WAL checkpoints (the ~1000-commit spike cadence, POC #1) |
|
||||
| Postgres | `synchronous_commit` | per-session knob; `on` is ship config (p50 2.40 ms seam); `off` trades max-tail (40.9 ms) for slightly better p50 — measured, honest trade ([ADR-004](decisions/004-postgres-driver.md)); session-level SET mechanics POC-verified |
|
||||
|
||||
These are engine-configuration concerns, *not* trait surface (a
|
||||
consumer may set them via their own engine config — what part of
|
||||
engine config is contract-level `Store::open` shape vs engine-crate
|
||||
docs is OQ-04's config-shape item).
|
||||
These are engine-configuration concerns, *not* trait surface. What
|
||||
part of engine config is contract-level shape vs engine-crate docs is
|
||||
decided: constructors and option structs live in the engine crates;
|
||||
the contract is the trait the constructor returns
|
||||
([ADR-008](decisions/008-contract-v1-pinning.md) §6).
|
||||
|
||||
## Toolchain / platform notes
|
||||
|
||||
@@ -105,6 +106,7 @@ From both POCs (single-box, relative shapes are the deliverable —
|
||||
| [003](decisions/003-sqlite-driver.md) | SQLite driver | bundling, toolchain floor |
|
||||
| [004](decisions/004-postgres-driver.md) | Postgres driver | listener budget line, forwarder posture |
|
||||
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | where capability differences may surface |
|
||||
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 | constructor/options in engine crates; no capability surface in v1 (OQ-08) |
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -113,6 +115,4 @@ Open questions are tracked in
|
||||
questions affecting this document:
|
||||
|
||||
- **OQ-08**: capability-surface shape — compile-time vs runtime flags
|
||||
vs matrix-only (open)
|
||||
- **OQ-04**: engine-config surface in the contract's `Store::open`
|
||||
shape (shared) (open)
|
||||
vs matrix-only (open)
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-04
|
||||
last_updated: 2026-10-05
|
||||
---
|
||||
|
||||
# Postgres engine
|
||||
@@ -47,6 +47,10 @@ obligations live in the core spec.
|
||||
silent), re-LISTEN from the channel list after every reconnect
|
||||
(exponential backoff 50 ms → 2 s cap), and the synthetic
|
||||
reconnect-wake on the reserved channel
|
||||
(`__alkstore_listener_reconnected__`,
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) §4) — broadcast to
|
||||
*every* subscriber's receiver, per the wake contract's
|
||||
reconnect-recovery semantics
|
||||
([ADR-006](decisions/006-wake-and-delivery-contract.md)).
|
||||
- One listener serves N channels and N subscribers; re-attach is a
|
||||
broadcast re-subscribe (no server round-trips); per-channel
|
||||
@@ -98,6 +102,7 @@ the record in [ADR-003](decisions/003-sqlite-driver.md).
|
||||
| [005](decisions/005-dependency-ownership.md) | Ownership | published libs as-is; `postgres-notify` derive-not-adopt |
|
||||
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | LISTEN push, no replay, synthetic reconnect-wake |
|
||||
| [007](decisions/007-transactional-seam.md) | Tx seam | direct pooled-object handle, no bridging |
|
||||
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 | pinned surface; reserved reconnect-wake channel string; `PayloadTooLarge` taxonomy variant |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-04
|
||||
last_updated: 2026-10-05
|
||||
---
|
||||
|
||||
# SQLite engine
|
||||
@@ -101,6 +101,7 @@ consumption stands.
|
||||
| [005](decisions/005-dependency-ownership.md) | Ownership | published honker-core; named fork triggers |
|
||||
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | data_version watcher, coalescing, death-closes-receivers |
|
||||
| [007](decisions/007-transactional-seam.md) | Tx seam | writer-slot lease, `BEGIN IMMEDIATE`, `spawn_blocking` round-trips |
|
||||
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 | pinned surface; outbox backing queue derived under the reserved prefix; wake payload transport unused (non-contract) |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-04
|
||||
last_updated: 2026-10-05
|
||||
---
|
||||
|
||||
# alkstore — Open Questions
|
||||
@@ -11,10 +11,12 @@ The Phase 0 register's questions (`OQ-ST-01..08` in
|
||||
mirror OQ-ST-01..08 one-to-one**, keeping their Phase 0 statuses
|
||||
(a resolved register question stays listed here as resolved, with the
|
||||
ADR that carries its decision). New Phase 1 questions append from
|
||||
OQ-09. Suggested resolution order: **OQ-04 first** (the contract
|
||||
surface everything else hangs off), then OQ-09/OQ-10 (scoped to
|
||||
OQ-04's outcome), then OQ-05/OQ-08 (OQ-05 has an independent design
|
||||
track; OQ-08 depends only on the trait's shape, OQ-04).
|
||||
OQ-09. Resolution order so far: **OQ-04 resolved** (2026-10-05,
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) — the contract surface
|
||||
everything else hangs off). Next: OQ-05/OQ-09 (the queue semantics
|
||||
track; OQ-09 also owns the scheduler guarantee row), OQ-06
|
||||
(the SQLite dependency gate), OQ-08 (rides the now-pinned trait
|
||||
shape), OQ-10 (versioning discipline for contract extensions).
|
||||
|
||||
Resolved questions stay listed with their resolution; they are not
|
||||
deleted.
|
||||
@@ -78,7 +80,7 @@ deleted.
|
||||
|
||||
## Theme: Core contract
|
||||
|
||||
### OQ-04: Contract pinning — the exact trait surface and its semantics *(== OQ-ST-04)*
|
||||
### OQ-04: Contract pinning — the exact trait surface and its semantics *(== OQ-ST-04)* — **RESOLVED**
|
||||
|
||||
*(Retitled in promotion: OQ-ST-04's framing — "the reactive
|
||||
abstraction — what does the unified notify surface look like?" —
|
||||
@@ -87,38 +89,30 @@ narrowed to the pinning work its own record already scoped.)*
|
||||
- **Origin**: [core-contract.md](core-contract.md),
|
||||
[ADR-006](decisions/006-wake-and-delivery-contract.md),
|
||||
[ADR-007](decisions/007-transactional-seam.md)
|
||||
- **Status**: open (de-risked; both engine sides POC-verified — this is
|
||||
paper work over a complete evidence base)
|
||||
- **Status**: resolved (2026-10-05, Phase 1 — [ADR-008](decisions/008-contract-v1-pinning.md))
|
||||
- **Priority**: high
|
||||
- **Resolution**: open. The decisions that bound it are made
|
||||
([ADR-006](decisions/006-wake-and-delivery-contract.md): opaque-wake + re-read, the delivery-guarantee table, the
|
||||
reserved namespace's existence; [ADR-007](decisions/007-transactional-seam.md): caller-held `TxHandle`
|
||||
with `*_tx` methods). What remains to pin, per the honker-rs surface
|
||||
as starting artifact against the inventory rows:
|
||||
- Which surface parts are contract v1 and which are engine-extension
|
||||
(queue depth, scheduler surface, lock semantics details).
|
||||
- `TxHandle` representation: `dyn` + per-engine downcast (POC
|
||||
sketch, works, two recorded frictions) vs generic/enum handle.
|
||||
- The reserved/meta namespace: exact strings (Postgres
|
||||
reconnect-wake channel like `__listener_reconnected__`), and
|
||||
whether SQLite-side internal names carry an equivalent prefix.
|
||||
- Error taxonomy: what callers can match on per mechanism (e.g.
|
||||
`PayloadTooLarge` is POC-verified pg-side; SQLite's notify has no
|
||||
8000-byte limit — is the error universal?).
|
||||
- Naming/grouping of the honker-rs surface where renames clarify
|
||||
(e.g. what `listen()` returns — a wake receiver? a subscription
|
||||
handle?).
|
||||
- The `Store::open` config shape — what engine-configuration
|
||||
(durability knobs, pool sizing, listener posture) is consumer
|
||||
surface vs engine crate docs.
|
||||
- Verification backlog for properties the POCs pinned on one engine
|
||||
only — e.g. lock TTL/expiry re-acquisition is POC-pinned on
|
||||
Postgres but rests on honker's machinery on SQLite; the contract
|
||||
test suite must pin the SQLite side too.
|
||||
- Delivery-guarantee rows for locks and scheduler (the guarantee
|
||||
table in [ADR-006](decisions/006-wake-and-delivery-contract.md) covers notify/streams/queues; lock-vs-TTL-race
|
||||
and scheduler-tick guarantees need either rows there or an
|
||||
explicit "scheduler is queues" absorption via OQ-09).
|
||||
- **Resolution**: Pinned as contract v1 by
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md): (1) the v1
|
||||
surface partition — notify/streams/queue-skeleton/locks/outbox/tx
|
||||
seam are contract; queue depth rides OQ-05, scheduler shape rides
|
||||
OQ-09, capabilities ride OQ-08, `claim_waker` stays engine-internal,
|
||||
cut-flag rows stay out. (2) `TxHandle`: the `*_tx` methods live on
|
||||
the handle trait — boxed `dyn` handle, no downcast, no enum (the
|
||||
enum would invert ADR-001's dependency direction); the POCs'
|
||||
`as_any_mut` friction dissolves by construction. (3) `listen()`
|
||||
returns a `WakeReceiver` delivering opaque `Wake { channel }` —
|
||||
honker's payload transport is not surfaced (uniformity with the
|
||||
opaque contract, ADR-006). (4) Reserved prefix `__alkstore_`, with
|
||||
`__alkstore_listener_reconnected__` as the one v1-reserved string
|
||||
(the POC's ad-hoc `__listener_reconnected__` renamed at
|
||||
implementation). (5) Error taxonomy: `PayloadTooLarge` /
|
||||
`ReservedName` / `InvalidName` / `Closed` / `Codec` / `Database`,
|
||||
pinned by the act-differently rule. (6) `Store::open` is engine-crate
|
||||
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
|
||||
backlog) tracks the one-engine-pinned properties.
|
||||
- **Cross-references**: OQ-01, OQ-10, OQ-09, OQ-08.
|
||||
|
||||
## Theme: Queues and scheduling
|
||||
@@ -155,7 +149,11 @@ narrowed to the pinning work its own record already scoped.)*
|
||||
inspectable/pausable schedule objects (`add/pause/resume/update/
|
||||
list/remove`), it stays a first-class mechanism (the full honker
|
||||
shape). Decided against the inventory rows, not the whole honker
|
||||
menu.
|
||||
menu. **Also owns the delivery-guarantee table's scheduler row**
|
||||
(transferred by [ADR-008](decisions/008-contract-v1-pinning.md) §7:
|
||||
under collapse the scheduler inherits the queues row; otherwise a
|
||||
dedicated row is pinned with the surface decision). The scheduler
|
||||
surface is *not* part of contract v1 ([ADR-008](decisions/008-contract-v1-pinning.md) §1).
|
||||
- **Cross-references**: OQ-05, OQ-04.
|
||||
|
||||
## Theme: Engines and dependencies
|
||||
@@ -190,7 +188,8 @@ narrowed to the pinning work its own record already scoped.)*
|
||||
honker's honesty posture, inherited). The unified surface must not
|
||||
pretend SQLite is multi-host. Options: per-engine capability flags
|
||||
(`Store::capabilities()`), a documented deployment matrix only
|
||||
([deployment.md] carries the facts), or compile-time knowledge only
|
||||
([deployment.md](deployment.md) carries the facts), or compile-time
|
||||
knowledge only
|
||||
(a consumer choosing the SQLite engine knows). Rides OQ-04: the
|
||||
trait's shape constrains where capability differences can surface.
|
||||
- **Cross-references**: OQ-04, [ADR-006](decisions/006-wake-and-delivery-contract.md).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-04
|
||||
last_updated: 2026-10-05
|
||||
---
|
||||
|
||||
# alkstore — Overview
|
||||
@@ -28,7 +28,7 @@ Per [ADR-001](decisions/001-crate-split.md):
|
||||
|
||||
| Crate | Contents | Driver dependencies |
|
||||
|---|---|---|
|
||||
| `alkstore` (core) | trait surface, types, error model, capability flags | none |
|
||||
| `alkstore` (core) | trait surface, types, error model | none (capability flags, if ever, are OQ-08's to add) |
|
||||
| `alkstore-sqlite` | SQLite engine ([ADR-003](decisions/003-sqlite-driver.md)) | rusqlite, honker-core |
|
||||
| `alkstore-postgres` | Postgres engine ([ADR-004](decisions/004-postgres-driver.md)) | tokio-postgres, deadpool-postgres |
|
||||
| (mem engine, optional) | test convenience, decided at implementation ([ADR-001](decisions/001-crate-split.md)) | none |
|
||||
@@ -71,6 +71,7 @@ Per [ADR-002](decisions/002-feature-scope.md):
|
||||
| [005](decisions/005-dependency-ownership.md) | Published libraries by default, named fork triggers | Accepted |
|
||||
| [006](decisions/006-wake-and-delivery-contract.md) | Opaque wake + re-read; notify-vs-streams guarantee split | Accepted |
|
||||
| [007](decisions/007-transactional-seam.md) | Caller-held `TxHandle` with `*_tx` methods | Accepted |
|
||||
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 surface pinning (partition, TxHandle shape, wake type, reserved strings, error taxonomy) | Accepted |
|
||||
|
||||
## Non-goals
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-04
|
||||
last_updated: 2026-10-05
|
||||
---
|
||||
|
||||
# Queues, scheduler, outbox — semantics depth
|
||||
@@ -30,9 +30,11 @@ when made.
|
||||
- **Wake-driven consumption**: the pg engine's default is LISTEN-driven
|
||||
claim with a re-poll safety net; SQLite's is watcher wake with
|
||||
per-subscription fanout ([ADR-006](decisions/006-wake-and-delivery-contract.md)).
|
||||
- **Job options** (the honker-rs surface, the contract starting
|
||||
artifact): `delay, priority, max_attempts, expires` at enqueue;
|
||||
`ack / retry / fail / heartbeat` on the job handle.
|
||||
- **Job options** (the contract v1 skeleton,
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) §1, from the
|
||||
honker-rs surface): `delay, priority, max_attempts, expires` at
|
||||
enqueue; `ack / retry / fail / heartbeat` on the job handle. Deeper
|
||||
`QueueOpts`/retry surface is this document's design space.
|
||||
- **Cut-flag context**: result storage is
|
||||
[ADR-002](decisions/002-feature-scope.md)'s cut-flag row; if
|
||||
OQ-05's design shows queue consumers provably need
|
||||
@@ -95,11 +97,13 @@ when made.
|
||||
|
||||
- Schema-scoped DDL (the pg-boss design) vs shared-schema table
|
||||
naming. Rides the reserved-namespace contract
|
||||
([ADR-006](decisions/006-wake-and-delivery-contract.md), OQ-04):
|
||||
queue/stream/lock tables and the reserved channel prefix
|
||||
must be collision-proof against consumer tables in the same
|
||||
database (the alkblobs co-tenant-tables precedent — ADR-008 in
|
||||
`/workspace/@alkdev/alkblobs/docs/architecture/decisions/`).
|
||||
([ADR-006](decisions/006-wake-and-delivery-contract.md),
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) §4):
|
||||
the reserved *channel* namespace is pinned (`__alkstore_` prefix);
|
||||
queue/stream/lock **table** layout remains this document's
|
||||
OQ-05 work and must be collision-proof against consumer tables in
|
||||
the same database (alkblobs' ADR-008 — co-tenant-tables precedent —
|
||||
at `/workspace/@alkdev/alkblobs/docs/architecture/decisions/`).
|
||||
|
||||
## Reference material
|
||||
|
||||
@@ -125,6 +129,7 @@ when made.
|
||||
| [005](decisions/005-dependency-ownership.md) | Ownership | design-reference postures for the queue family |
|
||||
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | wake-driven consumption, guarantee table |
|
||||
| [007](decisions/007-transactional-seam.md) | Tx seam | enqueue_tx commit-atomicity |
|
||||
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 | queue skeleton + EnqueueOpts pinned; depth is OQ-05's extension path |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
Reference in new issue
Block a user