Files
alkblobs/docs/architecture/store-api.md
T
glm-5.3-flash 9bbe6f3ad9 docs(architecture): ADR-009 — pg-lo admitted as the fs tier's second engine on POC #7
- ADR-009 (new): postgres Large Objects ship as the fs tier's second
  engine (feature pg-lo, default-off), admitted on POC #7's passed
  gate (contract 10/10, durable put ~= fs at >=1 MiB, lo_get window
  gets, transactional LO lifecycle = zero crash orphans). Engine
  shape: companion table (lo_entries) is the contract authority; put =
  one tx (lo_create + lowrite + row + pin); get = descriptorless
  lo_get windows (lo_*64 discipline); named operator deltas (catalog
  space never returned / autovacuum inherited / sweep for the legacy
  bypass class only) and named performance deltas (cached-get 20-50x
  behind page-cache fs single-stream, ~700 MB/s aggregate at 16
  readers = the fleet serving picture).
- Ripple: backends-and-dispatch (pg-lo shipped; fleet locality
  contract gains the whole-fleet-pg-lo option), overview (layer map,
  feature table + pg-lo row, OQ -> shipped pointer), requirements
  (engine def + REQ-2 note updated), README (current-state + ADR
  table), store-api (invariant 1/6 engine lists), ADR-008 Status
  amendment (pg-lo posture superseded by ADR-009), open-questions
  (audit trail completed: decided -> sequenced -> completed).

Verification: docs-only; all cross-references resolve (ADR/REQ/POC);
register, findings, spec, and both engine-ADR status lines agree.
2026-10-03 04:30:31 +00:00

178 lines
8.9 KiB
Markdown
Raw 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.
---
status: draft
last_updated: 2026-10-03
---
# Store API
## What this is
The store core's public surface: the typed facade above the backends
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): which kv/fs engine a node
runs changes nothing here except fleet-GC mechanism wiring (ADR-008).
## 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
(fs 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).
- **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). **`Batch`** groups 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 a file handle. Dispatch
fall-through: a small-tier miss queries the large tier (POC #1
finding 6).
- **`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). `EntryMeta`
carries `len: u64` and the key's algorithm (the "type" from the
store's perspective is the hash algorithm — there is no other type at
this layer). `stat` rides the `Backend` trait's `size` method
(ADR-008): an indexed-column probe on kv engines, a file stat on the
fs `local` engine — 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 as `get` does
(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 verified
`get` after commit. The store exposes the seam; the fanout policy
lives in the ops layer (ADR-001).
### Lifecycle
- **`has(key)`, `delete(key)`** — deletion participates in ADR-005's
delete windows; direct deletes are permitted but refuse protected
keys — pinned or re-observed live via registered sources (the same
per-key arbitration sweeps apply, typed error) — and are unusual by
posture; most deletion flows through sweeps.
- **`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).
- **`sweep() → SweepReport`** — explicit mark-and-sweep; embedders own
the cadence (ADR-005, no ambient timers). A sweep with no registered
liveness sources aborts without deleting — the safe default. Under
a fleet (multiple nodes 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, one sweeper
holds the advisory lock, and delete arbitration is SQL-level — the
facade's invariant text is unchanged.
## Error model
`thiserror`; no panics in library code; no `unwrap`/`expect` outside
tests (AGENTS convention 2). Distinguished failure families:
- `Missing` — key absent (get/stat miss)
- `Verification` — put/get hash-check failure (content ≠ key)
- `Io(String)` — backend media failure (stringly because
`std::io::Error` is not stable across versions)
- `GcAborted` — nothing deleted; sweep not run. Two cause classes
(ADR-005: a protection source failed; ADR-008: another node holds
the fleet sweeper lock) — the typed error distinguishes them.
- `KeyInvalid` — malformed key bytes at the boundary (hashing doc)
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::sync` for lifecycle correlation;
`parking_lot` for short-held internal locks (AGENTS convention 3);
poisoned locks degrade via `unwrap_or_else(|e| e.into_inner())`.
- Blocking file work lives in `spawn_blocking` inside 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)
1. 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; fs: `local` + pg-lo;
ADR-007/008/009)
(interop with real git remains the source of truth, per POC #1's
lesson about hardcoded vectors).
2. Dedup: putting identical content twice (same or different path) is
one pool entry.
3. Stage hygiene: any failure mid-put leaves zero stage files; any
success leaves exactly one committed entry.
4. 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`.
5. `list()` correctness is observable only through GC — list-related
tests assert through sweep outcomes (POC #1 finding 2's lesson
codified).
6. `size`/`stat` on any engine returns the entry length without
fetching content (asserted per-backend: kv probe, fs stat, pg-lo
companion-row probe — ADR-008/009).
## Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| [002](decisions/002-canonical-hash-and-key-encoding.md) | Hashing & keys | derivation + encoding this surface is built on |
| [003](decisions/003-backend-contract-and-dispatch.md) | Dispatch | routing is a pure function of length; pre-threshold buffering |
| [005](decisions/005-pooled-cas-and-gc-mechanism.md) | Pools & GC | pins, liveness seams, delete windows, sweep semantics |
| [006](decisions/006-verification-posture-and-transfer-encoding.md) | Verification | whole-blob checks; slice digests out-of-band |
| [008](decisions/008-trait-size-probe-fleet-gc-and-fs-engines.md) | Trait `size` + fleet GC | stat/accounting ride the trait's `size` probe; fleet pins/sweeps are engine-backed |
## Open Questions
None owned by this document beyond the cross-references above.
## References
- `docs/research/poc-largeblob-findings.md` findings A1/A5/A6,
benchmark table
- `docs/research/poc-trait-dispatch-findings.md` findings 1–3, 7
- ADR-003 (put-path buffering), ADR-005 (pin/Batch/Pin lifecycle),
ADR-006 (range-read semantics), ADR-002 (keys)
- [ops-surface.md](ops-surface.md) — the fanout/fetch consumer of the
read path
- [gc-and-namespaces.md](gc-and-namespaces.md) — the lifecycle seam
details