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

8.9 KiB
Raw Permalink Blame History

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
core-trait-surface Core trait surface (Store, TxHandle, mechanism handles, receivers) completed
core-value-types
moderate medium project implementation
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):

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

  • Every trait above with the exact pinned method sets and signatures; compiles as object-safe (Box<dyn ...> everywhere the contract says)
  • with_tx provided method present with the pinned signature
  • A compile-probe test constructs Box<dyn TxHandle>, Box<dyn Queue>, etc. from a trivial local impl, proving object safety of the whole surface
  • 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
  • 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 bools 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.