182 lines
8.9 KiB
Markdown
182 lines
8.9 KiB
Markdown
---
|
||
id: core-trait-surface
|
||
name: Core trait surface (Store, TxHandle, mechanism handles, receivers)
|
||
status: completed
|
||
depends_on: [core-value-types]
|
||
scope: moderate
|
||
risk: medium
|
||
impact: project
|
||
level: implementation
|
||
tags: [wave-1, core]
|
||
---
|
||
|
||
## Description
|
||
|
||
Implement the core crate's full trait surface — contract v1 in code.
|
||
Every shape is pinned by ADR text; this task is faithful transcription,
|
||
and it is the surface both engines implement in waves 3–4, so shape
|
||
errors here are the expensive kind. The wave-1 review gate checks this
|
||
task line-by-line against the ADRs.
|
||
|
||
**`Store` trait** (ADR-008 §1/§6, ADR-019 §1, ADR-009 §1):
|
||
|
||
```text
|
||
begin_tx() -> Box<dyn TxHandle + Send>
|
||
notify(channel, payload)
|
||
listen(channel) -> Box<dyn WakeReceiver>
|
||
stream(name) -> Box<dyn StreamHandle>
|
||
queue(name, opts) -> Box<dyn Queue>
|
||
outbox(name) -> Box<dyn Outbox>
|
||
try_lock(name, owner, ttl) -> Option<Box<dyn Lock>>
|
||
schedule(name, spec, queue, payload, opts) -> Result<Schedule>
|
||
unschedule(name) -> bool
|
||
run_schedules(stop: StopToken) -> Result<()>
|
||
```
|
||
|
||
Constructors/options live in engine crates (ADR-008 §6) — core defines
|
||
only the trait. Name-bearing methods validate at the entry point
|
||
(shared helper from `core-errors-and-validation`); the trait's doc
|
||
contract states that obligation (engines must reject before any round
|
||
trip, on auto-commit and tx paths alike).
|
||
|
||
**`TxHandle` trait** (ADR-008 §2, ADR-014, ADR-015 §2, ADR-021 §1/§4):
|
||
`enqueue_tx`, `publish_tx`, `publish_with_key_tx`, `notify_tx`,
|
||
`save_offset_tx`, `get_job_tx`, `get_offset_tx`, `read_since_tx`,
|
||
`read_from_consumer_tx`, `outbox_enqueue_tx`, `commit(self:
|
||
Box<Self>) -> Result<()>`. Drop = rollback is an engine obligation
|
||
stated in the trait docs. No generic methods (object safety); payloads
|
||
cross as `serde_json::Value`; async methods use the boxed-future form
|
||
(family-standard async trait posture).
|
||
|
||
**Mechanism handles** (ADR-019 §1, exact method sets): `Queue { name,
|
||
enqueue, claim_one, claim_batch, ack_batch, cancel, get_job,
|
||
sweep_expired }`; `StreamHandle { name, publish, publish_with_key,
|
||
read_since, read_from_consumer, save_offset, get_offset, trim_to,
|
||
subscribe }`; `Outbox { name, enqueue, run_once }`; `Lock { name,
|
||
renew, release(self: Box<Self>) }`; `JobHandle { job(&self) -> &Job,
|
||
ack(self), retry(self, err, delay) -> bool, fail(self, err) -> bool,
|
||
heartbeat(self, extend) -> bool }` — the one-shot/repeatable split is
|
||
deliberate (heartbeat takes `&self`).
|
||
|
||
**Receivers** (ADR-008 §3/§8): `WakeReceiver { recv() ->
|
||
Option<Wake>, try_recv() -> Result<Option<Wake>>, recv_timeout(d) ->
|
||
Result<Option<Wake>> }` (timeout expiry = `Ok(None)`; `Err` carries
|
||
`Database`/`Closed` only); `EventReceiver { recv() ->
|
||
Option<Result<StreamEvent>>, try_recv() -> Result<Option<StreamEvent>>,
|
||
read_since(offset, limit) -> Result<Vec<StreamEvent>>,
|
||
save_offset(&mut self) -> Result<()>, offset() -> i64 }` — no
|
||
`recv_timeout` on `EventReceiver` (deliberate absence, ADR-008 §8
|
||
third-round annotation).
|
||
|
||
**`run_once` delivery callable** (ADR-019 §5): consumer-supplied async
|
||
callable taking `Box<dyn JobHandle>`, returning the boxed future of
|
||
`Result<()>`; object-safe encoding (named one-method trait object or
|
||
`&mut dyn FnMut` form — implementer's choice under the pinned
|
||
semantics).
|
||
|
||
`with_tx` (ADR-007, signature pinned 2026-10-07): `with_tx(f) ->
|
||
Result<()>` with `f: FnOnce(&mut dyn TxHandle) -> BoxedFuture<Result<()>>`
|
||
— `Ok` ⇒ commit, `Err`/drop/panic ⇒ rollback; non-generic, values
|
||
return through captured state, no handle escapes the closure. This
|
||
lives on the `Store` trait as a provided method (default body over
|
||
`begin_tx`).
|
||
|
||
All traits re-exported from `src/lib.rs`. Doc comments carry the
|
||
pinned semantics (guarantee-row references, validity predicate,
|
||
drop=rollback, no-claim_tx absence rationale).
|
||
|
||
## Acceptance Criteria
|
||
|
||
- [x] Every trait above with the exact pinned method sets and
|
||
signatures; compiles as object-safe (`Box<dyn ...>` everywhere
|
||
the contract says)
|
||
- [x] `with_tx` provided method present with the pinned signature
|
||
- [x] A compile-probe test constructs `Box<dyn TxHandle>`,
|
||
`Box<dyn Queue>`, etc. from a trivial local impl, proving object
|
||
safety of the whole surface
|
||
- [x] Trait docs state: entry-point validation obligation, drop =
|
||
rollback, the uniform handle-op validity predicate reference,
|
||
heartbeat's absolute-reset semantics, no `claim_tx` rationale
|
||
- [x] `cargo test -p alkstore`, clippy `-D warnings`, fmt clean
|
||
|
||
## References
|
||
|
||
- docs/architecture/core-contract.md (the seam, mechanism contracts)
|
||
- docs/architecture/decisions/008-contract-v1-pinning.md §1–§3, §6, §8
|
||
- docs/architecture/decisions/007-transactional-seam.md
|
||
- docs/architecture/decisions/014-outbox-tx-enqueue.md
|
||
- docs/architecture/decisions/019-mechanism-handle-surfaces.md §1–§5
|
||
- docs/architecture/decisions/021-tx-reads-and-value-shape-fixes.md
|
||
|
||
## Notes
|
||
|
||
- **Async posture**: the "desugared boxed-future form" (ADR-008 §2) is
|
||
realized as a named alias, `BoxedFuture<'a, T> =
|
||
Pin<Box<dyn Future<Output = T> + Send + 'a>>` in
|
||
`src/future.rs` — no `async_trait` macro, no core dependency beyond
|
||
the pinned posture (Send bounds the futures: `begin_tx` returns
|
||
`Box<dyn TxHandle + Send>`, so its futures must be equally
|
||
portable). Sync methods (`try_recv`/`save_offset`/`offset` on
|
||
receivers, `job(&self)`, `name(&self)`) stay sync as pinned.
|
||
- **`with_tx` dyn encoding**: the pinned signature is
|
||
non-generic — a `Store` trait object must stay object-safe — so the
|
||
generic-`F` form does not compile on the trait; the pinned
|
||
`FnOnce(&mut dyn TxHandle) -> BoxedFuture<Result<()>>` shape crosses
|
||
dyn-encoded as `Box<dyn FnOnce ...>`, aliased `WithTxClosure<'a>`
|
||
(re-exported) to keep the complex type out of signatures. Values
|
||
return through captured state; the provided default body commits on
|
||
`Ok`, drops (rolls back) on `Err`.
|
||
- **Delivery callable encoding** (ADR-019 §5's implementer's choice):
|
||
the named one-method trait object — `trait Delivery { fn
|
||
deliver(&mut self, Box<dyn JobHandle>) -> BoxedFuture<Result<()>>;
|
||
}` — with `run_once(worker_id, delivery: &mut dyn Delivery)`.
|
||
`&mut dyn FnMut` cannot return boxed futures of borrowed data under
|
||
object safety, so the named-trait form is the workable answer; the
|
||
engine acks on the closure's `Ok` and retries on its `Err`.
|
||
- **Fallibility**: the ADR sketches elide `Result<>` for brevity
|
||
(ADR-008 §2's note: every fallible op returns the §5 taxonomy);
|
||
entry points validating names must err, so constructors
|
||
(`stream`/`queue`/`outbox`/`listen`) are `Result`-returning as is
|
||
`notify` (PayloadTooLarge); `begin_tx` returns `Result` (SQLite
|
||
writer-slot acquisition can fail); no-claim and no-lock outcomes
|
||
stay values (`Option`), handle-op outcomes stay `bool`s per
|
||
ADR-008 §5's act-differently rule.
|
||
- **Scheduler error variants** (`InvalidSpec { spec }`,
|
||
`LeadershipLost` — ADR-009 §6) joined `Error` here per
|
||
`core-errors-and-validation`'s note (exactly-six list was that
|
||
task's scope; their signatures pin in this task). Unit-tested.
|
||
- **`tokio` added as a *dev*-dependency only** (test to drive the
|
||
boxed-future probes) — runtime deps remain exactly
|
||
serde/serde_json/thiserror (ADR-020 §4, ADR-001: no driver deps in
|
||
core).
|
||
- `Wake`'s `#[non_exhaustive]` prevented the probe's struct-literal
|
||
construction — the probe re-exports or constructs nothing of the
|
||
read types beyond the existing value-type tests; trait-object
|
||
construction is the probe's whole point.
|
||
- Every trait doc carries the pinned semantics text: entry-point
|
||
validation obligations (Store; TxHandle's own mirror — name-bearing
|
||
`*_tx` methods validate like their auto-commit counterparts),
|
||
drop=rollback (TxHandle), no-`claim_tx` rationale (Queue), the
|
||
uniform validity predicate + absolute-reset heartbeat (JobHandle),
|
||
monotone saves (StreamHandle/EventReceiver), terminal-close arms
|
||
and the deliberate no-`recv_timeout` absence (receivers).
|
||
|
||
## Summary
|
||
|
||
The core trait surface — contract v1 in code — is implemented and
|
||
re-exported from `alkstore/src/lib.rs`: `BoxedFuture` +
|
||
`TxHandle` (the eleven pinned methods incl. ADR-021 §1's four tx
|
||
reads, `commit(self: Box<Self>)`, drop=rollback docs);
|
||
`Store` (the pinned method set + the `with_tx` provided method in the
|
||
pinned dyn-encoded signature, with commit/rollback dispositions —
|
||
unit-tested); mechanism handles `Queue`/`JobHandle`/`StreamHandle`/
|
||
`Outbox` (+ `Delivery`)/`Lock` with the exact ADR-019 §1/§3/§5 method
|
||
sets; receivers `WakeReceiver` (timeout expiry = `Ok(None)`; `Err` =
|
||
Database/Closed only) and `EventReceiver` (no `recv_timeout`,
|
||
deliberate); `Error::InvalidSpec`/`LeadershipLost` added (ADR-009
|
||
§6). Object-safety compile probes construct every trait object from
|
||
trivial local impls (`trait_objects_construct`), and the `with_tx`
|
||
dispositions run against a probe store (`with_tx_commit_and_
|
||
rollback_dispositions`) — 23 tests green; `cargo test -p alkstore`,
|
||
workspace `cargo build`, clippy `-D warnings` (workspace), and
|
||
`cargo fmt --check` all clean. |