Expand description
The LFS pointer-lifecycle row store (wave 9) — the durable
reference map ADR-022 §4 places alkgit-side: one row per reference,
(repo_id, oid, size), over the co-tenant lfs_objects table that
ships day-one in both engine step sets (ADR-024 §5 — the table is
unconditional in the schema, the lfs feature gates this surface
consuming it, so the stamp stays unit-shaped). The repo_id is the
registry-resolved id (the ops namespace on the batch’s ACL path);
the oid is the LFS media oid (64 lowercase hex — the raw sha256 of
the content, byte-exact the GitObjectSha256 digest under alkblobs
ADR-018 §1, though the row store itself carries no key arithmetic);
the surface is consumed by the batch adapter (assertion at
check/upload time, per-repo reads) and the LFS liveness source (the
full row read feeding the sweep chain).
§The reference surface (LfsRows)
LfsRows::assert— insert a reference, or the refcount-aware upsert: re-asserting the same(repo_id, oid)pair in one repo is one row (the pair is the primary key; no second row, no refcount bump) and the row’s size refreshes to the asserted value (the oid is the content digest, so honest traffic asserts the same size — the last assertion’s metadata wins).LfsRows::remove— remove one reference row. An absent row is a success no-op (removal is a cleanup op — no error path there).LfsRows::remove_repo— the repo-deletion leg: the repo’s rows go with it. The composition’s record-removal transactions carry the twins’ in-SQL copies of this leg unconditionally (lfs.md §3: rows go with the repo, same removal as refs/registry); the mem shape’s leg rides its record-transaction legs in the composition entry (crate::compose— theMemSeededBackendpattern). The unconditional posture: the table is unconditional in both step sets, the rows only exist when thelfssurface wrote them, and a mixed deployment that turned the feature off must not leak a removed repo’s rows.LfsRows::rows/LfsRows::repo_rows— the liveness read (all rows / per-repo rows, canonical(repo_id, oid)order): the shape the LFS liveness source and the batch adapter consume.
§The refcount semantics (the cross-reference re-protection
contract, ADR-022 §4)
The CAS pool is global — the same content referenced by two repos
is one key with two rows — so the liveness source must re-protect
the key while any row references it (POC-L’s refcount
requirement, replacing the POC’s crude second live-row scan). The
refcount column carries that data:
- What counts: a row’s refcount is the number of rows referencing the row’s oid (itself included) — with the pair primary key, the number of distinct repos referencing the oid.
- What bumps it: exactly one event — a newly asserted row
(a previously-unrepresented
(repo_id, oid)pair). The refcount-aware upsert (a same-pair re-assert) changes nothing. - What decrements it: exactly one event — a removed row
(
LfsRows::remove, or the repo-deletion legs). Every surviving row of the oid carries the recomputed count, so every row of an oid reads the same current count whatever the engine. - What reads it: the liveness reads ([
LfsRow::refcount) — consumed by the wave-9 liveness source (the re-protection contract’s data: while a key’s count is ≥ 1 the key stays referenced; the last row’s removal makes the media sweep-eligible) and the OQ-20 deferred per-oid pruning’s future queries.
The engine twins maintain the column transactionally: one
transaction per call (the refs-rows posture, ADR-019 §2 — the
sqlite twin’s BEGIN IMMEDIATE, the pg twin’s transaction and
its schema advisory lock, the sqlite write-lock class’s analog
serializing the same-oid recompute across co-tenant connections),
recompute-after-write so concurrent mutations converge on exact
counts. The in-memory impl recomputes from the row map —
exact-by-construction.
§Repo-lifetime scoping v1
Rows live from assertion (batch check/upload — the moment the server learns the pair) to repo deletion; no per-oid pruning — OQ-20 stands deferred (first concrete row-volume need). There is no sweep, no TTL, and no mutation of rows anywhere outside this surface’s ops and the two removal legs.
§Errors
The StorageError family (the refs tables’ posture — the
co-tenant row stores speak the core’s object-storage errors):
table-layer input validation is StorageError::Invalid (a
non-64-hex-lowercase oid, an empty repo id, a size over the
engine’s integer range), engine failures are
StorageError::Io (message-only — no engine source types on
the surface).
Structs§
- InMemory
LfsRows - The in-memory LFS row store — the semantically exact reference
impl, engine-free and single-process in the crate’s base (the
refs-tables’ pattern: no engine dependency, no feature beyond the
lfsgate this surface carries — ADR-025’s placement rule). Rows live in a(repo_id, oid) → sizesorted map (canonical order falls out of the BTreeMap); the refcounts recompute from the map on every mutation (exact-by-construction). The whole call runs under one short-held write lock (the in-memory “transaction”; the asynctokio::sync::RwLock) over both maps — the rows and the refcounts mutate in the same locked section, so a concurrent pair of same-oid mutations serializes with exact recomputes (a two-lock split would let interleaved phases land in reversed order: a stale one-off undercount — the wave-9 gate’s fix). - LfsRow
- One LFS pointer-lifecycle row as the surface reads it back: the resolved repo id, the 64-hex LFS oid, the asserted size, and the oid’s current refcount (the module doc’s refcount semantics).
- Sqlite
LfsRows - The sqlite row twin over the co-tenant
lfs_objectsrows (substrate.md §refs’ row-store pattern, ADR-019 §2): every write call (LfsRows::assert,LfsRows::remove,LfsRows::remove_repo) runs its check-and-writes inside oneBEGIN IMMEDIATEtransaction on the co-tenant connection — two clashing calls (cross-connection included) serialize on the write lock and the refcount recompute reads the committed winner. The refcount recompute is the transaction’s trailing statement — the touched oid’s rows all carry the same current count at commit. Blocking SQL bridges off the async executor (tokio::task::spawn_blocking) per the composition’s concurrency model; construction takes the already-migrated (CoTenantSqlite::open) handle — no construction I/O here. The record-removal leg that takes a repo’s rows with the record rides the registry twin’s own in-SQL copy (crate::sqlite— unconditional, see the module doc); this impl’sLfsRows::remove_repois the surface’s own leg for the same row set. - Versioned
LfsRows - The LFS rows’ write counter — the LFS liveness source’s in-process
invalidation half (
crate::liveness::LfsMedia’s arbitration epoch signal): bumped on everyassertthrough the composition’s rows surface, read by the source’s arbitration consult. SeeVersionedLfsRows.
Traits§
- LfsRows
- The LFS pointer-lifecycle row surface (substrate.md §public API’s
“the LFS liveness/pointer-lifecycle surface under
lfs” — the row-op half; the exact names this module pins are the OQ-03 substrate-side inventory of the wave-9 rows task). Three impls in the refs-tables’ pattern:InMemoryLfsRows(the reference impl, engine-free), the sqlite twin (SqliteLfsRows, thesqlitefeature), and the postgres twin ([PgLfsRows], thepostgres+lfsfeatures).