Skip to main content

Module lfs

Module lfs 

Source
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 — the MemSeededBackend pattern). The unconditional posture: the table is unconditional in both step sets, the rows only exist when the lfs surface 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§

InMemoryLfsRows
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 lfs gate this surface carries — ADR-025’s placement rule). Rows live in a (repo_id, oid) → size sorted 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 async tokio::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).
SqliteLfsRows
The sqlite row twin over the co-tenant lfs_objects rows (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 one BEGIN IMMEDIATE transaction 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’s LfsRows::remove_repo is the surface’s own leg for the same row set.
VersionedLfsRows
The LFS rows’ write counter — the LFS liveness source’s in-process invalidation half (crate::liveness::LfsMedia’s arbitration epoch signal): bumped on every assert through the composition’s rows surface, read by the source’s arbitration consult. See VersionedLfsRows.

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, the sqlite feature), and the postgres twin ([PgLfsRows], the postgres + lfs features).