docs(architecture): ADR-008 — trait size probe, fleet GC, fs-tier engines; requirements anchor

- requirements.md (new): the three use cases pinned as REQ-1..4 with
  the node/pool/fleet/engine vocabulary defined once — ends per-session
  re-derivation of consumer facts. REQ-2 (replicator fleet over one
  shared pg pool, incl. large-blob serving) is recorded as a planning
  fact predating all POCs, on the operator's authority.
- ADR-008: Backend trait gains size(key) length probe (before any
  backend ships data — ADR-002 one-way-door discipline); fleet GC
  mechanism (DB-backed pin rows committed atomically with entries,
  TTL+renewal semantics, liveness = embedder-owned table, protect
  callback is single-node-only, advisory-locked single sweeper,
  staged re-arbitrated delete window on both engines); fs tier becomes
  engine-selectable (local default; pg-lo named candidate) with the
  fleet locality contract (shared media or re-routing; mixed tiers are
  a documented deployment invariant, not a constructor-provable one).
- poc-pglo-spec.md (new): POC #7 spec — pg Large Objects as the fs
  tier's pg-lo engine; instruments, decision gate, registered in
  phase-0.md OQ-BL-06.
- ADR-003/005 status amendments point to ADR-008's extensions; specs
  ripple (backends/store-api/gc/ops/overview/README).
- open-questions.md: deferral-policy header gains the decisions-vs-
  sequenced-work distinction; pg-lo's why-not-parked audit trail
  recorded.
- research fixes: postgres POC renumbered #4->#5 to the canonical
  register (phase-0 OQ-BL-06), redb cross-refs fixed, thinking-
  artifact sentence in B1 replaced with the honest reading.

Verification: docs-only change; reference-integrity sweep across the
tree (ADR/REQ/POC refs resolve); architecture-reviewer pass on the
delta — original 3 criticals addressed, its follow-up (fleet liveness
form, pin TTL, staged-delete semantics, enforceability, shared-media
caveats) fixed in this commit.
This commit is contained in:
glm-5.3-flash committed 2026-10-03 03:41:49 +00:00
1 parent 395d58cf3f
commit 281c37876e
16 files changed
+689 -88

No files matched your search

+19 -2
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-02 last_updated: 2026-10-03
--- ---
# alkblobs — Architecture # alkblobs — Architecture
@@ -36,12 +36,24 @@ there — not inherited as hedges:
(POCs #3/#5/#6); the would-be "deployment trigger" framing was caught (POCs #3/#5/#6); the would-be "deployment trigger" framing was caught
at review as circular hedging (a deployment running alkblobs cannot at review as circular hedging (a deployment running alkblobs cannot
pre-exist the engine) and closed on the OQ-09 precedent. pre-exist the engine) and closed on the OQ-09 precedent.
- **ADR-008** (2026-10-03) repairs the seams ADR-007 stressed, against
the pinned consumer requirement REQ-2 (requirements.md — a replicator
node may run a **fleet** of store instances sharing one pg pool, a
planning fact predating all POCs): the `Backend` trait gains a
`size` probe (before any backend ships data); fleet GC gets
DB-backed pins, an advisory-locked single sweeper, and SQL-level
delete arbitration (single-node postures unchanged); and the fs tier
becomes engine-selectable (`local` default; `pg-lo` named candidate,
admission-gated on POC #7). It also adds
[requirements.md](requirements.md) — the REQ anchor that ends
per-session re-derivation of consumer facts.
## Architecture Documents ## Architecture Documents
| Document | Scope | Status | | Document | Scope | Status |
|---|---|---| |---|---|---|
| [overview.md](overview.md) | Purpose, consumers, layer map, dependency posture | draft | | [overview.md](overview.md) | Purpose, consumers, layer map, dependency posture | draft |
| [requirements.md](requirements.md) | Consumer requirements REQ-1..4, node/pool/fleet vocabulary | draft |
| [hashing-and-keys.md](hashing-and-keys.md) | Canonical hash, key encoding (the one-way door) | draft | | [hashing-and-keys.md](hashing-and-keys.md) | Canonical hash, key encoding (the one-way door) | draft |
| [store-api.md](store-api.md) | Store facade: put/get/stat/range/pin/batch/sweep surface, errors, invariants | draft | | [store-api.md](store-api.md) | Store facade: put/get/stat/range/pin/batch/sweep surface, errors, invariants | draft |
| [backends-and-dispatch.md](backends-and-dispatch.md) | Backend trait contract, shipped backends, size-threshold dispatch | draft | | [backends-and-dispatch.md](backends-and-dispatch.md) | Backend trait contract, shipped backends, size-threshold dispatch | draft |
@@ -60,6 +72,7 @@ there — not inherited as hedges:
| [005](decisions/005-pooled-cas-and-gc-mechanism.md) | Pooled CAS, liveness seams, mark-and-sweep, delete windows | Accepted | | [005](decisions/005-pooled-cas-and-gc-mechanism.md) | Pooled CAS, liveness seams, mark-and-sweep, delete windows | Accepted |
| [006](decisions/006-verification-posture-and-transfer-encoding.md) | Whole-blob verification; transfer encoding excluded by scoping | Accepted | | [006](decisions/006-verification-posture-and-transfer-encoding.md) | Whole-blob verification; transfer encoding excluded by scoping | Accepted |
| [007](decisions/007-two-kv-engines-sqlite-and-postgres.md) | Two kv engines (sqlite + postgres) behind one trait; constructor-selected | Accepted | | [007](decisions/007-two-kv-engines-sqlite-and-postgres.md) | Two kv engines (sqlite + postgres) behind one trait; constructor-selected | Accepted |
| [008](decisions/008-trait-size-probe-fleet-gc-and-fs-engines.md) | Trait `size` probe; fleet GC (DB-backed pins, advisory-locked sweeper); fs-tier engines | Accepted |
## Open Questions ## Open Questions
@@ -68,7 +81,11 @@ Tracked in [open-questions.md](open-questions.md). The Phase 0 register
remain parked — OQ-07/OQ-08 externally-owned (alkgit seam mapping, remain parked — OQ-07/OQ-08 externally-owned (alkgit seam mapping,
alkfs intake; carried for visibility, gating nothing here). OQ-09 alkfs intake; carried for visibility, gating nothing here). OQ-09
(namespace-visibility default) resolved closed-by-default; OQ-10 (namespace-visibility default) resolved closed-by-default; OQ-10
(second kv engine) resolved by ADR-007. **Deferral (second kv engine) resolved by ADR-007. The pg-lo fs-engine question
resolved to *sequenced work*, not a parked question: REQ-2/ADR-008
named it the candidate fs-tier engine and POC #7
(`docs/research/poc-pglo-spec.md`) is its admission evidence — a task,
not a wait. **Deferral
policy (the "Schrödinger's code" rule):** a *decision this crate needs policy (the "Schrödinger's code" rule):** a *decision this crate needs
before shipping* may not be deferred on a dependency that is itself before shipping* may not be deferred on a dependency that is itself
waiting for this crate to exist. Two parking kinds remain legitimate: waiting for this crate to exist. Two parking kinds remain legitimate:
+48 -15
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-02 last_updated: 2026-10-03
--- ---
# Backends and dispatch # Backends and dispatch
@@ -39,10 +39,16 @@ is ever reachable through that door.
is also the persistence boundary — a kv row or an fs filename has no is also the persistence boundary — a kv row or an fs filename has no
schema-migration story, so the byte layout must be self-describing schema-migration story, so the byte layout must be self-describing
(algorithm-tagged keys). (algorithm-tagged keys).
- **Methods:** `has` / `get` / `put` / `delete` / `list` / `name`. - **Methods:** `has` / `get` / `put` / `delete` / `list` / `name` /
Streaming put/get shapes ride the store layer's seams (store-api.md); `size` (the length probe added by ADR-008). Streaming put/get shapes
ride the store layer's seams (store-api.md);
backends provide byte- or handle-level primitives underneath backends provide byte- or handle-level primitives underneath
(`get` on the fs tier yields a file handle, not a loaded buffer). (`get` on the fs tier yields a file handle, not a loaded buffer).
`size(key)` returns the entry's length or `None` if absent —
metadata only, never a content fetch: kv engines probe the indexed
`size` column both row shapes carry (POC #5 B5); the `local` fs
engine stats the file. Store-level `stat` and pool accounting ride
this method.
- **`list()` complete by contract.** A *malformed* list is as deadly as - **`list()` complete by contract.** A *malformed* list is as deadly as
an incomplete one: list correctness is only observable through GC an incomplete one: list correctness is only observable through GC
(POC #1 finding 2 — the redb key-vs-value trap produced a sweep that (POC #1 finding 2 — the redb key-vs-value trap produced a sweep that
@@ -122,18 +128,43 @@ sweep-outcome CI gate, run against dockerized postgres under
near-linearly to ~37k puts/s at 24 conns — the only engine whose near-linearly to ~37k puts/s at 24 conns — the only engine whose
throughput *increases* under concurrency. throughput *increases* under concurrency.
### fs backend (feature `fs`, default-on) ### fs backend (feature `fs`, default-on); engines: `local` (default) + `pg-lo` (candidate)
Large blobs. Flat sharded layout: `{hex-prefix}/{hex-prefix}/{hash}` Large blobs. Like the kv tier (ADR-007), the fs tier is **one contract,
sharding survives from iroh's conclusion (limits directory size on multiple engines** — the medium is a per-node constructor choice
huge pools); stage-then-commit-rename for the two-pass unknown-length (ADR-008; REQ-2 in requirements.md makes the engine concept load-bearing
path; pread-based range reads (POC #3 finding A2 — local range serving for fleets):
is sound, e.g. for packfiles).
- **`local` engine** (the default; the ADR-003 backend as originally
specced): flat sharded layout:
`{hex-prefix}/{hex-prefix}/{hash}` sharding survives from iroh's
conclusion (limits directory size on huge pools); stage-then-commit-
rename for the two-pass unknown-length path; pread-based range
reads (POC #3 finding A2 — local range serving is sound, e.g. for
packfiles).
- **`pg-lo` engine** (candidate, *not shipped*; ADR-008): postgres
Large Objects as the fs tier's storage — the same engine-behind-
one-trait move as ADR-007, one tier over. Admission is gated on
POC #7's measured evidence and its own engine ADR (see
`docs/research/poc-pglo-spec.md`).
- **Fleet locality contract (ADR-008):** over one shared pool, the fs
tier is either shared media (every node's `local` root on the same
fleet-shared media — soundness caveats and the required media
guarantees in ADR-008) or re-routed (one storage node serves pool
large-blob content via the ops surface; client nodes are
kv-tier-only over pool content). Mixed per-node-local fs tiers over
one pool is the partitioning failure this contract exists to
prevent — a documented deployment invariant the constructor cannot
fully prove but must be declared against (ADR-008 names the
enforceable seam and the detection symptom).
### mem backend (feature `mem`, default-off) ### mem backend (feature `mem`, default-off)
`BTreeMap`-shaped ephemeral backend for tests and in-process `BTreeMap`-shaped ephemeral backend for tests and in-process
ephemerality. A testing/utility tier, never a production story. ephemerality. A testing/utility tier, never a production story.
Implements the full trait contract including `size` (trivially) — it
exists partly as the reference impl every other backend's conformance
tests can mirror.
## Size-threshold dispatch (ADR-003) ## Size-threshold dispatch (ADR-003)
@@ -156,12 +187,13 @@ ephemerality. A testing/utility tier, never a production story.
The trait is open to future implementations (network stores, S3-like The trait is open to future implementations (network stores, S3-like
tiers), but nothing in the current consumer set requires one, and the tiers), but nothing in the current consumer set requires one, and the
contract is deliberately hostile to half-implementations (complete contract is deliberately hostile to half-implementations (complete
`list()`, GC-participating `delete`). Any future backend — or third kv `list()`, GC-participating `delete`). Any future backend — or a third
engine — is a new ADR carrying its own sweep-safety story (ADR-007 is kv engine or second fs engine — is a new ADR carrying its own
the precedent for an engine addition: measured evidence first, impl sweep-safety story (ADR-007 is the precedent for a kv engine
contract naming the durability/ops posture deltas). This crate's addition; ADR-008 named pg-lo as the candidate second fs engine with
roadmap is not blocked on one (see open-questions.md — alkfs intake POC #7 as its admission evidence — the door is exercised twice). This
may name needs externally; OQ-08). crate's roadmap is not blocked on one (see open-questions.md — alkfs
intake may name needs externally; OQ-08).
## Design Decisions ## Design Decisions
@@ -171,6 +203,7 @@ may name needs externally; OQ-08).
| [004](decisions/004-two-backends-no-third.md) | Two backends, no third | scope boundary against manifest-layer absorption | | [004](decisions/004-two-backends-no-third.md) | Two backends, no third | scope boundary against manifest-layer absorption |
| [005](decisions/005-pooled-cas-and-gc-mechanism.md) | Namespace-blindness | backends see hashes only | | [005](decisions/005-pooled-cas-and-gc-mechanism.md) | Namespace-blindness | backends see hashes only |
| [007](decisions/007-two-kv-engines-sqlite-and-postgres.md) | Two kv engines | sqlite (default) + postgres behind one trait; one relational engine + fs per node | | [007](decisions/007-two-kv-engines-sqlite-and-postgres.md) | Two kv engines | sqlite (default) + postgres behind one trait; one relational engine + fs per node |
| [008](decisions/008-trait-size-probe-fleet-gc-and-fs-engines.md) | `size` probe + fleet GC + fs engines | trait length probe; DB-backed pins/advisory-locked sweeper for fleets; fs tier engine-selectable (`local` default, pg-lo candidate) |
## Open Questions ## Open Questions
@@ -4,7 +4,11 @@
Accepted (engine pin amended by ADR-007 — the kv tier ships two Accepted (engine pin amended by ADR-007 — the kv tier ships two
engines, sqlite and postgres, behind this contract; the dispatch layer engines, sqlite and postgres, behind this contract; the dispatch layer
and trait are unchanged) and trait are unchanged. The trait was extended by ADR-008 with a
`size` length probe — before any backend ships data — and the fs
tier became engine-selectable (`local` default; pg-lo candidate), so
this ADR's "engines"/"backends" counts are read through ADR-004's
tier≠engine scope note)
## Context ## Context
@@ -38,9 +42,10 @@ Evidence in hand (no further POC or consumer-waiting needed):
generic-over-`K` alternative was considered and rejected in POC #1 generic-over-`K` alternative was considered and rejected in POC #1
finding 1: with one key type, generics buy nothing and break the finding 1: with one key type, generics buy nothing and break the
persistence boundary.) persistence boundary.)
- **Methods:** `has`/`get`/`put`/`delete`/`list`/`name`. Streaming and - **Methods:** `has`/`get`/`put`/`delete`/`list`/`name`/`size` (the
range shapes ride the store layer above (fs `get` yields a file length probe added by ADR-008 before any backend shipped data).
handle, not a loaded buffer). Streaming and range shapes ride the store layer above (fs `get`
yields a file handle, not a loaded buffer).
- **`list()` complete by contract** — and *well-formed*: a malformed - **`list()` complete by contract** — and *well-formed*: a malformed
list (wrong destructure, values-as-keys) is as deadly as an list (wrong destructure, values-as-keys) is as deadly as an
incomplete one, and silent until a sweep deletes the wrong things incomplete one, and silent until a sweep deletes the wrong things
@@ -2,7 +2,13 @@
## Status ## Status
Accepted Accepted (the sweep-safety protocol here is the single-node posture;
ADR-008 extends the mechanism to fleet topologies — DB-backed pins,
advisory-locked single sweeper, SQL-level delete arbitration under
shared-pool engines (REQ-2) — while keeping this ADR's invariants
and the in-process protocol unchanged everywhere else. The "one pool
per node" phrasing below reads as "per deployment" for fleets; the
vocabulary is pinned in requirements.md)
## Context ## Context
@@ -64,7 +64,9 @@ relational engine a per-node choice rather than a crate fork.
1. **sqlite stays the default engine**: zero-ops, no daemon, solo 1. **sqlite stays the default engine**: zero-ops, no daemon, solo
economics (44k solo puts/s at 1-16 KiB), and consumer co-tenancy economics (44k solo puts/s at 1-16 KiB), and consumer co-tenancy
rides the same file (honker's WAL-NORMAL defaults are literally the rides the same file (honker — the alk* family's SQLite
notify/queue extension, `/workspace/honker` — ships WAL + the
NORMAL-synchronous posture, so its defaults are literally the
shipped tier). Single-machine nodes are sqlite by measurement, and shipped tier). Single-machine nodes are sqlite by measurement, and
remain so even at sustained rates well above observed push traffic remain so even at sustained rates well above observed push traffic
(~1.2k/s WAL ceiling vs. single-node push rates an order below it). (~1.2k/s WAL ceiling vs. single-node push rates an order below it).
@@ -0,0 +1,256 @@
# ADR-008: Trait length probe, fleet GC, and fs-tier engines
## Status
Accepted
## Context
Three defects surfaced in the post-ADR-007 architecture review, all
concentrated at the seams ADR-007 touched and all load-bearing for
REQ-2 (a replicator node may run multiple store instances — a fleet —
sharing one pool via postgres; requirements.md):
1. **The `Backend` trait has no length probe.** The store surface
promises `stat(key) → Option<EntryMeta>` ("cheap length/type
probe"; store-api.md) and pool accounting rides `list` + `stat`
(ADR-005), but the trait's methods are `has/get/put/delete/list/
name` with no value-metadata access. The kv engines' row shapes
both carry a `size` column (POC #5 B5), and the fs tier can stat a
file — but with no trait method, kv `stat` degenerates to a full
value load. The trait is the crate's second stable seam (ADR-003);
discovering this during implementation would be a breaking trait
change after backends exist.
2. **ADR-005's sweep-safety protocol is per-node by construction.**
Pins, the pin/liveness arbitration lock, and delete-window
arbitration all live inside one process (ADR-005 §Decision). That
is sound for sqlite and single-instance postgres (REQ-1, REQ-3
topologies) — but under a fleet (REQ-2), node A's sweep enumerates
the *shared* pool and arbitrates against A's in-process pin map;
node B's in-flight put is invisible. Data loss, structurally: the
topology REQ-2 requires does not satisfy the protocol ADR-005
specifies.
3. **The fs tier is node-local, silently partitioning a fleet.** The
fs backend stores to a sharded layout on the node's own filesystem
(ADR-003). Under a fleet sharing one kv pool, a >threshold blob
put via node A exists only on A's disk; node B's get fall-through
queries its own fs and misses, and `list()` unions disagree per
node. REQ-2 makes this topology a shipping requirement, not a
hypothetical.
These three travel together: the fleet topology forces #2 and #3, and
#3's resolution constrains what the fs tier must support, while #1 is
a trait-level gap any topology hits.
## Decision
### 1. `size(key) → Option<u64>` joins the `Backend` trait
The trait gains a length probe: `size(key)` returns the entry's byte
length or `None` if absent. Required of all engines; semantics per
tier:
- **kv engines**: an indexed column probe (`SELECT size …WHERE key`),
never a value load. The `size` column both POC row shapes carry
becomes contract-relevant.
- **fs engine (`local`)**: stat/metadata read, no content fetch.
- The store facade's `stat` (store-api.md) and the accounting path
(ADR-005) ride this method; the `has` method remains a pure
existence probe.
`size` is a trait addition *before* any backend ships data — the
one-way-door discipline of ADR-002/003 applied to the trait itself.
### 2. Fleet GC: the delete window is durable and re-arbitrated in the database; pins are DB-backed under fleet engines
The store facade's GC invariant is unchanged — **a visible pool entry
is never deleted while liveness protects it, and an in-flight put is
never deleted by a sweep started before it committed** (ADR-005).
Under a fleet, the mechanism moves into the shared engine:
- **Pins are rows.** `put`'s pin-before-publish (ADR-005) becomes two
inserts in one transaction on the fleet engine: the entry row and
its pin row commit together (or neither does). The pin row carries
an owner (node id) and expiry: the pin guard renews its row every
TTL/3 (default TTL 60 s, constructor-tunable — fleet constructor
parameters, not API). **Protection = an un-expired pin row**: at
delete-time arbitration, only `expiry > now()` pins count; an
expired pin is abandoned (its holder failed to renew — the same
liveness-failure signal as an in-process guard's drop) and is reaped
by the sweeper's maintenance step (the GC-of-GC cleanup). A node
that pauses past TTL loses its pins; delete-then-recover (ADR-005)
remains the correctness backstop — content is re-put byte-identically
under the same key.
- **Registered liveness sources have one fleet form: a liveness
table.** A fleet embedder registers its liveness as a named table of
`(key, …)` rows in the shared engine (schema pinned by the embedder,
shape contractually `(key bytea, …)` — any columns it needs beyond
the key). In-process callback *views* are not a fleet liveness form:
delete-time arbitration queries/finger-locks tables per key, so a
source must be a table for the SQL protocol to see it. Single-node
embedders keep callback sources unchanged (ADR-005).
- **The protect callback is single-node-only.** A fleet sweeper cannot
consult another node's in-process callback, so under fleet engines
the callback's fleet expression is: the embedder materializes its
protect set into its liveness table (or pin rows) *before* invoking
sweep — the callback's abort semantics remain for
single-node/topology-local sweeps that cannot express their
protection as rows.
- **One sweeper per pool.** A fleet node acquires a pg advisory lock
before sweeping and holds it for the whole sweep. Two nodes
sweeping concurrently is not a state this crate tolerates — the
lock is the admission gate, and a second-node sweep returns the
typed `GcAborted` (no deletion) rather than racing. `GcAborted`
thus carries two cause classes (protection-source failure —
ADR-005; sweeper lock contention — this ADR); the typed error's
fleet variant identifies which.
- **Delete-time arbitration is SQL-level.** The delete window's
re-check (ADR-005: "arbitrate under the pin/liveness lock at delete
time") executes as `SELECT … FOR UPDATE` against pin rows and
liveness tables at delete time, then batch-deletes. The **staged
re-arbitrated delete**: during mark, candidate-dead keys are staged
as rows carrying `visible_after = mark_end + window` (the delete
window's delay — default 5 min, constructor-tunable per
deployment); the executor (the sweeper, or the next sweep's owner)
re-arbitrates each staged candidate *at execution time* against
pins and liveness tables before deleting. This is the durable
delete window on every engine — the window table is engine rows on
sqlite as well; what differs by topology is only where the liveness
state lives (in-process maps and callbacks for single nodes;
tables and SQL arbitration for fleets), not whether staging exists.
No external queue dependency is required (the pattern is
engine-native transactions; consumer-side queues like honker or
pg-boss-lineage tools remain consumer-layer options, per ADR-004's
boundary).
- **Single-node postures are unchanged.** sqlite and single-instance
postgres keep ADR-005's in-process protocol verbatim: the pin map,
RAII guards, and arbitration lock stay in-process; only the
*persistence* of the delete window (the staged-candidate table)
is shared machinery. The pin-row expiry/ttl machinery exists only
where fleet engines are active.
### 3. The fs tier is engine-selectable; fleet topology dictates which engines are permissible
The fs tier is a tier, not a medium. It ships engines the way the kv
tier does (ADR-007), and a deployment's topology determines which are
valid:
- **`local` (default):** the sharded-layout filesystem backend,
unchanged (ADR-003). Valid for any single-node topology (REQ-1,
REQ-4) and for fleet nodes using **shared media** (below).
- **Fleet + shared media (deployment requirement):** multi-node pools
may keep the `local` engine by pointing every node's fs tier at
shared/replicated media. The soundness claim is *qualified*, not
assumed: immutable-once-committed files plus stage-then-commit-
rename are sound on media that provides (a) atomic rename within a
directory, (b) close-to-open consistency (a written+closed file is
complete to any later reader — NFSv3+ with the close-to-open
discipline; NFSv4 and EFS-class stores qualify), and (c) no
cross-node POSIX locking reliance (this store takes none — its
cross-node coordination is the fleet engine's locks, never file
locks; honker's NFS warning is about sqlite's lock protocol, not
this shape). Verifying (a)-(c) on the deployment's actual media is
the deployment's responsibility; a verification pass is recorded
ops work, not crate code.
- **Fleet re-routing (topology alternative):** a fleet whose nodes do
not share media designates **one storage node**; large-blob writes
and reads over the pool route through it via the ops surface
(verified fetch/put; ADR-001). Client nodes' constructors are
configured with a **kv-tier-only dispatch** (or an fs tier reserved
for node-private, consumer-owned content — which is outside the
pool and outside this crate's GC; it is the consumer's own storage
beside the pool per ADR-004, not pool content). The facade shape on
a client node is its own store for sub-threshold content plus its
registered ops-put path for large content — a consumer-side
composition, not a store-level mode.
- **`pg-lo` (candidate engine, not shipped):** postgres Large Objects
as the fs tier's storage — the same "engine behind one contract"
move as ADR-007, one tier over. Named here so the door is explicit,
with its admission gated on measured evidence (POC #7: LO write/read
curves at the packfile regime, tx-scoped handle cost under
pooling, `pg_largeobject`/vacuum posture under churn, crash-orphan
behavior) and its own engine ADR. **Not shipped speculatively;**
REQ-2's immediate fleet answers are shared media or re-routing;
pg-lo is the consolidation option if those are unacceptable.
- **The no-mixed-fs-tiers rule is a deployment invariant, enforced at
the enforceable seam.** Cross-node configuration cannot be validated
by any one constructor (it sees only its own node). What the
constructor *can* enforce: a fleet-mode constructor (fleet engines
active) requires an explicit fs-tier declaration — `local` (with the
operator's assertion that its root is fleet-shared media), routed
(no local fs over pool content), or — when shipped — `pg-lo`. The
cross-node truth ("is node B's `local` actually the same media as
node A's?") is a deployment invariant this crate documents and ops
verifies, not something a constructor can prove; a partitioned
tier's observable signature (get fall-through misses for entries
another node wrote) is the documented detection symptom.
- **`pg-lo` (candidate engine, not shipped):** postgres Large Objects
as the fs tier's storage — the same "engine behind one contract"
move as ADR-007, one tier over. Named here so the door is explicit,
with its admission gated on measured evidence (POC #7: LO write/read
curves at the packfile regime, tx-scoped handle cost under
pooling, `pg_largeobject`/vacuum posture under churn, crash-orphan
behavior) and its own engine ADR. **Not shipped speculatively;**
REQ-2's immediate fleet answers are shared media or re-routing;
pg-lo is the consolidation option if those are unacceptable.
In sum: a fleet's fs tier over pool content is either shared media (all
nodes' `local` roots on the same fleet-shared media) or routed (one
storage node); mixing per-node-local media over one pool is the
partitioning failure this decision exists to prevent.
## Consequences
**Positive**
- The trait now expressibly supports `stat` and pool accounting on
every engine; the "cheap probe" claim of store-api.md is true rather
than aspirational.
- REQ-2 (fleet over one pool) has a complete, specced GC story: pins
move to the shared engine atomically with put-commits, one sweeper
holds the lock, deletes re-arbitrate in-transaction. The ADR-005
invariant extends to fleets as a *stated* mechanism, not an
accident of single-process luck.
- The fs tier's fleet problem has named, shipped-code-free answers
(shared media, re-routing) and a named consolidation candidate
(pg-lo) whose admission path is the same POC-first door as ADR-007.
**Negative**
- The `Backend` trait gains a method before any backend ships — the
correct moment, but every engine's task list grows by one method.
- Fleet GC is real state machinery: pin rows with expiry (GC-of-GC
surface for orphaned pins at node crash — the expiry is the cleanup
handle), advisory-lock coordination, and SQL-level arbitration
added on top of the already-intricate delete-window state machine.
- `pg-lo` remains unbuilt: a fleet that cannot share media and cannot
re-route pays for its ADR + POC before shipping large blobs — the
measured, deliberate deferral of the ADR-006 pattern, not a hedge.
**Neutral**
- The sqlite and single-node-postgres postures are unchanged; this ADR
costs existing paths nothing at runtime.
- `pg-lo`, if admitted, rides the same driver stack ADR-007 already
ships (tokio-postgres + deadpool) via SQL `lo_*` functions — no
new driver dependency (the `postgres_large_object` crate is a dead
0.15-era io-trait glue; unnecessary).
## References
- [requirements.md](../requirements.md) — REQ-2 (the fleet fact this
ADR binds), REQ-1/3/4 (the topologies left unchanged)
- POC #5 (`poc-postgres-kv-findings.md` B5 — row shapes with `size`
columns; the driver stack pg-lo would ride)
- ADR-002 (the one-way-door discipline this trait change applies),
ADR-003 (the trait contract and admission gate `size` and pg-lo
amend/enter), ADR-004 (tier≠engine, extended to the fs tier),
ADR-005 (the GC invariant and single-node protocol extended here,
not replaced), ADR-006 (the deferred-cost pattern pg-lo's admission
follows), ADR-007 (the engine-behind-one-trait precedent)
- [backends-and-dispatch.md](../backends-and-dispatch.md) (the trait
and tier surface this amends);
[store-api.md](../store-api.md) (`stat`'s contract);
[gc-and-namespaces.md](../gc-and-namespaces.md) (the fleet GC
mechanism); [ops-surface.md](../ops-surface.md) (the re-routing
topology's transport)
+19 -10
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-02 last_updated: 2026-10-03
--- ---
# Pooling, namespaces, and GC # Pooling, namespaces, and GC
@@ -16,11 +16,17 @@ price, and this document specifies the mechanism the POCs validated
## The pooled CAS ## The pooled CAS
- **One pool per node.** Repos, workspaces, and appfile stores are all - **One pool per deployment; a node is one store instance** (the
engine/vocabulary definitions live in requirements.md). Repos,
workspaces, and appfile stores are all
*sets of hash references* over the same CAS — never walled-off *sets of hash references* over the same CAS — never walled-off
per-repo stores. Git's `alternates`/object-pool mechanism (GitLab per-repo stores. Git's `alternates`/object-pool mechanism (GitLab
object pools) is the same idea done awkwardly; the pool generalizes it object pools) is the same idea done awkwardly; the pool generalizes it
to unrelated repos and to p2p replication. to unrelated repos and to p2p replication. **A fleet (REQ-2) — two or
more nodes sharing one pool — is the one topology where the GC
mechanism below extends beyond a node: ADR-008 pins, sweeper
locking, and delete arbitration move into the shared engine;
the invariants are unchanged.**
- **One address space.** Under the canonical hash (ADR-002), the same - **One address space.** Under the canonical hash (ADR-002), the same
file content has the same address whether it entered via a git oid or file content has the same address whether it entered via a git oid or
a workspace manifest — cross-consumer dedup is free by construction, a workspace manifest — cross-consumer dedup is free by construction,
@@ -116,16 +122,19 @@ existence flips atomically per key).
| [005](decisions/005-pooled-cas-and-gc-mechanism.md) | Pooled CAS & GC | one pool, registered liveness, mark-and-sweep, delete windows, no ambient scheduling | | [005](decisions/005-pooled-cas-and-gc-mechanism.md) | Pooled CAS & GC | one pool, registered liveness, mark-and-sweep, delete windows, no ambient scheduling |
| [003](decisions/003-backend-contract-and-dispatch.md) | list() contract | complete lists are the GC substrate | | [003](decisions/003-backend-contract-and-dispatch.md) | list() contract | complete lists are the GC substrate |
| [004](decisions/004-two-backends-no-third.md) | Structure-blindness | manifests/refs never sink into the store | | [004](decisions/004-two-backends-no-third.md) | Structure-blindness | manifests/refs never sink into the store |
| [008](decisions/008-trait-size-probe-fleet-gc-and-fs-engines.md) | Fleet GC extension | DB-backed pins, advisory-locked single sweeper, SQL delete arbitration — for shared-pool (fleet) topologies only; the in-process protocol is unchanged elsewhere |
## Open Questions ## Open Questions
None owned by this document. The p2p replicator's cross-node GC None owned by this document. Shared-pool GC coordination within a
coordination is a property of the replicator policy layer (above the deployment is now in-crate (fleet mechanism: ADR-008). What remains a
crate), governed by the same seam — it raises a question here only replicator-policy-layer property (above the crate) is *cross-pool*
when a consumer *names a requirement* (OQ-07/OQ-08 intake), which is coordination — gossip sync between separately-pooled replicator nodes
re-entry via a new decision at a new ADR, not a parked question: no deciding what each keeps — governed by the same seam; it raises a
decision here waits on a consumer, and a future requirement does not question here only when a consumer *names a requirement*
create one retroactively. (OQ-07/OQ-08 intake), which is re-entry via a new decision at a new
ADR, not a parked question: no decision here waits on a consumer, and
a future requirement does not create one retroactively.
## References ## References
+32 -4
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-02 last_updated: 2026-10-03
--- ---
# Open Questions # Open Questions
@@ -31,6 +31,19 @@ make before shipping. Phase 0's residual list was swept under this
rule: every residual either resolved into an ADR (the evidence was rule: every residual either resolved into an ADR (the evidence was
already in hand) or re-owned as below. already in hand) or re-owned as below.
**The line between the two kinds** (added 2026-10-03, after the
pg-lo round): deferring on a consumer's *design decision* is valid —
the deciding fact is obtainable independently of this crate (OQ-07,
OQ-08). Deferring on a consumer's *deployment of this crate* is
invalid — that fact is created by this crate (OQ-10's original
framing, OQ-09's original framing). And a third thing is neither:
**decided-but-sequenced work**. Once a question resolves to a decision
(the pg-lo engine is the named fs-tier candidate under REQ-2/ADR-008),
what remains is the evidence pipeline the decision's own admission
gate requires (POC #7) — a task with an owner, not a parked question,
and not a hedge. A decision is not "unmade" because its verification
is pending; deferral policy applies to decisions, not to work.
**Index of active OQs:** OQ-07, OQ-08 (externally-owned, carried **Index of active OQs:** OQ-07, OQ-08 (externally-owned, carried
for visibility). OQ-09 and OQ-10 are resolved (recorded below). for visibility). OQ-09 and OQ-10 are resolved (recorded below).
Promoted Phase 0 questions OQ-BL-01..06 are recorded here with their Promoted Phase 0 questions OQ-BL-01..06 are recorded here with their
@@ -205,9 +218,12 @@ resolutions for traceability.
- **Origin**: `docs/research/phase-0.md` - **Origin**: `docs/research/phase-0.md`
- **Status**: resolved (complete: #1, #3 passed; #2 absorbed; #4 - **Status**: resolved (complete: #1, #3 passed; #2 absorbed; #4
covered in miniature, concurrency half specified as architecture in covered in miniature, concurrency half specified as architecture in
ADR-005) ADR-005; post-convergence additions #5 postgres and #6 redb passed
- **Resolution**: register complete; Phase 0 ended; evidence trail in and fed ADR-007; #7 pg-lo specified — `docs/research/poc-pglo-spec.md`,
`docs/research/`. requested by REQ-2/ADR-008)
- **Resolution**: register complete and extended post-convergence;
Phase 0 ended; evidence trail in `docs/research/`. The register's
canonical numbering lives in phase-0.md OQ-BL-06.
- **Cross-references**: all ADRs above - **Cross-references**: all ADRs above
--- ---
@@ -229,6 +245,18 @@ a deployment trigger — the same invalid shape, caught at review: the
trigger was a fact only this crate could create. It resolved to trigger was a fact only this crate could create. It resolved to
ADR-007 (two kv engines; see its entry above). ADR-007 (two kv engines; see its entry above).
The pg-lo fs-engine question (raised in the ADR-008 review round,
2026-10-03) never became a parked question: REQ-2 made the
multi-instance-with-large-blobs topology a standing requirement
(requirements.md — a planning fact predating all POCs), so there was
nothing to defer *on*. It resolved to ADR-008 naming pg-lo as the
candidate fs-tier engine + POC #7 (`docs/research/poc-pglo-spec.md`)
as its admission evidence — sequenced work, per the deferral-policy
header's third category. An earlier draft of that OQ parked on
"alkgit's architecture process names the topology" — rendered moot the
same day by the REQ-2 fact; recorded here because the audit trail of
*why not parked* is part of this document's job.
OQ-07/OQ-08 are owned by other repos' processes and are not alkblobs OQ-07/OQ-08 are owned by other repos' processes and are not alkblobs
tracker tasks (their outcome arrives *through* their owners, not tracker tasks (their outcome arrives *through* their owners, not
through any artifact this repo creates). through any artifact this repo creates).
+7 -2
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-02 last_updated: 2026-10-03
--- ---
# Ops surface (alkcall-backed network operations) # Ops surface (alkcall-backed network operations)
@@ -142,7 +142,11 @@ side channel: fetch handlers call `get`/`read_range`/`stat`; put
handlers call `put`; both honor pins and delete windows. There is no handlers call `put`; both honor pins and delete windows. There is no
ops-internal path around the store facade (ADR-001 §Consequences — ops-internal path around the store facade (ADR-001 §Consequences —
this is what makes "re-homing the ops surface later loses nothing" this is what makes "re-homing the ops surface later loses nothing"
true). true). One fleet topology additionally rides this module: **re-routing
(ADR-008)** — a fleet whose nodes do not share fs media routes
large-blob access through a single storage node via these ops, with
client nodes' fs tiers serving only node-private content. The module
supports this shape without change; no fleet-specific op exists.
## Design Decisions ## Design Decisions
@@ -150,6 +154,7 @@ true).
|---|---|---| |---|---|---|
| [001](decisions/001-substrate-posture-and-ops-placement.md) | Substrate posture & placement | alkcall-backed, feature-gated ops module; conformance-not-deviation rationale | | [001](decisions/001-substrate-posture-and-ops-placement.md) | Substrate posture & placement | alkcall-backed, feature-gated ops module; conformance-not-deviation rationale |
| [006](decisions/006-verification-posture-and-transfer-encoding.md) | Verification | verified-fetch via carried digests; no chunk trees | | [006](decisions/006-verification-posture-and-transfer-encoding.md) | Verification | verified-fetch via carried digests; no chunk trees |
| [008](decisions/008-trait-size-probe-fleet-gc-and-fs-engines.md) | Fleet re-routing topology | non-shared-media fleets route large-blob access through a storage node via these ops; no new ops |
## Open Questions ## Open Questions
+32 -13
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-02 last_updated: 2026-10-03
--- ---
# Overview # Overview
@@ -10,9 +10,15 @@ last_updated: 2026-10-02
`alkblobs` is content-addressed blob storage in the alk* family: a `alkblobs` is content-addressed blob storage in the alk* family: a
put/get/verify store keyed under one canonical content hash (the put/get/verify store keyed under one canonical content hash (the
git-family derivation; legacy SHA-1-tolerant), with pluggable backends git-family derivation; legacy SHA-1-tolerant), with pluggable backends
(small blobs in a kv store, large blobs on a filesystem fallback), split (small blobs in a kv store, large blobs on a filesystem fallback),
from any wire/ops-protocol layer. Network ops ride the alkcall substrate split from any wire/ops-protocol layer. Network ops ride the alkcall
behind a feature-gated `ops` module (ADR-001). substrate behind a feature-gated `ops` module (ADR-001).
Deployment-relevant consumer facts — what each use case forces on this
crate — are pinned in [requirements.md](requirements.md) (REQ-1..4,
with the **node/pool/fleet/engine** vocabulary defined there). This
tree references REQs by ID; per-session re-derivation from vision prose
is exactly what that document exists to prevent.
## Why it exists — the extracted shared core ## Why it exists — the extracted shared core
@@ -60,10 +66,11 @@ or three times.
│ size-threshold dispatch (ADR-003) │ │ size-threshold dispatch (ADR-003) │
└───────┬───────────────────┬───────────────────────────────┘ └───────┬───────────────────┬───────────────────────────────┘
▼ ▼ ▼ ▼
kv backend fs backend (ADR-003: exactly two tiers; │ kv backend fs backend (ADR-003: exactly two tiers;
│ ADR-007: kv = 2 engines) │ │ ADR-007: kv = 2 engines;
▼ │ ▼ ADR-008: fs engines too)
engine per node: sqlite (default) | postgres (feature "postgres") │ engine per node: sqlite (default) | postgres (feature "postgres")
│ fs engine per node: local (default) | pg-lo (candidate, POC #7)
transport: none, anywhere. alkgit/alkfs or the embedder dials; transport: none, anywhere. alkgit/alkfs or the embedder dials;
an alkcall Connection is handed in at the ops boundary. an alkcall Connection is handed in at the ops boundary.
@@ -74,15 +81,22 @@ or three times.
| Layer | Dependencies | Notes | | Layer | Dependencies | Notes |
|---|---|---| |---|---|---|
| base crate (store core + backends) | `tokio`, `thiserror`, `parking_lot`, plus `rusqlite` behind the `kv` feature and nothing behind `fs`/`mem` | no alkcall, no serialization frameworks | | base crate (store core + backends) | `tokio`, `thiserror`, `parking_lot`, plus `rusqlite` behind the `kv` feature and nothing behind `fs`/`mem` | no alkcall, no serialization frameworks |
| `postgres` feature (ADR-007) | adds `tokio-postgres` + `deadpool-postgres` behind the kv tier's second engine, default-off | same `Backend` contract, own sweep-safety CI gate under `--all-features` | | `postgres` feature (ADR-007) | adds `tokio-postgres` + `deadpool-postgres` behind the kv tier's second engine, default-off | depends on `kv`'s contract surface (the `Backend` trait definition is base-crate code); the engine's *impl* is additive. Same `Backend` contract, own sweep-safety CI gate under `--all-features` |
| `ops` feature | adds `alkcall` (feature-gated, default-off) | JSON op specs + binary channels + `AccessControl`; ADR-001 | | `ops` feature | adds `alkcall` (feature-gated, default-off) | JSON op specs + binary channels + `AccessControl`; ADR-001 |
| consumer crates | depend on alkblobs (and on alkcall directly when they speak ops) | alkgit, alkfs | | consumer crates | depend on alkblobs (and on alkcall directly when they speak ops) | alkgit, alkfs |
Backends are feature-gated; both `kv` and `fs` ship default-on Backends are feature-gated; both `kv` and `fs` ship default-on
(batteries-included, the alkgit feature-model pattern); the `kv` tier's (batteries-included, the alkgit feature-model pattern — default-on
postgres engine (`postgres`, ADR-007) and the `ops` feature are features, opt-out rather than opt-in); the `kv` tier's postgres engine
default-off. Wasm: the backends are not a wasm story; a wasm client is a (`postgres`, ADR-007) and the `ops` feature are default-off. A pg-only
consumer of the `ops` surface (which rides alkcall, itself kv node (postgres engine without rusqlite) is **not** a supported
configuration this crate optimizes for — feature-graph work for it is
deferred until a named requirement exists (the base `kv` feature
pulling rusqlite is the accepted v1 shape; convention 8's
lean-base principle applies to *default* builds, which stay lean since
`kv` is what the feature model controls, not what default-on means
downstream). Wasm: the backends are not a wasm story; a wasm client is
a consumer of the `ops` surface (which rides alkcall, itself
wasm-clean), never an in-process embedder (ADR-001 §Consequences). wasm-clean), never an in-process embedder (ADR-001 §Consequences).
## Non-goals (each enforced by an ADR) ## Non-goals (each enforced by an ADR)
@@ -111,6 +125,7 @@ wasm-clean), never an in-process embedder (ADR-001 §Consequences).
| [005](decisions/005-pooled-cas-and-gc-mechanism.md) | Pooled CAS & GC | flat pool, registered liveness, delete windows | | [005](decisions/005-pooled-cas-and-gc-mechanism.md) | Pooled CAS & GC | flat pool, registered liveness, delete windows |
| [006](decisions/006-verification-posture-and-transfer-encoding.md) | Verification posture | whole-blob verification; chunk trees excluded by scoping | | [006](decisions/006-verification-posture-and-transfer-encoding.md) | Verification posture | whole-blob verification; chunk trees excluded by scoping |
| [007](decisions/007-two-kv-engines-sqlite-and-postgres.md) | Two kv engines | sqlite default + postgres behind one trait; one relational engine + fs per node | | [007](decisions/007-two-kv-engines-sqlite-and-postgres.md) | Two kv engines | sqlite default + postgres behind one trait; one relational engine + fs per node |
| [008](decisions/008-trait-size-probe-fleet-gc-and-fs-engines.md) | `size` probe + fleet GC + fs engines | trait length probe; fleet pins/sweeps engine-backed; fs tier engine-selectable |
## Open Questions ## Open Questions
@@ -125,6 +140,10 @@ crate level:
open-read requirement) open-read requirement)
- **OQ-10**: second kv engine — resolved by ADR-007 (sqlite + postgres - **OQ-10**: second kv engine — resolved by ADR-007 (sqlite + postgres
shipped behind one trait; per-node constructor choice) shipped behind one trait; per-node constructor choice)
- **POC #7** (`docs/research/poc-pglo-spec.md`): pg Large Objects as
the fs tier's `pg-lo` engine — spec'd **sequenced work** (not a
parked question; open-questions.md's policy header distinguishes the
two): admission evidence for ADR-008's named candidate (REQ-2)
## References ## References
+103
View File
@@ -0,0 +1,103 @@
---
status: draft
last_updated: 2026-10-03
---
# Requirements
The deployment-relevant facts each use case forces on this crate.
Specs and ADRs reference these by ID (REQ-n); a future session must be
able to read this document and never re-derive a requirement from
conversation or vision prose. **This document is the anchor: when a
consumer fact changes, it changes here first, with a dated note, and
the ADRs re-derive from it.**
## Vocabulary (defined once; used everywhere)
- **Engine** — a concrete storage implementation of one tier's
contract: the kv tier's engines are sqlite and postgres (ADR-007);
the fs tier's engines are `local` (default) and `pg-lo` (candidate —
see REQ-2, ADR-008).
- **Pool** — the flat content-addressed namespace of one deployment
(`hash → bytes` everywhere; ADR-005).
- **Node** — one running store instance (one process embedder; its fs
tier is its own media). Node is the GC arbitration domain for
single-node topologies (ADR-005's pins, liveness locks, and delete
windows live inside a node); under a fleet, arbitration moves into
the shared engine (ADR-008).
- **Fleet** — two or more nodes over **one shared pool** (e.g. via one
postgres instance). A fleet is the only topology in which
cross-node GC and fs-locality questions exist (ADR-008).
- **Deployment** — one operator's whole system: typically one fleet
(multi-node) or one node; consumer tables, op registrations, and
sweep cadence are deployment choices.
## The use cases and their requirements
### UC-1: Self-hosted git serving (gitea-replacement scale)
Git repos for a user/team on one machine. Small-object regime dominated
by loose objects in the kv tier; packs and large artifacts (git-lfs
shape) on the fs tier.
- **REQ-1 (traces: ADR-007, POC #3 A4)** — single-machine economics:
sqlite is the default kv engine; single-writer WAL ceiling (~1.2k
objects/s under contention) is comfortably above single-node push
rates. **Zero-ops is a requirement**: no daemon beyond the node
process itself.
### UC-2: Distributed git with replicator nodes hosting repos for many users
The p2p/OSS sketch (phase-0 §p2p shape): replicators are push/clone
endpoints serving many concurrent users; gossip-style sync between
replicators; ACL/ownership above (alkcall layer).
- **REQ-2 (traces: ADR-007, ADR-008)** — **a replicator node may run
multiple store instances (a fleet) sharing one pool** via a shared
postgres kv engine, including serving large (>threshold) pool
content from that fleet. This is stated as a requirement, dated
2026-10-03 (recording a planning fact from the use case's origin
discussion; it predates all POCs and architecture docs, recorded
here on the operator's authority as the use case's stated shape).
Consequences are derived in ADR-008: fleet GC (DB-backed pins,
advisory-locked sweeper, SQL delete arbitration), the fs-tier
engine concept with its locality contract, and the pg-lo candidate
engine (POC #7).
- **REQ-3 (traces: ADR-007, POC #5 B3/B4)** — concurrency scale-out:
the fleet's hub-class nodes need put throughput that *increases*
under concurrency (pg: near-linear to ~37k puts/s at 24 conns);
sqlite's WAL ceiling (~1.2k/s, latency-climbs-unbounded) bounds
single-node sustained write pressure regardless of client count.
This is the measured basis for the per-node engine split.
### UC-3: Agent workspaces (many agents, near-identical content)
Agents working simultaneously in mostly-identical workspaces, each
making small edits; a shared pool dedups by construction; per-workspace
path→hash manifests above.
- **REQ-4 (traces: ADR-005, ADR-002)** — pooling and one address space
across workspaces; dedup must be structural (not configured). Small
edits mean the small-blob regime dominates: sqlite-class kv
performance is what matters; the fs tier is hit rarely (large
workspace artifacts).
## What this document is not
- **Not consumer architecture**: how alkgit maps refs/manifests onto
the store remains OQ-07 (externally-owned); how alkfs shapes
workspaces remains OQ-08. This document records what each use case
*forces on this crate*, not what each consumer *is*.
- **Not a requirements-change process substitute**: a new or changed
REQ arrives here with a dated note and owner; ADRs that depended on
the old fact state their reopen conditions and reopen when this
file changes.
## Decision trace
| REQ | Introduced | Source | ADRs |
|---|---|---|---|
| REQ-1 | 2026-10-03 (fact: phase-0 §deployment posture) | POC #3 A4 | ADR-007 |
| REQ-2 | 2026-10-03 (fact: use case origin discussion, pre-POC) | user/planning record | ADR-007, ADR-008 |
| REQ-3 | 2026-10-03 (fact: phase-0 §deployment posture) | POC #5 B3/B4 | ADR-007 |
| REQ-4 | 2026-10-03 (fact: phase-0 §vision) | POC #1, ADR-002 | ADR-005 |
+30 -9
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-01 last_updated: 2026-10-03
--- ---
# Store API # Store API
@@ -11,7 +11,9 @@ The store core's public surface: the typed facade above the backends
that consumers (alkgit, alkfs, the ops module) program against. It owns that consumers (alkgit, alkfs, the ops module) program against. It owns
typed keys and hashing (ADR-002), size-threshold dispatch (ADR-003), typed keys and hashing (ADR-002), size-threshold dispatch (ADR-003),
the put-pinning seam, and the liveness/sweep seam (ADR-005) — and the put-pinning seam, and the liveness/sweep seam (ADR-005) — and
nothing else: no paths, no manifests, no wire (ADR-004, ADR-001). 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 ## Public surface
@@ -57,7 +59,9 @@ nothing else: no paths, no manifests, no wire (ADR-004, ADR-001).
primitive and the packfile-serving surface rides this). `EntryMeta` primitive and the packfile-serving surface rides this). `EntryMeta`
carries `len: u64` and the key's algorithm (the "type" from the 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 store's perspective is the hash algorithm — there is no other type at
this layer). 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 - **`read_range(key, range) → (slice, slice_digest)`** — slice plus an
out-of-band slice digest: SHA-256 over the slice bytes, fixed by out-of-band slice digest: SHA-256 over the slice bytes, fixed by
convention so callers and ops handlers agree without negotiation convention so callers and ops handlers agree without negotiation
@@ -85,7 +89,12 @@ nothing else: no paths, no manifests, no wire (ADR-004, ADR-001).
(finding 7) is why the verb/name is pinned here). (finding 7) is why the verb/name is pinned here).
- **`sweep() → SweepReport`** — explicit mark-and-sweep; embedders own - **`sweep() → SweepReport`** — explicit mark-and-sweep; embedders own
the cadence (ADR-005, no ambient timers). A sweep with no registered the cadence (ADR-005, no ambient timers). A sweep with no registered
liveness sources aborts without deleting — the safe default. 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 ## Error model
@@ -96,7 +105,9 @@ tests (AGENTS convention 2). Distinguished failure families:
- `Verification` — put/get hash-check failure (content ≠ key) - `Verification` — put/get hash-check failure (content ≠ key)
- `Io(String)` — backend media failure (stringly because - `Io(String)` — backend media failure (stringly because
`std::io::Error` is not stable across versions) `std::io::Error` is not stable across versions)
- `GcAborted` — a protection source failed; nothing deleted (ADR-005) - `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) - `KeyInvalid` — malformed key bytes at the boundary (hashing doc)
Virgin-store semantics: read paths on a fresh store see absent/empty, Virgin-store semantics: read paths on a fresh store see absent/empty,
@@ -117,18 +128,27 @@ lesson generalizes to any kv engine).
## Invariants (the test gate) ## Invariants (the test gate)
1. Whole-blob put/get round-trips byte-identical under the canonical 1. Whole-blob put/get round-trips byte-identical under the canonical
derivation, both tiers, both put paths (interop with real git derivation, both tiers, both put paths, **and every shipped engine
remains the source of truth, per POC #1's lesson about hardcoded of both tiers** (kv: sqlite + postgres; fs: `local`; ADR-007/008)
vectors). (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 2. Dedup: putting identical content twice (same or different path) is
one pool entry. one pool entry.
3. Stage hygiene: any failure mid-put leaves zero stage files; any 3. Stage hygiene: any failure mid-put leaves zero stage files; any
success leaves exactly one committed entry. success leaves exactly one committed entry.
4. Sweep safety: with correct liveness registered, sweep counts are 4. Sweep safety: with correct liveness registered, sweep counts are
exact; with aborting sources, sweep deletes nothing. 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 5. `list()` correctness is observable only through GC — list-related
tests assert through sweep outcomes (POC #1 finding 2's lesson tests assert through sweep outcomes (POC #1 finding 2's lesson
codified). codified).
6. `size`/`stat` on any engine returns the entry length without
fetching content (asserted per-backend: kv probe, fs stat —
ADR-008).
## Design Decisions ## Design Decisions
@@ -138,6 +158,7 @@ lesson generalizes to any kv engine).
| [003](decisions/003-backend-contract-and-dispatch.md) | Dispatch | routing is a pure function of length; pre-threshold buffering | | [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 | | [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 | | [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 ## Open Questions
+2 -3
View File
@@ -1,8 +1,6 @@
--- ---
status: converged status: converged
last_updated: 2026-10-02 (Phase 0 convergence — all POCs passed, OQs last_updated: 2026-10-03 (POC #7 spec added to the register OQ-BL-06; Phase 0 remains closed)
settled into decisions + residuals; phase-0 plan complete;
OQ-BL-01 ops-surface substrate settled: alkcall)
--- ---
# alkblobs — Phase 0 (Exploration) # alkblobs — Phase 0 (Exploration)
@@ -595,6 +593,7 @@ POC #3 pack analysis):**
| 4 | Pooled CAS + GC: namespace reference tables over the flat pool; mark-and-sweep with protect-callback + TempTag pinning; delete-then-recover semantics | **Covered in miniature by #1** — namespace tables + sweep + recover validated single-threaded; the concurrency half is Phase 1 implementation work, not a POC gate | `poc-trait-dispatch-findings.md` findings 3/7/8 | | 4 | Pooled CAS + GC: namespace reference tables over the flat pool; mark-and-sweep with protect-callback + TempTag pinning; delete-then-recover semantics | **Covered in miniature by #1** — namespace tables + sweep + recover validated single-threaded; the concurrency half is Phase 1 implementation work, not a POC gate | `poc-trait-dispatch-findings.md` findings 3/7/8 |
| 5 | Postgres as the kv engine (added post-convergence): inherited sqlite/fs/pg benchmark arms + write/read concurrency scale-out probes | **Passed 2026-10-02** (6 findings B1-B6: single-conn pg floor ~1 ms fsync-dominated, ~40× storage overhead; pg PUT scale-out ~12× at 12 conns / ~37k ops/s vs sqlite's ~1.2k WAL-serialized ceiling) | `poc-postgres-kv-findings.md`; code: `/workspace/alkblobs-postgres-poc` | | 5 | Postgres as the kv engine (added post-convergence): inherited sqlite/fs/pg benchmark arms + write/read concurrency scale-out probes | **Passed 2026-10-02** (6 findings B1-B6: single-conn pg floor ~1 ms fsync-dominated, ~40× storage overhead; pg PUT scale-out ~12× at 12 conns / ~37k ops/s vs sqlite's ~1.2k WAL-serialized ceiling) | `poc-postgres-kv-findings.md`; code: `/workspace/alkblobs-postgres-poc` |
| 6 | redb as the kv engine (added post-convergence, same standing as #5): inherited sqlite/fs arms + redb durability decomposition + scale-out probe | **Passed 2026-10-02** (6 findings C1-C6: the "2-7× over sqlite" claim inverted — sqlite ~430× over redb at crash-consistent puts; redb Immediate = 1 fdatasync/commit, 8-30 ms on this disk; reads ~530k/s but irrelevant; write scale-out flat ~42/s; ruled out at a durability-tier mismatch, not a benchmark quibble) | `poc-redb-kv-findings.md`; code: `/workspace/alkblobs-redb-poc` | | 6 | redb as the kv engine (added post-convergence, same standing as #5): inherited sqlite/fs arms + redb durability decomposition + scale-out probe | **Passed 2026-10-02** (6 findings C1-C6: the "2-7× over sqlite" claim inverted — sqlite ~430× over redb at crash-consistent puts; redb Immediate = 1 fdatasync/commit, 8-30 ms on this disk; reads ~530k/s but irrelevant; write scale-out flat ~42/s; ruled out at a durability-tier mismatch, not a benchmark quibble) | `poc-redb-kv-findings.md`; code: `/workspace/alkblobs-redb-poc` |
| 7 | Postgres Large Objects as the fs tier's `pg-lo` engine (added 2026-10-03, per ADR-008 naming it the candidate fs-tier engine for REQ-2 fleets): LO write/read curves at the packfile regime, tx-scoped handle cost under pooling, pg_largeobject/vacuum posture under churn, crash-orphan behavior | **Specified, not run** — `poc-pglo-spec.md`; admission evidence for the pg-lo engine ADR | `poc-pglo-spec.md` |
Sequencing outcome: #1 and #3 passed; #2 absorbed/validated under #1; Sequencing outcome: #1 and #3 passed; #2 absorbed/validated under #1;
#4's single-threaded core covered by #1 (the mechanism choice it #4's single-threaded core covered by #1 (the mechanism choice it
+94
View File
@@ -0,0 +1,94 @@
---
status: specified
title: "POC #7 — postgres Large Objects as the fs tier's pg-lo engine"
last_updated: 2026-10-03
---
# POC: postgres Large Objects as the fs tier's pg-lo engine — spec
> **POC register #7**, added post-Phase-0 per the ADR-003/007
> substitution door (the third use of it, after postgres-on-kv and
> redb-on-kv). Code: standalone crate
> `/workspace/alkblobs-pglo-poc` (clone the POC #5 crate's harness
> conventions; add a `PgLoBackend` implementing the ADR-008 trait
> contract including `size`). Findings land here regardless, per the
> established convention. Requested by REQ-2 (requirements.md — the
> fleet-with-large-blobs consolidation option; ADR-008 names pg-lo as
> the candidate fs-tier engine). Status: **specified, not run**.
## What this POC must decide
The ADR-008 question: can postgres Large Objects hold the fs tier's
contract for the fleet topology — serving the ≥128 KiB regime
(packfiles, large artifacts) from the same engine the kv tier already
rides — at acceptable performance and with an honest ops posture? The
fs tier's contract (ADR-003/008): `get` yields a pread-able handle,
stage-then-commit semantics for unknown-length puts, complete
`list()`, GC-participating `delete`, `size` without content fetch.
Large Objects' native shape differs from files in one structural way
the ADR names: **LO descriptors are transaction-scoped** (lo_open →
loread/lowrite must run inside a tx). The POC must measure what that
does to the handle pattern under a connection pool, not assume it away.
## Instruments
1. **Inherited `bench_small`-shaped harness, LO arm** — the POC #3/#5
methodology at fs-tier-relevant sizes: **128 KiB, 256 KiB, 1 MiB,
16 MiB, 128 MiB**, sequential and random-replay, p50/p99. Compare
against: local fs (the `local` engine baseline, same harness) and
kv-tier pg (bytea) at the lower sizes, as cross-checks of
methodology parity. (`loread`/`lowrite` via SQL functions through
tokio-postgres — the `postgres_large_object` crate targets
postgres 0.15 and is dead; do not use it.)
2. **Handle-cost probe** (`pgdiag7`): the tx-scoped descriptor under
deadpool — cost of open-tx → lo_open → lseek/loread per range read,
held vs. re-opened per read; pool pressure under N concurrent
readers (the ops fetch handler's real shape).
3. **Write path probe** (`pgdiag8`): lo_create + chunked lowrite
(the LOBLKSIZE ~2 KiB page layout matters here — measure write
amplification as value size grows); unknown-length put shape
(stage into one LO, companion row on commit); verify
`size`-without-fetch (`SELECT size` from the companion table).
4. **Churn/vacuum probe**: repeated put/delete cycles at 1–16 MiB;
measure `pg_largeobject` catalog growth, TOAST/vacuum behavior,
autovacuum posture (the fs engine inherits ADR-008/007's
maintenance-surface honesty requirement).
5. **Crash/orphan probe**: kill client mid-write-tx; verify no orphan
LO and no half-commit; kill after lo_create before commit; write
the orphan-recovery sweep story the engine ADR will need (compare
`lo_get`-visible OIDs against the companion table — never trust
the catalogs).
## Decision gate (mirrors ADR-007's)
pg-lo is admitted as a shipped fs-tier engine iff:
- **Performance:** LO put/get at 128 KiB–16 MiB is within an
order-of-magnitude of local fs on the same box (the tier is chosen
for fleet correctness, not solo speed — but a 10× gap would put the
fleet's packfile serving materially behind a single-node fs
deployment, and the ADR must say so if measured);
- **Contract:** complete `list()` (companion table), GC-participating
delete (`lo_unlink` under the delete window), virgin-store no-op
reads, `size` probe — all hold, proven via the exact-count
sweep-outcome test shape under `--all-features`;
- **Ops posture:** named deltas (tx-scoped handles, autovacuum/vacuum
on `pg_largeobject`, orphan recovery sweep) with measured or
documented-bounded costs.
Findings feed an engine ADR in the ADR-007 shape (measured curves,
posture deltas as requirements). If the gate fails, ADR-008's
recorded statement stands: shared-media or re-routing are REQ-2's
fleet answers, and pg-lo's named cost was paid as evidence, not
shipped blind.
## Register note (phase-0.md OQ-BL-06 convention)
The canonical register is phase-0.md OQ-BL-06: #1 trait dispatch,
#2 absorbed, #3 large-blob, #4 pooled-CAS (covered by #1), #5
postgres, #6 redb. The post-convergence POC files briefly carried
inconsistent self-numberings (the postgres file titled itself "#4");
they are renumbered to the register. This POC is **#7** — specified
2026-10-03, requested by REQ-2 (requirements.md; ADR-008 names pg-lo
as the candidate fs-tier engine).
+17 -13
View File
@@ -1,14 +1,17 @@
--- ---
status: passed status: passed
title: "POC #4 — postgres as the kv engine: single-conn penalty measured, concurrency scale-out measured" title: "POC #5 — postgres as the kv engine: single-conn penalty measured, concurrency scale-out measured"
last_updated: 2026-10-02 last_updated: 2026-10-03
--- ---
# POC: postgres as the kv engine — findings # POC: postgres as the kv engine — findings
> **POC register #4**, added after Phase 0 convergence (the register was > **POC register #5**, added after Phase 0 convergence (the register was
> complete at #3; this is the ADR-003 "substitution seam" input arriving > complete at #3; this is the ADR-003 "substitution seam" input arriving
> early). Code: standalone crate `/workspace/alkblobs-postgres-poc` > early). *Renumbered 2026-10-03 from a self-assigned "#4" to the
> canonical register (phase-0.md OQ-BL-06), where #4 is the pooled-CAS
> POC covered by #1; the redb file's cross-references were fixed in the
> same pass.)* Code: standalone crate `/workspace/alkblobs-postgres-poc`
> (a copy of the POC #3 crate with a `PgKv` backend added; findings land > (a copy of the POC #3 crate with a `PgKv` backend added; findings land
> here regardless, per the established convention). Date: 2026-10-02. > here regardless, per the established convention). Date: 2026-10-02.
> Status: **passed** — the inherited POC #3 test suite (14 tests, byte-exact > Status: **passed** — the inherited POC #3 test suite (14 tests, byte-exact
@@ -94,13 +97,14 @@ Readings:
floor is 2-5× fs's ~200-450 µs syscall chain (and 10-50× sqlite). floor is 2-5× fs's ~200-450 µs syscall chain (and 10-50× sqlite).
POC #3's fs numbers reproduce within noise, confirming methodology POC #3's fs numbers reproduce within noise, confirming methodology
parity. parity.
- **At 64-128 KiB postgres pulls ahead of sqlite** (~800-1000 vs - **At 64-128 KiB postgres sits between fs and sqlite** (~800-1000 vs
~2050-4100 — actually *behind* sqlite, but ahead of fs on gets and fs ~2300-2900 on gets — *behind* fs on some arms; sqlite still leads
comparable on puts) — wait, no: at 64 KiB sqlite still wins ~4.1k at 64 KiB). The honest reading: pg holds no decisive win in
(~4.1k). The honest reading: pg crosses fs somewhere in the 16-64 KiB the 1-128 KiB single-worker band on this disk; **the pg-vs-fs
band for puts and is ~at-parity through 64-128 KiB. **The pg-vs-fs crossover, where it exists at all, sits higher than sqlite-vs-fs's**.
crossover is real but sits an order of magnitude higher than Note: pg's decisive advantage was never the solo 1-D curve — it is
sqlite-vs-fs.** the concurrency axis (B3/B4), where it multiplies and sqlite
serializes.
- The sqlite numbers reproduce A4 within noise (44k vs 48k at 1 KiB), - The sqlite numbers reproduce A4 within noise (44k vs 48k at 1 KiB),
so the methodology transfer is sound. so the methodology transfer is sound.
@@ -252,7 +256,7 @@ a deployment running the *fs tier anyway* has none of either cost.
list()-complete-by-contract test would need the vacuum-aware list()-complete-by-contract test would need the vacuum-aware
equivalent of POC #1's exact-count sweep assertions. equivalent of POC #1's exact-count sweep assertions.
## Engine deployment posture (recorded 2026-10-02 — the two-engine story, post-POC #4/#6) ## Engine deployment posture (recorded 2026-10-02 — the two-engine story, post-POC #5/#6)
The three alkblobs-relevant git/vfs use cases, mapped onto the measured The three alkblobs-relevant git/vfs use cases, mapped onto the measured
curves; this was the conclusion OQ-10 carried before its resolution curves; this was the conclusion OQ-10 carried before its resolution
@@ -262,7 +266,7 @@ in ADR-007):
|---|---|---| |---|---|---|
| **sqlite** | self-hosted git serving (gitea-replacement scale), agent-workspace pools, small/personal replicators | 44k solo puts/s at 1-16 KiB (POC #3 A4, reproduced here); WAL single-writer ceiling ~1.2k/s under contention is comfortably above single-node push rates; zero-ops, no daemon; honker rides the same file (same WAL-NORMAL posture, its defaults are literally the shipped tier) | | **sqlite** | self-hosted git serving (gitea-replacement scale), agent-workspace pools, small/personal replicators | 44k solo puts/s at 1-16 KiB (POC #3 A4, reproduced here); WAL single-writer ceiling ~1.2k/s under contention is comfortably above single-node push rates; zero-ops, no daemon; honker rides the same file (same WAL-NORMAL posture, its defaults are literally the shipped tier) |
| **postgres** | public multi-tenant replicator nodes (many users pushing, gossip catch-up, bulk ingest), consolidated-ops deployments (already-running pg) | near-linear put scale-out to ~21k at 4 conns, ~28k at 8, ~32k at 12, ~37k at 24-48 (B3); reads scale to ~44k at 24 conns (B4); the only engine whose throughput *increases* under concurrency | | **postgres** | public multi-tenant replicator nodes (many users pushing, gossip catch-up, bulk ingest), consolidated-ops deployments (already-running pg) | near-linear put scale-out to ~21k at 4 conns, ~28k at 8, ~32k at 12, ~37k at 24-48 (B3); reads scale to ~44k at 24 conns (B4); the only engine whose throughput *increases* under concurrency |
| fs (tier, not engine) | ≥128-256 KiB content, packfiles | crossover measured (POC #3 A4), stable across #4/#6 reproductions | | fs (tier, not engine) | ≥128-256 KiB content, packfiles | crossover measured (POC #3 A4), stable across #5/#6 reproductions |
The decision rule the two POCs establish: **the trigger is topology, The decision rule the two POCs establish: **the trigger is topology,
not throughput** — cross-MACHINE write access to one pool makes postgres not throughput** — cross-MACHINE write access to one pool makes postgres
+11 -11
View File
@@ -11,13 +11,13 @@ last_updated: 2026-10-02
> crate `/workspace/alkblobs-redb-poc` — a copy of the POC crate with the > crate `/workspace/alkblobs-redb-poc` — a copy of the POC crate with the
> pg arm replaced by a `RedbKv` backend (`redb` 4.3, the engine iroh-blobs' > pg arm replaced by a `RedbKv` backend (`redb` 4.3, the engine iroh-blobs'
> fs store uses for its metadata tables). The inherited sqlite/fs arms and > fs store uses for its metadata tables). The inherited sqlite/fs arms and
> the 14-test suite are byte-identical to POC #3/#4's methodology. > the 14-test suite are byte-identical to POC #3/#5's methodology.
> Date: 2026-10-02. Status: **passed** (14 tests, clippy `-D warnings` > Date: 2026-10-02. Status: **passed** (14 tests, clippy `-D warnings`
> clean, fmt clean). > clean, fmt clean).
## What this POC set out to decide ## What this POC set out to decide
The "opposite route" from POC #4: postgres is complicated, we only need a The "opposite route" from POC #5: postgres is complicated, we only need a
kv — what about a high-performance *embedded* kv like redb, which the kv — what about a high-performance *embedded* kv like redb, which the
DESIGN.md lineage (iroh-blobs' "faster than the filesystem for small DESIGN.md lineage (iroh-blobs' "faster than the filesystem for small
items") and downstream folklore put anywhere from **2-7× over sqlite**, items") and downstream folklore put anywhere from **2-7× over sqlite**,
@@ -26,11 +26,11 @@ margin at equal semantics, the kv tier's engine pin (ADR-003) is again
informed rather than hypothetical — and so is the anti-recommendation if informed rather than hypothetical — and so is the anti-recommendation if
it doesn't. it doesn't.
Same 2D instrument as POC #4 (the shape of the question, learned there): Same 2D instrument as POC #5 (the shape of the question, learned there):
1.Solo sweep (inherited `bench_small` harness, redb/sqlite/fs arms at 1.Solo sweep (inherited `bench_small` harness, redb/sqlite/fs arms at
1-256 KiB, identical working set) — where the 2-7× claim should show up. 1-256 KiB, identical working set) — where the 2-7× claim should show up.
2. **Worker scale-out probe** (`redb_conc`, plus the POC #4 `sqlite_conc` 2. **Worker scale-out probe** (`redb_conc`, plus the POC #5 `sqlite_conc`
head-to-head) — redb is a single-file single-writer engine; does it head-to-head) — redb is a single-file single-writer engine; does it
degrade like sqlite's WAL or survive like postgres? degrade like sqlite's WAL or survive like postgres?
@@ -129,17 +129,17 @@ were never the bottleneck axis (sqlite was already the fastest get at
adding threads only adds internal transaction-lock contention plus the adding threads only adds internal transaction-lock contention plus the
None-durability freelist growth penalty (pages freed between durable None-durability freelist growth penalty (pages freed between durable
commits accumulate until an Immediate commit collects them — redb's commits accumulate until an Immediate commit collects them — redb's
documented "rapid growth" behavior). Contrast POC #4: sqlite's documented "rapid growth" behavior). Contrast POC #5: sqlite's
contentioned WAL plateaued ~1.2k, pg *scaled* to 28k+. redb's None mode contentioned WAL plateaued ~1.2k, pg *scaled* to 28k+. redb's None mode
is the only arm in three POCs where throughput goes **down** as workers is the only arm in three POCs where throughput goes **down** as workers
go up. go up.
### Finding C5 (the decisive one): redb write scale-out is perfectly flat — ~40-44 ops/s at 1, 2, 4, and 8 threads ### Finding C5 (the decisive one): redb write scale-out is perfectly flat — ~40-44 ops/s at 1, 2, 4, and 8 threads
`redb_conc` Immediate mode (same fresh-key discipline as POC #4's `redb_conc` Immediate mode (same fresh-key discipline as POC #5's
`pgdiag6`/`sqlite_conc`): `pgdiag6`/`sqlite_conc`):
| workers | redb Immediate | sqlite (same probe) | pg (from POC #4) | | workers | redb Immediate | sqlite (same probe) | pg (from POC #5) |
|---------|---------------|---------------------|------------------| |---------|---------------|---------------------|------------------|
| 1 | ~42-44 ops/s | ~700-800/s | ~2.5-3.2k/s | | 1 | ~42-44 ops/s | ~700-800/s | ~2.5-3.2k/s |
| 2 | ~40 ops/s | ~1.0-1.1k/s | ~4.6-5.5k/s | | 2 | ~40 ops/s | ~1.0-1.1k/s | ~4.6-5.5k/s |
@@ -168,7 +168,7 @@ go up.
crash-consistent (unpersisted commits can leave the file failing to crash-consistent (unpersisted commits can leave the file failing to
open until repaired — the freelist bookkeeping itself is lost on open until repaired — the freelist bookkeeping itself is lost on
crash), `Immediate` pays a full fdatasync per commit. The crash), `Immediate` pays a full fdatasync per commit. The
durability-posture comparison that POC #4 could tune continuously durability-posture comparison that POC #5 could tune continuously
(`synchronous_commit=off` on pg) doesn't exist here. (`synchronous_commit=off` on pg) doesn't exist here.
- redb's `Database` is process-lifetime; concurrency modes - redb's `Database` is process-lifetime; concurrency modes
(`ExclusiveWriter` default, `SingleWriter`, `MultiWriter`) only matter (`ExclusiveWriter` default, `SingleWriter`, `MultiWriter`) only matter
@@ -208,7 +208,7 @@ go up.
- **Anti-recommendation adopted (ADR-007, 2026-10-02): redb stays - **Anti-recommendation adopted (ADR-007, 2026-10-02): redb stays
ruled out** — recorded in the decision so the question does not ruled out** — recorded in the decision so the question does not
reopen implicitly. SQLite's engine pin now has four-way triangulated reopen implicitly. SQLite's engine pin now has four-way triangulated
evidence (POC #3 fs, POC #4/#5 pg, POC #6 redb, plus the lineage evidence (POC #3 fs, POC #5 pg, POC #6 redb, plus the lineage
citation), and ADR-007 ships the kv tier as sqlite + postgres. If a citation), and ADR-007 ships the kv tier as sqlite + postgres. If a
future consumer wanted redb-shaped durability trade-offs (ephemeral future consumer wanted redb-shaped durability trade-offs (ephemeral
cache tier), that's a *different* tier than the kv engine slot. cache tier), that's a *different* tier than the kv engine slot.
@@ -222,7 +222,7 @@ go up.
- clippy `-D warnings` clean, fmt clean. - clippy `-D warnings` clean, fmt clean.
- **Honest caveats**: mdraid single dev box whose fdatasync latency - **Honest caveats**: mdraid single dev box whose fdatasync latency
ranged 0.24-30 ms during the session under co-resident load (pg ranged 0.24-30 ms during the session under co-resident load (pg
container churn from POC #4 ran through the first bench; stopped container churn from POC #5 ran through the first bench; stopped
before the final redbdiag re-run — relative conclusions unaffected, before the final redbdiag re-run — relative conclusions unaffected,
absolute redb-Immediate numbers are *device-pressure-dependent* and absolute redb-Immediate numbers are *device-pressure-dependent* and
the honest statement is "1 fdatasync per commit, cost = device the honest statement is "1 fdatasync per commit, cost = device
@@ -237,4 +237,4 @@ go up.
`redbdiag2.rs` (None + periodic-flush group-commit emulation), `redbdiag2.rs` (None + periodic-flush group-commit emulation),
`redbdiag3.rs` (commit-vs-insert split, per-put file growth), `redbdiag3.rs` (commit-vs-insert split, per-put file growth),
`redbdiag4.rs` (warm distribution), `redb_conc.rs` (scale-out, both `redbdiag4.rs` (warm distribution), `redb_conc.rs` (scale-out, both
durability modes), `sqlite_conc.rs` (copied head-to-head from #4). durability modes), `sqlite_conc.rs` (copied head-to-head from #5).