- No sibling (alkstore) dependency in-core; reactivity is deployment-layer composition above the facade — cadence stays embedder-owned (ADR-005), with alkstore the named substrate for it; in-crate notify/scheduler/queues/invalidation hooks are non-goals. - Fleet sweeper lock pinned as 'named exclusive sweep lease' semantics; shipped provider unchanged (pg advisory clause); provider swaps are ADR-007-shaped substitution doors. - Composition contract for downstream ADRs: co-tenancy of CAS/GC state + reactive machinery + consumer/liveness tables on one fleet host; no-cross-store-atomicity ruled; compensation rests on idempotent re-puts, windowed deletes, sweeps-as-report-data. - OQ-11 resolved (the register's one live circular hedge): engine features pull their own driver clusters — sqlite becomes feature 'sqlite' (default-on, carries rusqlite); 'kv'/'large' tier features retired; 'local' ungated; pg-pure builds are supported, non-default. Deciding fact already in-tree (REQ-2/ADR-009). - requirements.md: UC-4/REQ-5 records the composition facts (operator record 2026-10-11, corroborated by alkstore's consumer-inventory). - Publish guard stated pre-code: facade ships before publish; semver-is-contract from first publish (alkstore ADR-017 discipline). - Amended accepted ADRs' feature wording (ADR-010 §1, ADR-011 §3/§4, ADR-003) with supersession notes; README/overview/open-questions/ backends/store-api/gc aligned; parked index now empty. Verification: docs-only round (no Rust surface touched); full-tree single-value check on the feature graph and sweep-lease wording.
14 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-10-11 (ADR-013 — the fleet sweeper lock's semantics pinned as the named exclusive sweep lease; provider unchanged; composition boundary referenced) |
Store API
What this is
The store core's public surface: the typed facade above the tier engines that consumers (alkgit, alkfs, the ops module) program against. It owns typed keys and hashing (ADR-002), size-threshold dispatch (ADR-003), the put-pinning seam, and the liveness/sweep seam (ADR-005) — and nothing else: no paths, no manifests, no wire (ADR-004, ADR-001). The facade is engine-blind (ADR-007/008/010): which engines a node runs changes nothing here except fleet-GC mechanism wiring (ADR-008's mechanism lives in the engine-state seam, ADR-010 §2).
Public surface
Write path
put(len: Option<u64>, stream)→(Key, Pin)— the streaming put seam (POC #3 finding A1). Two internal paths behind one signature:- Known length (encouraged, documented as such on the API): the
preamble hashes before content flows; one pass, no staging needed
for hashing. Covers git objects, pre-staged files (
stat-derived length), network offers,Content-Length-bearing uploads. - Unknown length: content buffers in memory up to the dispatch
threshold; on commit the buffer is the whole stored artifact
(kv tier) — or, on mid-stream threshold overflow, the buffer
flushes into a stage file and the stream continues into it
(large tier — the streaming pass cannot include the preamble, so
the derivation pass restarts over preamble + staged bytes; forced
by the preamble being inside the hashed input, not a design
choice). Verification runs over the eventual stored artifact as a
whole; a stream error on either path leaves no entry and no stage
file. Eliminates the A6 dispatch asymmetry by construction:
routing is a pure function of content length (ADR-003). The large
tier's stage mechanics are the staged-put contract (ADR-010
§1): the engine hands out a staging cursor (
local: the stage file; pg-lo: chunkedlowritestatements inside its tx), commit publishes, every failure path discards.
- Known length (encouraged, documented as such on the API): the
preamble hashes before content flows; one pass, no staging needed
for hashing. Covers git objects, pre-staged files (
- Stage-file hygiene invariant (POC #3 finding A5, codified): every failure path converges on stage-discard; every success path converges on commit-rename. No early returns that bypass cleanup; this is an asserted invariant with dedicated exact-count tests, not review hygiene.
Pin/Batch— RAII guard on the put path (ADR-005): the entry is liveness-protected until the guard drops or the caller converts it into a consumer-side reference (registered liveness, a root tag).Batchgroups multi-put writes under one batch-scoped pin (manifest writes are exactly this); a batch's pins drop together on batch commit or drop (ADR-005 §Decision).
Read path
get(key) → Option<(len, stream)>— small tier returns a bounded read; large tier streams (local: a pread loop; pg-lo:lo_getwindows). The per-engine shapes are hidden behind the crate-internal read cursor the store core drives (ADR-010 §1 — engines construct their own cursor bodies; the facade's shape is uniform). Dispatch fall-through: a small-tier miss queries the large tier (POC #1 finding 6 — the general rule, ADR-012 §6.1: every key-addressed op —get,stat,has,delete,read_range— consults the kv tier first, then the large tier). A miss on both tiers is aNone(get/stat) orErr(Missing)(read_range/delete) — see the error model.stat(key) → Option<EntryMeta>— cheap length/type probe (phase-0 OQ-BL-04 addendum; gix's header-only read is git's cheapest primitive and the packfile-serving surface rides this).EntryMetacarrieslen: u64and the key's algorithm (the "type" from the store's perspective is the hash algorithm — there is no other type at this layer).statrides theBackendtrait'ssizemethod (ADR-008): an indexed-column probe on kv engines, a file stat onlocal, a companion-row probe on pg-lo — never a value/content load.read_range(key, range) → (slice, slice_digest)— slice plus an out-of-band slice digest: SHA-256 over the slice bytes, fixed by convention so callers and ops handlers agree without negotiation (per-range verification against the canonical digest is impossible — ADR-006). Range reads fall through tiers exactly asgetdoes (small-tier miss slices the large tier's value). Local range serving (packfiles) is the consumer of this.get_stream/ fanout — the ops module's fetch handler is a broadcast above the store: one reader, store arm + subscriber arms (POC #3 finding A3); late joiners degrade to normal verifiedgetafter commit. The store exposes the seam; the fanout policy lives in the ops layer (ADR-001).
Lifecycle
-
has(key) → bool— the pure existence probe (ADR-008'shasdiscipline; asize-probe under the dispatch general rule — absent on both tiers isfalse; virgin-store reads are no-ops). -
delete(key) → Result<(), Error>— falls through tiers per the dispatch general rule above. Participates in ADR-005's delete windows; direct deletes are permitted but refuse protected keys — pinned, re-observed live via registered sources, or named by the protect callback (ADR-012 §6.4: the full protection set, not just pins — the ADR-005 invariant applied to the direct path) — with the typedGcRefuseerror. Direct deletes are unusual by posture; most deletion flows through sweeps. An embedder's correction flow deregisters/re-registers around the delete. -
list() → stream of keys— whole-pool enumeration; complete by contract (see backends doc for why list correctness is load-bearing). -
register_liveness_source(...)— the live-shared GC seam (ADR-005, §Decision; the POC's "install implies copy" failure (finding 7) is why the verb/name is pinned here). Fleet form (ADR-012 §6.6): the same verb registers a named liveness table reference in the shared engine (the embedder's own table per ADR-008 §2 — the store core reads it, never owns it); in-process callback sources remain the single-node form. -
sweep() → Result<SweepReport, Error>— explicit mark-and-sweep; embedders own the cadence (ADR-005, no ambient timers). Sweeps are never errors: an aborted sweep returnsOk(SweepReport)withaborted: Some(GcAbortCause)— there is no sweep-abort error variant (ADR-012 §1; the previously speccedGcAbortedsweep error is retired — see the error model). A sweep with no registered liveness sources aborts without deleting — the safe default — carryingGcAbortCause::NoLivenessSources.SweepReport's shape:candidates_staged,deleted,cancelled_by_arbitration(u64 counts),aborted: Option<GcAbortCause>, andstarted_at/finished_at.GcAbortCauseis a data enum with exactly three variants —ProtectFailed(protection-source failure, ADR-005),SweeperLock(fleet sweeper-lock contention, ADR-008),NoLivenessSources(the safe default above); callers match non-exhaustively; new causes are new ADRs (ADR-012 §1). The direct-delete refusal path is a different type: theGcRefuseerror (above), not a sweep outcome. Staged-window candidates overdue at sweep time are re-arbitrated and executed by that sweep (the executor is the next sweep — ADR-012 §6.7; non-SQL engines execute their window inline within the same sweep, ADR-012 §5).Under a fleet (multiple store instances over one pool — requirements.md REQ-2), sweep/put/delete semantics are mechanism-extended by ADR-008: pins are DB-backed rows committed atomically with entries (the store core holds the joint tx, ADR-010 §2; all GC state hosts on the fleet's kv engine — ADR-012 §4), one sweeper holds the advisory lock (named exclusive sweep lease semantics: one sweeper per pool, atomic test-and-set, holdership ends at release/owner death/engine-session loss; contention is a report abort, not an error — ADR-012 §1, semantics pinned ADR-013 §2), and delete arbitration is SQL-level — the facade's invariant text is unchanged. The facade owns no reactive machinery: cadence, maintenance dispatch, and invalidation wiring are embedder composition above it (ADR-005; ADR-013 §1).
Error model
thiserror; no panics in library code; no unwrap/expect outside
tests (AGENTS convention 2). Distinguished failure families:
Missing— key absent on ops that signal absence viaErr:read_rangeanddelete(ADR-012 §6.5).getandstatsignal absence withNone—Missingis not theirs.Verification— put/get hash-check failure (content ≠ key)Io(String)— backend media failure, covering cursor and staged-put I/O errors surface through the same trait error family (stringly becausestd::io::Erroris not stable across versions)GcRefuse— direct delete refused: the key is protected (pinned, registered-source-live, or protect-callback-named) — nothing deleted; the delete-window arbitration decides at delete time (ADR-005; namedGcRefuseper ADR-012 §1 — there is no sweep-abort error).KeyInvalid— malformed key bytes at the boundary (truncated, unknown algorithm — rejects, never guesses; ADR-002/hashing doc). One enum, both altitudes: the facade's error enum exposesMissing/Verification/Io/GcRefuse/KeyInvalid; theBackendtrait's methods return the same family as crate-internal variants —KeyInvalidnever fires at the trait boundary (keys arrive opaque there) andGcRefusenever fires below the facade (arbitration answers flow up as data; the facade decides and owns the refusal), so the trait's effective members areMissing/Verification/Io/KeyInvalid; the facade re-exposes them withGcRefuseadded (ADR-012 §6.5's family statement reads through this split).
Virgin-store semantics: read paths on a fresh store see absent/empty, never "table does not exist" errors (POC #1 finding 2 — the redb lesson generalizes to any kv engine).
Concurrency posture
- Async I/O throughout;
tokio::syncfor lifecycle correlation;parking_lotfor short-held internal locks (AGENTS convention 3); poisoned locks degrade viaunwrap_or_else(|e| e.into_inner()). - Blocking file work lives in
spawn_blockinginside backend impls (the alkgit trait-execution pattern); the store never blocks the executor. - The sweep-vs-put window is an architectural mechanism — delete windows — specified in ADR-005, not an implementation note.
Invariants (the test gate)
- Whole-blob put/get round-trips byte-identical under the canonical
derivation, both tiers, both put paths, and every shipped engine
of both tiers (kv: sqlite + postgres + mem; large:
local+ pg-lo; ADR-007/008/009/011) (interop with real git remains the source of truth, per POC #1's lesson about hardcoded vectors). - Dedup: putting identical content twice (same or different path) is one pool entry.
- Stage hygiene: any failure mid-put leaves zero stage files; any success leaves exactly one committed entry.
- Sweep safety: with correct liveness registered, sweep counts are
exact; with aborting sources, sweep deletes nothing. Engine-gated
form (ADR-008): single-node postures assert this under the
in-process protocol; fleet postures (shared-pool engines) assert it
under DB-backed pins + the advisory-locked sweep, with the
exact-count sweep-outcome shape run against dockerized postgres
under
--all-features. list()correctness is observable only through GC — list-related tests assert through sweep outcomes (POC #1 finding 2's lesson codified).size/staton any engine returns the entry length without fetching content (asserted per engine: kv row probe,localstat, pg-lo companion-row probe — ADR-008/009).- Engine-conformance uniformity (ADR-010 §1): every engine's
cursor/stage/list bodies pass the same contract suite mem
(the reference engine) passes — exact-count sweep outcomes, stage
hygiene, virgin-store no-ops, CAS re-put, ranged reads (the POC #7
gate is the norm; per-engine suite runs in CI,
--all-featuresfor the feature-gated engines).
Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| 002 | Hashing & keys | derivation + encoding this surface is built on |
| 003 | Dispatch | routing is a pure function of length; pre-threshold buffering |
| 005 | Pools & GC | pins, liveness seams, delete windows, sweep semantics |
| 006 | Verification | whole-blob checks; slice digests out-of-band |
| 008 | Trait size + fleet GC |
stat/accounting ride the trait's size probe; fleet pins/sweeps are engine-backed |
| 010 | I/O seams + GC-state home | read cursor / staged put behind the facade; the store core holds the joint entry+pin tx |
| 012 | Sweep-abort shape + wire/activation rulings | swept = Ok(report) always, GcRefuse for direct-delete refusal; fleet mode is a constructor declaration; GC state hosts on the kv engine |
| 013 | Sweep lease + composition boundary | the advisory lock's semantics = named exclusive sweep lease (provider unchanged); no reactive machinery/facade hooks in-core |
Open Questions
None owned by this document beyond the cross-references above.
References
docs/research/poc-largeblob-findings.mdfindings A1/A5/A6, benchmark tabledocs/research/poc-trait-dispatch-findings.mdfindings 1–3, 7- ADR-003 (put-path buffering), ADR-005 (pin/Batch/Pin lifecycle), ADR-006 (range-read semantics), ADR-002 (keys), ADR-012 (the sweep-abort shape, error-model ruling, fall-through general rule)
- ops-surface.md — the fanout/fetch consumer of the read path
- gc-and-namespaces.md — the lifecycle seam details