ADR-010 (new): the Backend trait's I/O seams pinned before any code — 'get' returns a crate-internal read cursor (kv engines materialize; 'local' pread loop; pg-lo lo_get windows), 'put' has two named forms (whole-value kv / staged-put large); GC state is store-core-owned via a crate-internal contract-tested engine-state seam on the SQL-backed engines (backends never learn liveness — ADR-005 verbatim); the store core holds the joint entry+pin transaction (corrects ADR-009 §2's ownership statement); kv-tier fleet-validity rule added (mirror of ADR-008 §3); ops pin-token TTL race resolved (the token IS the pin; renewal rides have/re-put; pause-past-TTL falls to delete-then-recover); feature graph pinned (pg-lo does not imply postgres). ADR-011 (new): vocabulary pinned — tier (contract, exactly two) vs engine (concrete impl, exactly five); store instance / node / fleet split (fleet = pool-sharing, not node count — fixes requirements.md's self-contradiction with REQ-2); mem demoted from 'backend'/'testing tier' to the kv tier's third engine (contract-reference engine); fs tier renamed 'large' including the feature name (last free moment before code exists); constructor modes pinned (dual-tier default, kv-only, mem-only) plus the composite fleet-validity predicate. Consistency round across all specs and ADR-003/004/007/008/009: stale pg-lo passages resolved per ADR-009's supersession; ADR-008 owner 'node id' → store-instance id; threshold wording corrected (conservative edge, not midpoint, of the 128-256 KiB crossover zone); SweepReport shape specced; mem classification unified; 008/009 ADR files renamed to match the tier rename; README deferral-policy recap aligned (third category = decided-but-sequenced work, not a parking kind); POC crate list completed. Verification: two independent architecture review rounds (the first found 4 criticals — unpinned trait I/O shapes, missing fleet-state seam, node/fleet self-contradiction, pin-token/TTL conflict — all resolved; final round: zero criticals); all markdown links resolve; ADR tables complete (11 ADRs).
156 lines
8.2 KiB
Markdown
156 lines
8.2 KiB
Markdown
---
|
|
status: draft
|
|
last_updated: 2026-10-03 (ADR-010 state-home rule + ADR-011 instance/fleet vocabulary)
|
|
---
|
|
|
|
# Pooling, namespaces, and GC
|
|
|
|
## What this is
|
|
|
|
The layer that makes the store *pooled*: one content-addressed pool per
|
|
node, dedup across every consumer by construction, with garbage
|
|
collection that is safe against concurrent writes. The pool property is
|
|
the reason both demanding consumers exist (overview.md); GC is its
|
|
price, and this document specifies the mechanism the POCs validated
|
|
(ADR-005).
|
|
|
|
## The pooled CAS
|
|
|
|
- **One pool per deployment; a store instance is one GC arbitration
|
|
domain** (the engine/vocabulary definitions live in requirements.md
|
|
/ ADR-011: a node — one process — may host several instances; the
|
|
instance is where pins and arbitration live for single-instance
|
|
topologies). Repos, workspaces, and appfile stores are all
|
|
*sets of hash references* over the same CAS — never walled-off
|
|
per-repo stores. Git's `alternates`/object-pool mechanism (GitLab
|
|
object pools) is the same idea done awkwardly; the pool generalizes it
|
|
to unrelated repos and to p2p replication. **A fleet (REQ-2) — two
|
|
or more store instances sharing one pool — is the one topology where
|
|
the GC mechanism below extends beyond one instance: ADR-008 pins,
|
|
sweeper locking, and delete arbitration move into the shared engine;
|
|
the invariants are unchanged. Where this crate's GC state (fleet
|
|
rows and locks) physically lives, and who holds the joint
|
|
entry+pin transaction, is ruled by ADR-010 §2: the store core owns
|
|
all GC state; SQL-backed engines host it through a crate-internal,
|
|
contract-tested seam; backends never learn liveness.**
|
|
- **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
|
|
a workspace manifest — cross-consumer dedup is free by construction,
|
|
not a feature.
|
|
- **Physically flat, logically namespaced** (the rudolfs inversion).
|
|
The byte layer is `hash → bytes`, backends namespace-blind; the
|
|
namespace is a *reference set* above the crate:
|
|
|
|
## Namespaces (logical, above the pool)
|
|
|
|
- A namespace is `namespace → set of root hashes` held by the consumer
|
|
(git refs *are* the names; an LFS pointer file in a tree *is* the
|
|
reference; an alkfs path tree *is* the manifest). The store never
|
|
walks consumer-side manifests — it stays structure-blind.
|
|
- The store-side counterpart is the **liveness seam**: consumers
|
|
register live-shared liveness sources (ADR-005, §Decision —
|
|
`register_liveness_source`; the POC's copy-semantics failure finding 7
|
|
is the pinning reason). Namespace tables may also exist purely
|
|
in-memory for ephemeral consumers; nothing in the store persists
|
|
namespaces — where roots live when a deployment has no SQL engine (no
|
|
kv engine with rows) is a
|
|
non-question by design: roots live with the consumer, whose references
|
|
are as durable as they need to be.
|
|
- ACL mapping for network ops: the namespace is the resource
|
|
(`resource_id_path` selects it) — see ops-surface.md.
|
|
|
|
## Mark-and-sweep GC (ADR-005)
|
|
|
|
Validated mechanism (POC #1 findings 3/7/8 — exact sweep counts, abort
|
|
semantics, delete-then-recover as byte-identical re-put):
|
|
|
|
- **Liveness sources (roots):**
|
|
1. consumer-registered liveness sources (live-shared, above),
|
|
2. put-path **pins** (RAII; batch scope available for multi-put
|
|
writes — manifest writes are exactly this),
|
|
3. the **protect callback** consulted before each sweep: it may add
|
|
externally-known hashes or **abort the run** (`GcAborted`; a
|
|
flaky protection source skips the sweep rather than risk
|
|
deletion — iroh's `ProtectOutcome::Abort` conclusion, adopted).
|
|
- **Sweep:** enumerate the whole pool (backends' complete `list()`,
|
|
ADR-003), compute the live set, batch-delete the dead
|
|
(batch-sized, ~100/batch, iroh's proven shape). Delete-then-recover
|
|
is a re-put — byte-identical under the same key (validated).
|
|
- **Traversal ownership: lean.** Liveness computation beyond "these
|
|
roots exist" is the consumer's job, handed over via the seam. The
|
|
store never learns manifest formats. If a real consumer's live-set
|
|
computation proves too heavy, store-native traversal is a *new* ADR
|
|
(documented constraint, not a parked hedge — the un-pause condition
|
|
is an external fact: a measured consumer cost).
|
|
|
|
## Delete windows (the sweep-vs-put race)
|
|
|
|
The race (POC #1 finding 3): a pin added between sweep-"list" and
|
|
sweep-"delete" is a lost pin — a blob deleted while in flight. The
|
|
architecture resolves it as a **delete window** with three phases —
|
|
mark (enumerate + live-set), per-key arbitration under the
|
|
pin/liveness lock *at delete time*, batched commit — resting on two
|
|
ordering invariants: **put commits pin before publish**, and **deletes
|
|
arbitrate under the pin lock at delete time** (full protocol + proof:
|
|
ADR-005 §Sweep; iroh `delete_set.rs`'s ProtectHandle/protect-cancel
|
|
shape and its serialized-actor pattern are the re-borrowed prior art —
|
|
their serialized actor is exactly "arbitrate at delete time"; the
|
|
window batches the arbitration).
|
|
|
|
Test-asserted property: **a visible pool entry is never deleted while
|
|
liveness (pin or registered source) protects it, and an in-flight put
|
|
is never deleted by a sweep started before it committed.** Direct
|
|
`delete(key)` refuses pinned keys (typed error; same arbitration).
|
|
|
|
`has()`/`get()` during a window observe either the old or the new
|
|
state; there is no torn observation (the pool's entries are immutable;
|
|
existence flips atomically per key).
|
|
|
|
## GC scheduling and scope (ADR-005)
|
|
|
|
- **Explicit `sweep()`; embedder owns cadence.** No background timers
|
|
in the crate. An embedder that wants interval sweeps adds them above
|
|
(one call). Consumer-driven sweeps (after a manifest prune, say) are
|
|
the same API.
|
|
- **Sweep scoping:** whole-pool is the base. Namespace-scoped sweeps
|
|
run by computing a narrower live set from that namespace's liveness
|
|
source; the pool remains shared (scoping is a liveness computation
|
|
choice, not a physical partition).
|
|
- **Accounting** (per-namespace sizes): computed from reference sets by
|
|
consumers; the store reports pool-level totals only (`list` +
|
|
`stat`). Physical-layer accounting would re-introduce namespace
|
|
physics — rejected with the per-namespace engine config
|
|
(ADR-003 §Consequences).
|
|
|
|
## Design Decisions
|
|
|
|
| ADR | Decision | Summary |
|
|
|---|---|---|
|
|
| [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 |
|
|
| [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-large-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 |
|
|
| [010](decisions/010-backend-io-seams-and-fleet-state-home.md) | GC-state home | the store core owns all GC state; SQL engines host it via the contract-tested engine-state seam; pin-tx ownership ruled |
|
|
|
|
## Open Questions
|
|
|
|
None owned by this document. Shared-pool GC coordination within a
|
|
deployment is now in-crate (fleet mechanism: ADR-008). What remains a
|
|
replicator-policy-layer property (above the crate) is *cross-pool*
|
|
coordination — gossip sync between separately-pooled replicator nodes
|
|
deciding what each keeps — governed by the same seam; it raises a
|
|
question here only when a consumer *names a requirement*
|
|
(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
|
|
|
|
- `docs/research/phase-0.md` OQ-BL-05 (settled decisions + residuals)
|
|
- `docs/research/poc-trait-dispatch-findings.md` findings 3/7/8
|
|
- `docs/research/iroh-blobs-eval.md` — gc.rs / delete_set.rs reads
|
|
(the re-borrowed conclusions)
|
|
- rudolfs — the physical-namespace anti-pattern (inverted)
|
|
- gix-odb — alternates/pool prior art for the p2p case
|
|
- ADR-005; [store-api.md](store-api.md) (Pin semantics, sweep API);
|
|
[ops-surface.md](ops-surface.md) (namespace-as-resource ACL) |