Files
alkstore/tasks/core-trait-surface.md

182 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.