Skip to main content

znippy_plugin_git/
git_ops.rs

1//! **`GitOps` — ours, typed, and base znippy never sees it.**
2//!
3//! §13.2: `ArchiveTypePlugin` is base znippy's contract and
4//! [`crate::NativeGitPlugin`] implements it — base stores, the plugin annotates,
5//! exactly as maven, python, rust-toolchain and media already work. `GitOps` is
6//! the *other* trait: the one gunnar calls, with git's own vocabulary in it.
7//!
8//! §13.3: `ArchiveTypePlugin::run_command(cmd, args)` is **CLI-only string
9//! dispatch** and delegates here. A server's hot path does not go through
10//! stringly-typed dispatch, so nothing below takes a `&str` command or an
11//! `args: &[String]`.
12//!
13//! ```text
14//!   gunnar ──typed──► GitOps  (this file, twelve methods)
15//!                       │
16//!      ┌────────────────┼──────────────────┬─────────────┐
17//!      ▼                ▼                  ▼             ▼
18//!   ArchiveWrite    ObjectIndex          RefLog         Gc
19//!   SafeWriter      ObjectReadStack      push log       NewGeneration
20//!   (blob fsync,    (stree → Arrow       (Arrow IPC     (link, compact,
21//!    journal row)    → redb tail)         frame = tx)    verify, rename)
22//! ```
23//!
24//! Every arm under that diagram already existed and was already measured. This
25//! file adds no storage, no index and no durability mechanism of its own — if a
26//! method here looks like it is inventing one, that is a bug.
27//!
28//! # The one thing that is NOT here
29//!
30//! `run_command` still lives on the plugin in [`crate::lib`], and it now
31//! delegates. Nothing git-shaped went into base znippy for any of this.
32
33use std::collections::{BTreeMap, HashMap};
34use std::fs::File;
35use std::path::{Path, PathBuf};
36use std::sync::{Arc, Mutex, RwLock};
37
38use anyhow::{anyhow, bail, Context, Result};
39use znippy_common::ReservedSection;
40
41use crate::arms::StoreConfig;
42use crate::exploded::ExplodedStats;
43use crate::exploded_cache::ExplodedCache;
44use crate::gc::{Gc, GcReport};
45use crate::graph::{assign_generations, CommitNode};
46use crate::index_layout::{ObjectIndex, OneTableFourColumns};
47use crate::indexer::{IndexJob, ObjectAbsorb, PushPath};
48use crate::object::{GitHashKind, GitObjectKind};
49use crate::pack_walk::PackWalk;
50use crate::reach::ReachEntry;
51use crate::read_stack::{ObjectReadStack, RebuildTriggers};
52use crate::refs::{RefLog, RefUpdate};
53use crate::resolve::BaseSource;
54
55// ── the contract, now owned by `git-storage-trait` ────────────────────────────
56//
57// `GitOps` (the neutral ELEVEN) and its value types live in the
58// `git-storage-trait` crate at this repo's root, so a gix backend
59// (edda/storage-git-gix) can implement the same contract without linking Arrow
60// or OpenZL. The twelfth method, `seal()`, returns `znippy_common::
61// ReservedSection` (an Arrow type a gix backend has no analog for), so it is an
62// **inherent** method on [`GitStore`] / [`SelectedStore`], not part of the
63// trait. The re-exports below keep every existing path
64// (`znippy_plugin_git::git_ops::{GitOps, Oid, …}`) alive.
65pub use git_storage_trait::{Extent, GitOps, Oid, RefRow, Stored, TxId};
66
67
68// ── the batch shape, which is measured and must not be papered over ───────────
69
70/// Which path a lookup of `n` oids takes.
71///
72/// The number this dispatch was written for is `index_layout_bench`'s: on the bare
73/// Arrow arms, `lookup_batch` with a **single** element costs 818 ns against
74/// `lookup`'s 482 ns — 1.7x worse — because the batch path allocates a result
75/// vector, a key vector and a second pass over them to serve one oid.
76///
77/// **RE-MEASURED against the shipping read stack, and the magnitude does not
78/// carry over.** oden 2026-08-07, release, 1-min loadavg 4.94 (another tenant's
79/// work — see LAW: never measure on a busy box, and read these as a ratio rather
80/// than as absolutes), `ObjectReadStack<OneTableFourColumns>` over a real
81/// repository's 2687-object pack, 20 000 iterations
82/// ([`crate::store::tests::the_batch_of_one_really_is_slower_than_the_serial_path`]):
83///
84/// | call | per oid |
85/// |---|---:|
86/// | `lookup` | **628 ns** |
87/// | `lookup_batch` with 1 oid | **639 ns** — 1.02x worse |
88/// | `lookup_batch` with 100 oids | **161 ns** — 3.9x better |
89///
90/// So: the *direction* holds and the dispatch is right — a batch of one is never
91/// faster — but on this stack at this size it costs 2%, not 70%. The real finding
92/// is the third row: the batch path is worth **3.9x** at
93/// [`BATCH_SATURATES_AT`], which is a much stronger reason to keep the two paths
94/// apart than the batch-of-one penalty ever was. Both numbers are stated because
95/// quoting only the 1.7x would be quoting a figure this stack does not reproduce.
96///
97/// The dispatch is a named function with its own guard precisely so it cannot
98/// quietly become `lookup_batch` for every n.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum LookupPath {
101    /// One oid: `ObjectIndex::lookup`.
102    Serial,
103    /// Two or more: `ObjectIndex::lookup_batch`, one pass.
104    Batch,
105}
106
107/// Oids at which the batch path stops getting faster. Reported, not enforced:
108/// splitting a bigger batch would cost a pass and buy nothing.
109pub const BATCH_SATURATES_AT: usize = 100;
110
111/// The dispatch, in one place.
112pub fn lookup_path(n: usize) -> LookupPath {
113    if n == 1 {
114        LookupPath::Serial
115    } else {
116        LookupPath::Batch
117    }
118}
119
120// ── the store ─────────────────────────────────────────────────────────────────
121
122/// A pack whose bytes are durable but whose objects the index has not absorbed.
123///
124/// §13.12's `indexed` bit, in the form this store needs it: membership in
125/// [`PackState::pending`] *is* the bit being clear. A read that arrives while it
126/// is non-empty falls back to absorbing it — slower, never wrong.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128struct PendingPack {
129    pack_id: u64,
130    offset: u64,
131    len: u64,
132    /// The journal queued it on open (a restart), not a push in this process.
133    /// What makes the progress row say `resumed 0%` after a reboot.
134    resumed: bool,
135}
136
137impl PendingPack {
138    fn from_job(j: IndexJob) -> Self {
139        Self {
140            pack_id: j.pack_id,
141            offset: j.offset,
142            len: j.len,
143            resumed: false,
144        }
145    }
146}
147
148/// **§13.12's `indexed` bit — one bit per pack, indexed by pack ordinal.**
149///
150/// A plain bitset, and the two rejected alternatives say why:
151///
152/// * **not a hash set.** Pack ordinals are dense and small: they are assigned
153///   `0..N` within **one repository** (§13.14 scopes a negotiation, and therefore
154///   this store, to one repository — account-wide they would be sparse and the
155///   array would grow with the account's total packs). Dense means the bit is one
156///   shift and one AND with no hashing, and 10 000 packs is **1250 bytes** of
157///   `words`, which lives in L1.
158/// * **not a bloom filter.** A bloom's false positive would be *actively wrong*
159///   here: it would report a pack absorbed when it is not, the read path would
160///   stop falling back, and negotiation would be told `absent` about an object
161///   whose bytes are durable — which makes a client withhold objects and lose
162///   data. The bitset has no such mode. `1` means absorbed, `0` means fall back,
163///   and both are exact.
164///
165/// **The bit is never stored.** It is derived on open by diffing two facts that
166/// are already durable — the journal's extents against the index's rows — in
167/// [`Absorber::adopt_journal`]. There is nothing for a derived bit to drift from.
168///
169/// **Why the extents sit beside it.** A `0` bit is only actionable with the
170/// extent to absorb, so `extent[i]` carries it; `None` means "this store has not
171/// been told about ordinal `i`", which is not the same as "not absorbed" and must
172/// not be counted as work.
173///
174/// **Why `absorbed` is a separate fact from `extent`.** The ack path does two
175/// things in this order: record the pack pending, then hand its extent to the
176/// account's channel. The indexer's worker is a different thread and can reach
177/// `absorb` *before* the first of those returns. With only a pending list, that
178/// race ends in a bit that is set after the rows are already in and is never
179/// cleared, so every later read pays a pointless fallback for the life of the
180/// process. The bit is what the two writers agree on: [`PackState::note`] will
181/// not queue a pack that is already done, and `absorb` will not do the work
182/// twice.
183///
184/// Touched only under short critical sections — never across an absorb, which is
185/// what the separate [`Absorber::gate`] is for. **The ack path must never block
186/// behind index work.**
187#[derive(Default)]
188struct PackState {
189    /// One bit per pack ordinal, 64 to a word. `1` = its rows are in the index.
190    words: Vec<u64>,
191    /// The extent of each known ordinal, so a `0` bit can be acted on.
192    extent: Vec<Option<Extent>>,
193    /// Known ordinals whose bit is `0` — what [`GitStore::unindexed_packs`]
194    /// reports, kept as a count so the read path's check is a field load rather
195    /// than a scan.
196    unabsorbed: usize,
197}
198
199impl PackState {
200    /// The bit.
201    fn is_absorbed(&self, pack_id: u64) -> bool {
202        let i = pack_id as usize;
203        self.words
204            .get(i / 64)
205            .is_some_and(|w| w >> (i % 64) & 1 == 1)
206    }
207
208    /// Record a durable pack whose rows are not in yet. The bit **going clear**.
209    ///
210    /// **Idempotent, and the extent slot is the single thing that makes it so.**
211    /// A second `note` for an ordinal that already has one — the ack path
212    /// arriving after the drain has already absorbed the pack, or a re-queue on
213    /// open racing the same pack's push — finds the slot filled and counts no new
214    /// work. [`PackState::mark_absorbed`] fills that same slot, so a pack that is
215    /// already done cannot be queued behind its own completion, and there is no
216    /// second condition here that could disagree with the first.
217    fn note(&mut self, pack_id: u64, extent: Extent) {
218        let i = pack_id as usize;
219        self.grow_to(i);
220        if self.extent[i].is_none() {
221            self.extent[i] = Some(extent);
222            self.unabsorbed += 1;
223        }
224    }
225
226    /// Room for ordinal `i` in both arrays. Dense ordinals are what makes this a
227    /// `resize` and not an insert.
228    fn grow_to(&mut self, i: usize) {
229        if self.words.len() <= i / 64 {
230            self.words.resize(i / 64 + 1, 0);
231        }
232        if self.extent.len() <= i {
233            self.extent.resize(i + 1, None);
234        }
235    }
236
237    /// The bit **going up**: this pack's rows are in the index.
238    fn mark_absorbed(&mut self, pack_id: u64, extent: Extent) {
239        let i = pack_id as usize;
240        self.grow_to(i);
241        if self.words[i / 64] >> (i % 64) & 1 == 0 {
242            self.words[i / 64] |= 1 << (i % 64);
243            if self.extent[i].is_some() {
244                self.unabsorbed -= 1;
245            }
246        }
247        self.extent[i] = Some(extent);
248    }
249
250    /// Every known pack whose bit is clear, in ordinal order.
251    fn pending(&self) -> Vec<PendingPack> {
252        self.extent
253            .iter()
254            .enumerate()
255            .filter(|(i, e)| e.is_some() && !self.is_absorbed(*i as u64))
256            .map(|(i, e)| {
257                let (offset, len) = e.expect("filtered on Some");
258                PendingPack {
259                    pack_id: i as u64,
260                    offset,
261                    len,
262                    resumed: true,
263                }
264            })
265            .collect()
266    }
267}
268
269/// The derived tables: the commit graph, the tree payloads the reachability
270/// bitmaps are built from, and this store's own ordinal space.
271///
272/// **The ordinal space is this struct's, not the projection's.** An
273/// [`crate::index_layout::IndexRow::ordinal`] is a row address inside one
274/// projection generation and is re-derived by every rebuild, so a bitmap built
275/// over projection ordinals would silently address different objects after any
276/// rebuild. The bitmaps here are over `ordinal`, which is rebuilt in the same
277/// fold that rebuilds them — the two cannot drift because one call produces both.
278/// **Nothing in here is a source of truth.** Every field is folded from the
279/// `objects` table and §14's exploded table by [`Absorber::refold`], and both of
280/// those are on disk — which is what makes a clean reopen come back with the
281/// same graph it shut down with. Before the exploded table existed, `graph` and
282/// `trees` were *accumulated* here as packs were absorbed and stored nowhere, so
283/// a clean shutdown lost them silently: MEASURED on oden 2026-08-08, a reopened
284/// store over a fully absorbed 2687-object pack had all 2687 rows and **0 of 551
285/// commits**, with `reachable()` returning a commit alone instead of its closure
286/// and `gc` seeing an empty live set.
287#[derive(Default)]
288struct Derived {
289    /// Parents-before-children, generations assigned. Folded from the exploded
290    /// table's commit payloads.
291    graph: Vec<CommitNode>,
292    // **`trees` is GONE (lane Q, 2026-08-30).** It held every tree payload in
293    // the repository, resident, keyed by a hex `String`, and was rebuilt from
294    // the exploded table on every fold. MEASURED on the real `torvalds/linux`
295    // mirror: 28.24 GB of tree payloads, 3.24× the 8.13 GiB pack, kept eagerly
296    // on disk so this map could be rebuilt into a 50.9 GiB RSS — on a box that
297    // ships with 16 GB. `crate::reach::TreeSource` replaced it with a lookup
298    // ([`Absorber::tree_source`]), so a tree is read the way every other object
299    // is: from the cache if it is there, from its own delta chain in the
300    // mmapped pack if it is not.
301    /// **The ordinal space, and the ONLY copy of it: every distinct oid in the
302    /// store, raw, fixed-width, back to back, in oid-lexicographic order.**
303    ///
304    /// Ordinal `o` is the slice `oids_raw[o * oid_len .. (o + 1) * oid_len]`, and
305    /// because the buffer is **sorted** the inverse (oid → ordinal) is a binary
306    /// search over it — [`crate::reach::OrdinalSpace`] is that pair of lookups
307    /// and owns nothing but a borrow of this field.
308    ///
309    /// # Why this is now the only representation
310    ///
311    /// It used to be one of THREE. `refold` also built `oids: Vec<String>` (the
312    /// same list, hex) and `ordinal: HashMap<String, u32>` (the inverse, hex
313    /// keys) — a 40-byte hex `String` per oid in the vector, a second as each
314    /// map key, plus 20 raw bytes here. MEASURED, lane MEMORY, oden 2026-08-31
315    /// (`examples/rss_per_open_store`): a cached open store cost **382 bytes of
316    /// live RSS per indexed object** on a blob-only corpus and **744–966 B** on
317    /// the two real repository packs — about half of it that pair. And it was
318    /// paid twice at every fold: REFOLD measured the hex `Vec` at ~0.13 s and
319    /// the `HashMap` at ~0.08 s of a 136k-object reopen, ~0.21 s spent turning
320    /// bytes the tail already held into `String`-keyed tables.
321    ///
322    /// The flat buffer already answered both questions — a slice and a binary
323    /// search — so the two `String`-per-oid tables were LAW 5's exact shape: a
324    /// second and third spelling of one list that a comment had to *promise*
325    /// agreed. They are gone; the serve path reads oid ⇄ ordinal straight off
326    /// this buffer. See `gunnar/.nornir/stinky.md` S-082.
327    ///
328    /// # Why raw rather than hex
329    ///
330    /// Because every *serving* answer is built out of raw bytes.
331    /// `reachable_raw` answers in oid bytes and each object is a
332    /// `copy_from_slice` of a slice of this buffer — nothing is encoded,
333    /// decoded or allocated per object. `reachable_oids` (the maintenance path)
334    /// encodes on the way out, at one `String` per object, and nothing on the
335    /// serve path calls it. The tail hands oids in oid order, which over
336    /// fixed-width oids is byte order, so this is already the sorted
337    /// concatenation with no sort of its own — see [`Absorber::refold`].
338    oids_raw: Vec<u8>,
339    /// The bitmaps, or empty if nothing has needed them since the last fold.
340    ///
341    /// **Behind an `Arc` since 2026-08-14, and that is a measurement and not a
342    /// tidy-up.** [`Absorber::reach_bitmaps`] used to hand out `d.reach.clone()`
343    /// — a deep copy of every commit's `RoaringBitmap` plus its oid `String` —
344    /// and `crate::serve::GitServe::select` calls
345    /// [`GitStore::reachable_oids`] **twice** per request (once for `want`, once
346    /// to close over `have`). MEASURED on oden with `stage-probe`, the
347    /// znippy-served `nornir` mirror (12 455 objects), a 4-object negotiated
348    /// fetch: the `select.bitmap` stage was **75.2 % of the request's CPU** and
349    /// **645 876 allocations**; at four requests sharing one warm store it was
350    /// still 62 762 allocations *per request*. None of that copying is work the
351    /// answer depends on — the table is immutable between folds — so the `Arc`
352    /// removes it rather than dividing it across cores.
353    reach: Arc<Vec<ReachEntry>>,
354    /// Every commit's oid as **raw bytes**, the form
355    /// [`crate::serve::GitServe::select`] tests a `want` against.
356    ///
357    /// `None` means a graph row's oid did not parse as hex, which `select`
358    /// answers by declining the whole request. That check is why this is
359    /// `Option` rather than a plain set: it used to run **per request** —
360    /// `graph_snapshot()` cloned the whole `Vec<CommitNode>` and then
361    /// `hex::decode`d every commit into a fresh `Vec<u8>` — and the answer
362    /// cannot change between folds, so it is folded once here and borrowed
363    /// after. The refusal is preserved exactly; only the arithmetic moved.
364    commit_raw: Option<Arc<std::collections::HashSet<Vec<u8>>>>,
365    /// **hex oid → index into [`Derived::graph`].** The graph-by-oid lookup
366    /// [`crate::reach::accumulate`]'s walk needs, folded once by the same call
367    /// that folds `graph` and `commit_raw` rather than rebuilt on every
368    /// negotiation. `reachable` used to `d.graph.iter().map(...).collect()` a
369    /// `HashMap<&str,&CommitNode>` per call — O(commits) per fetch, on every
370    /// fetch and clone — for a table that cannot change between folds. MEASURED
371    /// on oden 2026-09-01 (`bench_reachable_negotiation`, a 20 000-commit linear
372    /// history): a negotiation over a one-object closure spent **895 µs**
373    /// building this map and the reach one below, work the answer never used.
374    /// It is keyed by hex, not raw, because the walk keys by hex — parents and
375    /// tree oids in a [`CommitNode`] are hex — so a raw key would force a decode
376    /// per walked edge to save the O(w+h) tip encode, a bad trade at any size.
377    graph_index: HashMap<String, u32>,
378    /// **hex commit oid → index into the [`Derived::reach`] table**, built by
379    /// the same call that builds `reach` and cleared with it, so the pair is
380    /// always in step. Replaces the per-call `HashMap<&str,&RoaringBitmap>`
381    /// `reachable` rebuilt over every bitmap on every request. Behind an `Arc`
382    /// for the same reason `reach` is: it is handed to the negotiation with a
383    /// refcount bump, never a copy.
384    reach_index: Arc<HashMap<String, u32>>,
385    /// How many commits the graph held when [`Derived::reach`] was last built
386    /// from scratch — the denominator of the carry rule in
387    /// [`Absorber::refold`]. Bitmaps carried across a fold are exact for the
388    /// commits they cover, but every commit added since is served by walking
389    /// down to the nearest carried one, so once the additions outgrow
390    /// [`REACH_CARRY_MAX`] of this the next reader rebuilds the table whole.
391    reach_full_len: usize,
392    /// Verbatim packs, so any absorbed object's content can be re-derived from
393    /// the truth on demand. `(archive offset, len)`.
394    packs: Vec<(u64, u64)>,
395}
396
397/// **The carry rule's ceiling**: a fold carries the reachability bitmaps
398/// across when the graph has grown by at most this fraction of the commits the
399/// last full build covered (and never fewer than 64 commits' worth). Beyond it
400/// the table is rebuilt from scratch on the next read — which is what every
401/// fold used to do, unconditionally.
402///
403/// MEASURED 2026-09-05 on oden (`gunnar serve`, a 10 000-commit / 51 023-object
404/// repository over ssh): the FIRST clone after any push took 10.2 s and the next
405/// 1.3 s, and after one more 4-object push the pattern repeated (10.9 s, then
406/// 1.0 s). The fold had dropped `Derived::reach` and the next `reachable`
407/// rebuilt every commit's tree closure — 0.2 ms per object of server CPU for a
408/// push that changed nothing about any existing commit's reachability. A bitmap
409/// is a fact about an immutable commit; only its ordinal numbering moves when
410/// the sorted oid space grows, and a monotone remap costs one pass over the
411/// bits. Sized at a tenth so the amortised rebuild stays bounded by the pushes
412/// that earned it, like the persisted index's own fold threshold.
413///
414/// MEASURED AGAIN with the carry in place, same box, same repository, same
415/// sequence (seed push, clone, clone, 4-object push, clone, clone): 10.5 s,
416/// 1.15 s, then **1.12 s** and 1.15 s. The server logged the fold carrying all
417/// 501 sampled entries, and the one 9.3 s build from scratch is the first
418/// clone's, which no carry can serve. `reach_source_stats` is the same fact for
419/// a test to assert.
420pub(crate) const REACH_CARRY_MAX_FRACTION: usize = 10;
421/// The floor under the rule, in commits: a repository small enough that a tenth
422/// of it is a handful still carries across pushes of up to this many commits.
423pub(crate) const REACH_CARRY_MIN_COMMITS: usize = 64;
424
425/// **The object-level index, and the one thing that writes into it.**
426///
427/// Its own allocation rather than a field group inside [`GitStore`], and §13.9-12
428/// is the only reason: the drain runs on the account indexer's worker thread, so
429/// whatever it absorbs into cannot be reachable solely through a `&GitStore`.
430/// Splitting it out is what lets the background worker and a falling-back read
431/// call **the same** absorb instead of two copies of it (LAW 5) — the alternative
432/// was a second implementation on the worker side, which is precisely the
433/// twinning that law forbids.
434///
435/// It is a **leaf**: nothing in here points back at the store, so the worker's
436/// `Arc` closes no cycle and the store still drops (and joins its worker)
437/// normally.
438/// Entries below which [`GitStore::resolve_emit_payloads`] does **not** fan out.
439///
440/// A `std::thread::scope` spawn is ~10–20 µs a worker and the per-entry work it
441/// would divide is a bounds check plus a varint; a 100-object fetch would pay
442/// four spawns to save four microseconds of parsing. The threshold is what keeps
443/// `crate::serve`'s "one core's worth per transfer" true for every request that
444/// is not a bulk clone — and a bulk clone is the only request the copy this
445/// replaces was ever measured on.
446const FAN_OUT_AT: usize = 4096;
447
448/// The folded commit graph, cached beside the verbatim pack so a reopen *reads*
449/// it instead of re-deriving it from every commit payload (S-065).
450const GRAPH_CACHE_FILE: &str = "objects.graph";
451/// `objects.graph` framing: 4-byte magic + 1-byte version + 32-byte token, then
452/// the Arrow IPC commit graph.
453const GRAPH_CACHE_MAGIC: &[u8; 4] = b"GRPH";
454const GRAPH_CACHE_HEADER: usize = 4 + 1 + 32;
455
456/// **`objects.ordinal` is no longer written, and an existing one is removed at
457/// open.** It was S-082's memo of the ordinal space — every oid in sorted order,
458/// with a `(epoch, count)` header validated against the redb tail — so that a
459/// cold open could *read* the space instead of scanning the whole redb tail to
460/// rebuild it. Since 2026-09-04 the read stack persists its projection
461/// (`objects.index`) and the sorted oid list IS that index's oid column, mapped;
462/// [`ObjectReadStack::oids_raw`] hands it back with a merge of the few redb rows
463/// since the last fold. The scan the memo spared no longer exists, and a second
464/// file carrying the same bytes under a bespoke header is exactly the kind of
465/// file this store should not have. On the kernel mirror it was 278 964 902 B —
466/// 20.0 B/object, the oid column to the byte. The name is kept only so the
467/// stale file can be found and deleted.
468const ORDINAL_CACHE_FILE: &str = "objects.ordinal";
469
470/// The validity token for a persisted commit graph: blake3 over the sorted raw
471/// oids of **every** object in the store.
472///
473/// It is both necessary and sufficient. The graph is a pure, deterministic
474/// function of the commit payloads, which are content-addressed by their oids,
475/// so an identical object set yields a byte-identical graph — and this hashes
476/// the whole object set, in the one order `refold` already sorts it into, with
477/// no read the fold did not already make. A mismatch (any push, any gc) forces a
478/// recompute; a 256-bit blake3 makes an accidental match of two different object
479/// sets not a case worth engineering against.
480/// **`seen − live`: what a GC may drop.** `seen` is every oid the index held
481/// when the GC started (`oids_in_order`), `live` the reachable set computed
482/// after it. The difference is the dead set, and it is a POSITIVE set on
483/// purpose: `retain(|o| live.contains(o))` would also drop every row that
484/// arrived between the snapshot and the drop — a push in flight, whose refs
485/// are not updated yet — because to `live` those are simply unknown. A row in
486/// neither set is kept. `live` is hex, `seen` raw; the decode happens once
487/// here rather than once per row in the retain predicate.
488pub(crate) fn dead_oids(
489    seen: Vec<Vec<u8>>,
490    live: &std::collections::HashSet<String>,
491) -> std::collections::HashSet<Vec<u8>> {
492    let live_raw: std::collections::HashSet<Vec<u8>> =
493        live.iter().filter_map(|h| hex::decode(h).ok()).collect();
494    seen.into_iter().filter(|o| !live_raw.contains(o)).collect()
495}
496
497fn graph_token(oids_raw: &[u8]) -> [u8; 32] {
498    let mut h = znippy_common::blake3::Hasher::new();
499    h.update(oids_raw);
500    *h.finalize().as_bytes()
501}
502
503/// Commits that get a reachability bitmap on the **live** selection path.
504///
505/// # It was `usize::MAX`, and that was the single most expensive line in a fetch
506///
507/// The comment that stood here said a sampled table *"is for the sealed archive
508/// where the cost is disk"* and that every commit must be bitmapped on the live
509/// path. That was not a preference, it was forced:
510/// [`GitStore::reachable_oids`] had no walk, so a `want` with no bitmap could
511/// only contribute itself — a silent under-send — and `crate::serve`'s `select`
512/// therefore had to refuse unless *"in the graph"* and *"has a bitmap"* were the
513/// same fact.
514///
515/// MEASURED on oden 2026-08-14, one 4-object negotiated fetch out of the
516/// znippy-served `nornir` mirror (1 581 commits, 12 455 objects): the
517/// `select.bitmap` stage was **75 % of the request's CPU**, and **530 218 of its
518/// 575 349 allocations were this build** — which `refold` throws away again on
519/// the next push. The gix arm answered the identical fetch in 6.8 ms because it
520/// *reads* a `.bitmap` instead of building one.
521///
522/// [`crate::reach::accumulate`] is the walk that makes the cap legal, and 512 is
523/// the number [`crate::reach::ReachPolicy::default`] already uses for the sealed
524/// archive — the same figure git's own bitmap selection is of the order of, and
525/// deliberately not a second, different constant to keep in step.
526const LIVE_REACH_COMMITS: usize = 512;
527
528/// [`LIVE_REACH_COMMITS`], or `ZNIPPY_GIT_REACH_COMMITS`.
529///
530/// **An operator knob, not an arm.** It selects no implementation and changes no
531/// byte of any answer: the walk covers whatever the table does not, so the cap
532/// trades build cost against walk cost and nothing else. That property is the
533/// whole subject of
534/// `the_bounded_walk_answers_exactly_what_a_full_bitmap_table_answers`, which
535/// drives this very knob to its two extremes over one store.
536///
537/// Read **once per process** through [`crate::arms::read_env`], so it is
538/// counted with every other environment read this crate makes, and so a
539/// reachability request does not cost a `getenv` — until 2026-08-21 it did,
540/// once per `reach_bitmaps`/`reachable_oids` call, uncounted.
541fn live_reach_policy() -> crate::reach::ReachPolicy {
542    static MAX: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
543    let max_commits = *MAX.get_or_init(|| {
544        crate::arms::read_env(crate::arms::ENV_REACH_COMMITS)
545            .and_then(|v| v.parse::<usize>().ok())
546            .unwrap_or(LIVE_REACH_COMMITS)
547    });
548    crate::reach::ReachPolicy { max_commits }
549}
550
551/// Ceiling on phase-1 workers **per request**.
552///
553/// The serving tier admits ~32 concurrent transfers, and the fan-out primitive
554/// has no reentrancy detection, so an uncapped `gatling_for_each` here is
555/// 32 × ncores threads on a 128-core box. Four is a bound that leaves the
556/// admission gate in charge of the machine.
557const MAX_RESOLVE_WORKERS: usize = 4;
558
559/// Workers for phase 1: [`MAX_RESOLVE_WORKERS`], or `ZNIPPY_GIT_EMIT_WORKERS`.
560///
561/// **An operator knob, not an arm.** It selects no implementation and changes no
562/// byte of the emitted pack — a pack emitted at one worker and at sixteen is
563/// byte-identical, because phase 1 only decides *where each entry's bytes are*
564/// and phase 2 writes them in one serial order either way. `0` is read as "as
565/// many as this box has cores", which is `gatling_for_each`'s own convention.
566///
567/// Read **once per process** through [`crate::arms::read_env`] — see
568/// [`live_reach_policy`] for why; this one was a `getenv` per emitted request
569/// until 2026-08-21.
570fn emit_workers() -> usize {
571    static WORKERS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
572    *WORKERS.get_or_init(|| {
573        crate::arms::read_env(crate::arms::ENV_EMIT_WORKERS)
574            .and_then(|v| v.parse::<usize>().ok())
575            .unwrap_or(MAX_RESOLVE_WORKERS)
576    })
577}
578
579pub(crate) struct Absorber<S: ObjectIndex = OneTableFourColumns> {
580    blobs: PathBuf,
581    /// Read handle on the verbatim packs, for `pread` of an extent.
582    ///
583    /// The **fallback**, since 2026-08-14, not the ordinary path: `map` below
584    /// answers every extent the mapping covers, and this reads the ones past its
585    /// end (a push that landed after the snapshot was taken).
586    ///
587    /// **It is the writer's own open file description** (2026-08-30, lane AH),
588    /// not a second `File::open`: this reads with `pread` at an explicit offset
589    /// and shares no cursor with `append`, so a private descriptor bought a
590    /// kernel object and a page-cache alias per open repository and no
591    /// isolation. See [`crate::archive_write::open_blobs`] and
592    /// `tests/fd_per_store.rs`.
593    reader: Arc<File>,
594    /// The same file, mapped read-only. See [`crate::archive_map`] for why an
595    /// append-only file `gc` never rewrites is safe to map, and for the ~20 % of
596    /// serve cycles the `pread`-per-object shape was spending on copies.
597    map: crate::archive_map::ArchiveMap,
598    hash: GitHashKind,
599    /// The `objects` table: stree → Arrow → redb tail.
600    ///
601    /// `S` is the payload layout the projection is built in
602    /// ([`crate::arms::IndexArm`]) and it is a **type parameter, not a field**:
603    /// this is the hot path, every lookup goes through it, and the whole reason
604    /// the arms exist is to measure the differences between them. A `dyn` here
605    /// would put a virtual call on every single oid.
606    objects: ObjectReadStack<S>,
607    /// **§14's exploded objects table** — oid → resolved type and content, built
608    /// eagerly by this absorb and by nothing else.
609    ///
610    /// Beside the `objects` table rather than inside it because the two have
611    /// different lifetimes: `objects` is the index and losing it loses the
612    /// repository's addressing, while this one is derived, verifiable against the
613    /// verbatim bytes and deletable at any time. They are separate files so that
614    /// "droppable" is a `rm`, not a schema migration.
615    ///
616    /// SWAPPED 2026-08-13 from the redb `ExplodedTable` to **one Arrow IPC
617    /// table**, payload in the row. redb held 16.8 GB of kernel payload in a
618    /// 204 GB file — 12× — and serialised the gatling fan-out behind its one
619    /// writer at 96.8% of a single core. See [`crate::exploded_arrow`].
620    ///
621    /// **BUDGETED AND EVICTABLE since lane Q (2026-08-30)**: the two-pool
622    /// [`ExplodedCache`] — the indexer's eager pool (commits and tags) beside a
623    /// lazy pool the FIRST reader of a blob or tree fills and a byte budget
624    /// empties. The unbounded eager tier it replaces MEASURED **141.7 GiB** for
625    /// the 8.12 GiB `torvalds/linux` pack on the rig, 17× the pack, which is
626    /// more than one appliance holds for all its repositories together.
627    exploded: Arc<ExplodedCache>,
628    derived: RwLock<Derived>,
629    /// How many folds since open read the persisted commit graph off disk vs
630    /// recomputed it from the exploded table. Counters, not clocks — the only
631    /// thing that can say which path a fold took (LAW 2). See
632    /// [`Absorber::graph_source_stats`].
633    graph_read: std::sync::atomic::AtomicU64,
634    graph_recomputed: std::sync::atomic::AtomicU64,
635    /// **Is `objects.graph` read and written at all** ([`crate::arms::ENV_GRAPH_CACHE`],
636    /// read once at open). `false` folds from the exploded table every time and
637    /// leaves no file: a store that serves no reads holds no derived memo.
638    graph_cache: std::sync::atomic::AtomicBool,
639    /// How many folds CARRIED the reachability table across (renumbered, no
640    /// tree opened) vs how many reads had to build it from scratch. The only
641    /// witness that the carry ran — the bytes a carried and a rebuilt table
642    /// answer with are identical by construction (LAW 2). See
643    /// [`Absorber::reach_source_stats`].
644    reach_carried: std::sync::atomic::AtomicU64,
645    reach_rebuilt: std::sync::atomic::AtomicU64,
646    /// Where the last fold's **ordinal space** came from: how many oids straight
647    /// off the persisted index's column, how many off a scan of the redb tail.
648    /// The same shape as [`Self::graph_read`]/[`Self::graph_recomputed`], and for
649    /// the same reason: which source produced the space is not visible in the
650    /// (identical) bytes, only in a counter (LAW 2). See
651    /// [`Absorber::ordinal_source_stats`].
652    ordinal_folded: std::sync::atomic::AtomicU64,
653    ordinal_tail: std::sync::atomic::AtomicU64,
654    packs: Mutex<PackState>,
655    /// Serialises **absorption**, which is long. Held across a whole pack.
656    ///
657    /// Two absorbs of one pack are duplicated *work* and nothing worse: every
658    /// writer under it is keyed by oid — [`ObjectReadStack::append`] swallows an
659    /// identical second row, the exploded table overwrites its own key, and the
660    /// commit graph is folded from that table rather than pushed at, so it cannot
661    /// double. (It could before §14's table existed, when the graph was a `Vec`
662    /// accumulated per absorb; that is why this paragraph used to say the gate
663    /// was load-bearing for correctness and now does not.) The gate is still what
664    /// makes the ordering below hold: **the rows go in, and only then is the bit
665    /// cleared**, so a read either finds the bit still clear (and waits here) or
666    /// finds the rows. Neither order can produce "absent".
667    gate: Mutex<()>,
668    /// **The progress counter** ([`crate::progress`]) a dashboard or a stats
669    /// call reads. One per store, registered process-wide.
670    progress: Arc<crate::progress::IndexProgress>,
671    /// Set by [`GitStore::cancel_index`]: the repository is being deleted. The
672    /// resolver checks it every few thousand objects and aborts the pack; the
673    /// drain skips every pack still queued.
674    cancel: std::sync::atomic::AtomicBool,
675    /// Names the store in log lines: its root directory.
676    label: String,
677    /// The memory watchdog this store's worker pauses on. The process-wide one
678    /// unless a test hands in a fake ([`GitStore::set_mem_watchdog`]).
679    watchdog: RwLock<Arc<crate::memguard::MemWatchdog>>,
680    /// **Pending packs a reader has already scanned**, keyed by pack offset:
681    /// the walk and an oid-sorted row table, built by ONE read-only pass and
682    /// kept until the pack is absorbed. Every later lookup into that pack is
683    /// a binary search; a content read inflates one chain. Without this,
684    /// every `has` during the index window re-resolved the whole pack —
685    /// MEASURED on the rig 2026-08-30: a `push --mirror` of linux stalled
686    /// pre-ack, ~800 ref updates × one 8 GiB resolve each.
687    scanned: Mutex<HashMap<u64, Arc<ScannedPack>>>,
688    /// One scan build at a time: a second reader waits for the first reader's
689    /// pass rather than starting its own. That is waiting on a *reader*, not
690    /// on the indexer.
691    scan_build: Mutex<()>,
692    /// **Packs this store has stopped trying to index**, by ordinal, with how
693    /// many attempts it took to stop and the reason the last one gave.
694    ///
695    /// 🔴 The state this replaces was *no state*. A pack whose absorb failed
696    /// stayed pending, every absorb loop hit it FIRST (they run in ordinal
697    /// order) and aborted on it with `?`, so the packs behind it were never
698    /// reached — and the next push, and the one after that, re-did the same
699    /// doomed resolve and was refused for it. MEASURED on oden 2026-08-30
700    /// (lane AI): one pack that could not be absorbed refused **every** push to
701    /// that repository from then on, 21 of 140, and lane AD's standby re-asked
702    /// for it every 300 s for 21 minutes. Work that cannot succeed has to stop
703    /// and say why; see [`MAX_ABSORB_ATTEMPTS`] and [`Absorber::absorb_each`].
704    unindexable: Mutex<HashMap<u64, Unindexable>>,
705}
706
707/// A pack this store will not try to index again, and why.
708#[derive(Debug, Clone)]
709pub struct Unindexable {
710    /// The pack's ordinal.
711    pub pack_id: u64,
712    /// Its extent in the verbatim archive. The bytes are still there.
713    pub extent: Extent,
714    /// How many absorbs were spent before it stopped.
715    pub attempts: u32,
716    /// The last attempt's root cause — the backend's own sentence, which is
717    /// what a client and an operator both get told.
718    pub reason: String,
719}
720
721/// How many times one pack's absorb is attempted before the store stops.
722///
723/// Three, and the reason it is not one: an absorb can fail for a reason outside
724/// the pack — the memory watchdog gave up, the repository was being deleted, a
725/// transient read error — and a single bad moment must not condemn durable
726/// bytes. It is not thirty either, because the resolve is deterministic in the
727/// store's own state: a pack that failed three times with nothing else changing
728/// fails the fourth time for the same reason, and every one of those attempts
729/// is paid for by a client waiting on a push.
730pub const MAX_ABSORB_ATTEMPTS: u32 = 3;
731
732// **The pack this thread is inside an absorb of**, if any.
733//
734// `absorb_gated` holds `Absorber::gate` for the whole pack, and the resolver it
735// runs asks this very store for delta bases through `BaseSource::content`. That
736// re-entry must not take the gate again — a `std::sync::Mutex` is not
737// reentrant, so it would not be a wait, it would be a deadlock — and it must
738// not treat the pack it is halfway through as something it can absorb. One
739// `Cell` answers both questions, and it is the same guard shape
740// `scan_pending`'s `SCANNING` uses for the same reason.
741thread_local! {
742    static IN_ABSORB: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
743}
744
745/// Sets [`IN_ABSORB`] for the duration of one absorb and restores whatever was
746/// there before — absorbs nest (a pack whose base is in another pending pack).
747struct InAbsorb(Option<u64>);
748
749impl InAbsorb {
750    fn enter(pack_id: u64) -> Self {
751        InAbsorb(IN_ABSORB.with(|c| c.replace(Some(pack_id))))
752    }
753}
754
755impl Drop for InAbsorb {
756    fn drop(&mut self) {
757        IN_ABSORB.with(|c| c.set(self.0));
758    }
759}
760
761/// One pending pack, scanned read-only: what a lookup needs and nothing that
762/// the index will hold. O(objects) memory — for linux ~56 B of walk + 32 B of
763/// row per object, ~1.2 GB — and gone when the pack's bit goes up.
764///
765/// **Nothing on the production read path builds one of these any more.** A
766/// reader that needs an un-indexed pack indexes it instead
767/// ([`Absorber::index_pending_now`]); this remains for the one caller that
768/// cannot — a resolver already inside `absorb_gated`, which may consult a scan
769/// somebody else already built but must never start one.
770struct ScannedPack {
771    walk: crate::pack_walk::PackWalk,
772    /// `(oid, entry index, resolved size)`, sorted by oid.
773    rows: Vec<(Vec<u8>, u32, u64)>,
774    archive_offset: u64,
775}
776
777impl ScannedPack {
778    fn find(&self, oid: &[u8]) -> Option<(usize, u64)> {
779        self.rows
780            .binary_search_by(|(o, _, _)| o.as_slice().cmp(oid))
781            .ok()
782            .map(|i| (self.rows[i].1 as usize, self.rows[i].2))
783    }
784
785    fn row(&self, oid: &[u8]) -> Option<crate::index_layout::IndexRow> {
786        let (entry, size) = self.find(oid)?;
787        let e = &self.walk.entries[entry];
788        Some(crate::index_layout::IndexRow {
789            // Not ranked: the row is not in the index yet.
790            ordinal: u32::MAX,
791            offset: e.offset + self.archive_offset,
792            len: e.len,
793            obj_type: e.obj_type,
794            uncompressed_size: size,
795            delta_base: match &e.delta_base {
796                crate::pack_walk::DeltaBase::Offset(o) => o + self.archive_offset,
797                _ => 0,
798            },
799        })
800    }
801}
802
803/// One repository's git store.
804///
805/// | on disk | what it is | who owns the format |
806/// |---|---|---|
807/// | `<root>/objects.pack` | the verbatim pack bytes, appended | [`SafeWriter`] |
808/// | `<root>/objects.pack.journal` | the durable extent rows | [`SafeWriter`] |
809/// | `<root>/objects.tail` | the un-sealed index tail | [`ObjectReadStack`] (redb) |
810/// | `<root>/objects.exploded` | §14's resolved content, **droppable** | [`ExplodedArchive`] (one Arrow IPC table) |
811/// | `<root>/refs.log` | the ref transaction log | [`RefLog`] (Arrow IPC frames) |
812/// | `<root>/<name>.znippy` | the sealed archive GC compacts | base znippy |
813///
814/// Nothing in that table is new. The store is the composition, not a fifth
815/// format.
816/// # Which implementation of each trait, and how a caller picks
817///
818/// Three of the four boxes in the diagram at the top of this file have more than
819/// one implementation. [`StoreConfig`] is the choice, [`crate::arms`] documents
820/// what each arm promises, and the dispatch is split deliberately:
821///
822/// | trait | selected by | dispatch | cost |
823/// |---|---|---|---|
824/// | [`ObjectIndex`] | the type parameter `S` | **static** | none — the hot path is monomorphised |
825/// | [`ArchiveWrite`](crate::archive_write::ArchiveWrite) | [`StoreConfig::writer`] | `dyn`, inside [`PushPath`] as it already was | one virtual call **per pack append**, not per object |
826/// | [`Gc`] | [`StoreConfig::gc`] | `dyn` | one virtual call per `gc()` run |
827///
828/// `S` defaults to [`OneTableFourColumns`], so `GitStore` still names exactly
829/// what it named before and every existing caller compiles unchanged.
830pub struct GitStore<S: ObjectIndex = OneTableFourColumns> {
831    root: PathBuf,
832    /// The verbatim pack bytes. The **truth** (§14).
833    blobs: PathBuf,
834    /// The sealed znippy archive `gc` compacts. Only GC and `seal` touch it.
835    archive: PathBuf,
836    /// The selected [`ArchiveWrite`](crate::archive_write::ArchiveWrite) arm
837    /// plus the per-account index channel. The ack path.
838    push: PushPath,
839    account: String,
840    hash: GitHashKind,
841    /// The object-level index and the absorb behind it — **shared with the
842    /// account indexer's worker**, which is how a push ends in object rows
843    /// without a read having to ask for them.
844    absorber: Arc<Absorber<S>>,
845    refs: RefLog,
846    /// Serialises the ref log's read-compare-append. A CAS that read the
847    /// namespace, then appended, without holding this could be overtaken between
848    /// the two and would overwrite the update it did not see.
849    ref_gate: Mutex<()>,
850    /// The selected [`Gc`] arm. Built once, here, and run by
851    /// [`GitOps::gc`] — a `Box<dyn>` because a GC is a whole-archive
852    /// compaction that happens on a maintenance timer, so one virtual call
853    /// against it is unmeasurable and a third type parameter on every signature
854    /// in the crate is not free to read.
855    gc: Box<dyn Gc + Send + Sync>,
856    /// What this store was built with, for a bench row or a config dump. A
857    /// description of the arms already constructed — **nothing consults it on a
858    /// serving path**.
859    arms: StoreConfig,
860}
861
862impl GitStore<OneTableFourColumns> {
863    /// Open (creating) the store for one repository under `root`.
864    ///
865    /// **The default arms, and it reads no environment to get them.** An
866    /// operator who exported `ZNIPPY_GIT_WRITER=fast` does not change what this
867    /// builds — a caller that never asked for a selector must not have its
868    /// durability contract moved out from under it. Use
869    /// [`open_from_env`] when the selection is wanted.
870    ///
871    /// ⚠ One variable *is* read here, and it is not an arm:
872    /// `ZNIPPY_GIT_REDB_CACHE_BYTES` (see
873    /// [`crate::arms::redb_cache_bytes`]). It selects no implementation,
874    /// changes no durability contract and leaves the bytes on disk identical —
875    /// it caps redb's page cache, whose own default is 1 GiB *per database*, of
876    /// which this store opens two. The sentence above is about arms, and a
877    /// memory ceiling is not one.
878    pub fn open(root: &Path, account: &str) -> Result<Self> {
879        Self::open_with(root, account, GitHashKind::Sha1)
880    }
881
882    /// Same, with an explicit oid width. sha256 repositories exist and the store
883    /// does not get to assume otherwise; every oid it stores is
884    /// [`GitHashKind::oid_len`] bytes wide and the tail refuses a mixed batch.
885    ///
886    /// # What a reopen recovers, and why nothing had to be stored for it
887    ///
888    /// A store opened over an `objects.pack` some earlier process was still
889    /// indexing has to end up in the same state that process would have reached.
890    /// §13.12's `indexed` bit is what says so, and it is **derived here rather
891    /// than persisted**: a pack is unabsorbed *iff* its extent is in the journal
892    /// and its rows are not in the index, and both of those are already durable
893    /// (see [`Absorber::adopt_journal`]). The packs that come back clear are put
894    /// straight onto the account's channel, so the drain finishes the interrupted
895    /// work with no read having to ask — and until it does, every read falls back
896    /// exactly as it would have before the crash. **Slower, never wrong**, across
897    /// a restart as well as within one.
898    ///
899    /// The bits are set **before** the channel is fed, which is the same ordering
900    /// the ack path uses for the same reason: a read that arrives in between finds
901    /// the bit clear and waits, never an index that does not mention the pack.
902    ///
903    /// # What a reopen recovers of the DERIVED tables, and how that was fixed
904    ///
905    /// [`Derived::graph`] and [`Derived::trees`] come back **in full**, for every
906    /// pack, whether or not anything is re-queued. They are folded from §14's
907    /// exploded table ([`Absorber::refold`]), which is on disk, so a clean
908    /// shutdown carries them across.
909    ///
910    /// This was a live correctness hole until 2026-08-08 and it is recorded
911    /// rather than quietly repaired. The graph was *accumulated in RAM* as packs
912    /// were absorbed and stored nowhere; a store killed mid-absorb got it back
913    /// because the interrupted pack was re-queued and re-resolved, but a store
914    /// that shut down **cleanly** re-queued nothing and came back with an empty
915    /// graph. MEASURED on oden 2026-08-08: 2687 rows and **0 of 551 commits**,
916    /// `reachable` on a commit returning that commit alone instead of its
917    /// closure — a *quiet* wrong answer — and `gc` seeing an empty live set. The
918    /// fix is the table, because the payloads the graph is folded from had no
919    /// home; [`tests::a_clean_shutdown_and_reopen_keeps_the_graph_and_reachable`]
920    /// is what proves it, over a real process that exits normally.
921    ///
922    /// The exploded table is still **droppable**: if its row count is short of
923    /// the `objects` table's, [`Absorber::adopt_journal`] declares no pack
924    /// absorbed and every one of them is re-queued and re-exploded. Absent means
925    /// rebuild, never wrong (§13.12, applied unchanged to a derived table).
926    ///
927    /// **Also the default arms, and also no environment read** — see
928    /// [`open`](GitStore::open).
929    pub fn open_with(root: &Path, account: &str, hash: GitHashKind) -> Result<Self> {
930        // The default ARMS, and the environment's CEILING: the one variable
931        // this constructor reads, because an operator capping a server's
932        // footprint must not have to pick a different constructor to do it.
933        // See [`crate::arms::redb_cache_bytes`].
934        Self::open_with_arms(
935            root,
936            account,
937            hash,
938            StoreConfig::DEFAULT.with_redb_cache_bytes(crate::arms::redb_cache_bytes()?),
939        )
940    }
941}
942
943/// Where a stored entry's delta base sits, relative to the request being served.
944///
945/// The `Outside` arm carries the base in **both** namings because the two delta
946/// forms know only one each — an `OFS_DELTA` its archive offset, a `REF_DELTA`
947/// its oid — and [`GitStore::emit_set`]'s thin arm has to ask "does the client
948/// hold this" in whichever one the entry speaks.
949enum BaseOf {
950    /// Not a delta at all. There is nothing to place it after and nothing to
951    /// re-head; the stored entry is copied as it stands.
952    Whole,
953    /// A delta whose base **is** in the request, at this archive offset.
954    ///
955    /// The offset is carried rather than recomputed because a `REF_DELTA` does
956    /// not know it — it names its base by oid, and its own `delta_base` column
957    /// is `0` by construction — and the emitter needs it twice: as the
958    /// `EmitEntry::delta_base` that [`crate::pack_walk::topological_order`]
959    /// orders on, and as the base an in-pack ref-delta is re-headed to point at.
960    Inside { at: u64 },
961    /// A delta whose base the request does not carry.
962    Outside {
963        /// The base's archive offset, `0` when the entry named an oid instead.
964        offset: u64,
965        /// The base's oid, `None` when the entry named an offset instead — or
966        /// when the entry is too short to read one out of.
967        oid: Option<Vec<u8>>,
968    },
969}
970
971/// The bases a **thin** pack may name without carrying them: what the receiver
972/// already held before this transfer, keyed both ways.
973///
974/// Built by [`GitStore::client_bases`], which is where the argument for trusting
975/// it is written down.
976struct ExternalBases {
977    /// The held object's oid, keyed by its archive offset — the coordinate an
978    /// `OFS_DELTA` names its base in, and the only reason this map exists.
979    by_offset: HashMap<u64, OidKey>,
980    /// The same objects keyed the way a `REF_DELTA` names them.
981    held: std::collections::HashSet<OidKey>,
982}
983
984/// **A raw oid as a fixed-size, `Copy` hash key — 20 bytes for SHA-1, 32 for
985/// SHA-256, and no heap allocation either way.**
986///
987/// The two maps in [`ExternalBases`] are repository-sized: the client's `have`
988/// closure on an incremental fetch is very nearly every object in the store.
989/// Keying them by `Vec<u8>` is one heap allocation per entry per request —
990/// 12 112 of them for a 69-object fetch, measured on oden 2026-08-15 — for a
991/// question (*does the client hold this base?*) that never looks at more than
992/// `oid_len` bytes.
993///
994/// Padded to the widest hash rather than parameterised by width: a store holds
995/// exactly one hash kind, so every key in one set has the same real length and
996/// two different oids can never collide through the padding. The length is
997/// carried anyway, because a key that silently compared equal across widths is
998/// the kind of wrong that only shows up in a mixed-hash mirror.
999#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1000struct OidKey {
1001    bytes: [u8; 32],
1002    len: u8,
1003}
1004
1005impl OidKey {
1006    /// The key for `oid`, or a refusal for one wider than any git hash.
1007    fn new(oid: &[u8]) -> Result<Self> {
1008        let len = oid.len();
1009        if len == 0 || len > 32 {
1010            bail!("{len} is not the width of any git object id (1..=32 bytes)");
1011        }
1012        let mut bytes = [0u8; 32];
1013        bytes[..len].copy_from_slice(oid);
1014        Ok(OidKey {
1015            bytes,
1016            len: len as u8,
1017        })
1018    }
1019
1020    /// The oid back, at its real width.
1021    fn as_slice(&self) -> &[u8] {
1022        &self.bytes[..self.len as usize]
1023    }
1024}
1025
1026impl<S: ObjectIndex + 'static> GitStore<S> {
1027    /// **The one constructor**, with every arm named.
1028    ///
1029    /// `S` picks the [`ObjectIndex`] layout at compile time; `arms` picks the
1030    /// [`ArchiveWrite`](crate::archive_write::ArchiveWrite) and [`Gc`]
1031    /// implementations at run time. [`open`](GitStore::open) and
1032    /// [`open_with`](GitStore::open_with) are this function with
1033    /// [`StoreConfig::DEFAULT`], which is why nothing about them moved.
1034    ///
1035    /// `arms.index` is **not** consulted here — `S` is the index arm, and a
1036    /// value that disagreed with the type would be a second source of truth.
1037    /// [`open_from_env`] is the one place the two are tied together, and it does
1038    /// it by choosing `S` from the value.
1039    ///
1040    /// ```no_run
1041    /// use znippy_plugin_git::arms::{GcArm, StoreConfig, WriterArm};
1042    /// use znippy_plugin_git::index_layout::PackedPayload;
1043    /// use znippy_plugin_git::{GitHashKind, GitStore};
1044    ///
1045    /// // A rebuildable import mirror: the ceiling writer, the packed payload
1046    /// // layout, and an in-place compaction.
1047    /// let arms = StoreConfig::DEFAULT
1048    ///     .with_writer(WriterArm::Fast)
1049    ///     .with_gc(GcArm::CompactInPlace);
1050    /// let store = GitStore::<PackedPayload>::open_with_arms(
1051    ///     std::path::Path::new("/srv/repo"),
1052    ///     "alice",
1053    ///     GitHashKind::Sha1,
1054    ///     arms,
1055    /// )?;
1056    /// # Ok::<(), anyhow::Error>(())
1057    /// ```
1058    pub fn open_with_arms(
1059        root: &Path,
1060        account: &str,
1061        hash: GitHashKind,
1062        arms: StoreConfig,
1063    ) -> Result<Self> {
1064        std::fs::create_dir_all(root)
1065            .with_context(|| format!("creating the store root {}", root.display()))?;
1066        let blobs = root.join("objects.pack");
1067        let writer = arms.writer.create(&blobs)?;
1068        // `None` for an arm that keeps no durable ack log — see
1069        // [`crate::arms::WriterArm::journal`]. Everything below that touches the
1070        // journal is written against that `Option` rather than assuming one.
1071        let journal = arms.writer.journal(&blobs);
1072        // **The redb ceiling**, carried in on the config rather
1073        // than read here: `StoreConfig::from_env` reads it with the arms, and
1074        // `open`/`open_with` put the environment's value on `DEFAULT` before
1075        // getting here, so every constructor still honours the variable and
1076        // `arms.to_string()` prints what was actually applied. Not an arm — see
1077        // [`crate::arms::redb_cache_bytes`] — redb's own default is 1 GiB *per
1078        // database*, and this store opens exactly ONE of them, the
1079        // `ObjectReadStack` below. The `let _ = cache_bytes` two lines down is
1080        // what is left of the second.
1081        let cache_bytes = arms.redb_cache_bytes;
1082        let objects = ObjectReadStack::<S>::open(
1083            &root.join("objects.tail"),
1084            RebuildTriggers::default(),
1085            cache_bytes,
1086        )?;
1087        let _ = cache_bytes; // no page cache to size: the blob is pread, the index is a slice
1088        // A store written before 2026-09-04 carries `objects.ordinal`, the memo
1089        // of the sorted oid list the read stack's persisted index now IS. Its
1090        // bytes are never read again, so it goes on the first open by this
1091        // build — 278 964 902 B on the kernel mirror. Absent is the normal case
1092        // and not an event; anything else that stops the delete is logged, not
1093        // fatal, because the store does not depend on the file being gone.
1094        match std::fs::remove_file(root.join(ORDINAL_CACHE_FILE)) {
1095            Ok(()) => tracing::info!(
1096                target: "znippy.git",
1097                path = %root.join(ORDINAL_CACHE_FILE).display(),
1098                "removed a stale objects.ordinal — the persisted index's oid column is the \
1099                 ordinal space now"
1100            ),
1101            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
1102            Err(e) => tracing::warn!(
1103                target: "znippy.git",
1104                path = %root.join(ORDINAL_CACHE_FILE).display(),
1105                error = %e,
1106                "could not remove a stale objects.ordinal; it is dead weight, not a fault"
1107            ),
1108        }
1109        let exploded = Arc::new(ExplodedCache::open(
1110            &root.join("objects.exploded"),
1111            crate::exploded_arrow::ExplodePolicy::from_env()?,
1112            crate::exploded_cache::budget_from_env(root)?,
1113        )?)
1114        .registered();
1115        // **One descriptor on the blob file, not three.** The writer opened it
1116        // read/write and `flock`ed it; the absorber's `pread` fallback and the
1117        // index workers' `read_exact_at` both address it positionally, so they
1118        // take the same open file description rather than two more of their
1119        // own. 5 descriptors per open store → 3, which is the divisor of
1120        // gunnar's `open_repo_budget` (`tests/fd_per_store.rs`).
1121        let absorber = Arc::new(Absorber::<S>::open(
1122            &blobs,
1123            writer.blobs_shared(),
1124            hash,
1125            objects,
1126            exploded,
1127        )?);
1128
1129        // **The `indexed` bit, derived.** Every extent this archive has ever
1130        // acked, diffed against the rows the index holds. `SafeWriter::create`
1131        // above appends to that journal and never truncates it, which is what
1132        // makes the older extents still be here to diff against.
1133        //
1134        // An arm with no journal has nothing to derive the bit from, and that is
1135        // the honest consequence of choosing it rather than a gap: a
1136        // `FastWriter` store has no durable record that a pack was acked, so a
1137        // reopen re-queues nothing and there is nothing for it to re-queue.
1138        let acked = match journal.as_deref() {
1139            Some(p) if p.exists() => crate::archive_write::read_journal(p).with_context(|| {
1140                format!(
1141                    "reading {} — it is the durable half of the indexed bit and a store \
1142                         cannot be opened without knowing which packs it owes index work for",
1143                    p.display()
1144                )
1145            })?,
1146            _ => Vec::new(),
1147        };
1148        let requeue = absorber.adopt_journal(&acked)?;
1149
1150        // **The wiring.** The absorber goes into the push path, which hands it to
1151        // every account indexer it starts, so the drain behind `push_pack` ends
1152        // in object rows instead of one pack row. Nothing about the ack changes:
1153        // `append` still returns after the two fsyncs and this is all downstream
1154        // of that (§13.9).
1155        let push = PushPath::with_absorber(
1156            writer,
1157            &blobs,
1158            journal,
1159            absorber.clone() as Arc<dyn ObjectAbsorb>,
1160        )?;
1161        let refs = RefLog::new(root.join("refs.log"));
1162        let archive = root.join("repository.znippy");
1163
1164        let store = Self {
1165            root: root.to_path_buf(),
1166            blobs,
1167            archive,
1168            push,
1169            account: account.to_string(),
1170            hash,
1171            absorber,
1172            refs,
1173            ref_gate: Mutex::new(()),
1174            gc: arms.gc.create(),
1175            arms,
1176        };
1177        // The interrupted work, back on the channel it fell off. Nothing reads
1178        // here and nothing blocks: this is the same 24-byte handoff a push makes,
1179        // and the drain picks it up on its own thread (§13.9-11).
1180        let indexer = store.push.indexer(&store.account);
1181        for p in &requeue {
1182            indexer.submit(IndexJob {
1183                pack_id: p.pack_id,
1184                offset: p.offset,
1185                len: p.len,
1186            })?;
1187        }
1188        // A store reopened over an existing tail already knows its objects; the
1189        // derived tables are rebuilt from them rather than from a side-car,
1190        // because a side-car is a second truth.
1191        store.refold()?;
1192        Ok(store)
1193    }
1194
1195    /// The arms this store was built with. A description of what was
1196    /// constructed, not a switch — nothing on a serving path reads it.
1197    pub fn arms(&self) -> StoreConfig {
1198        self.arms
1199    }
1200
1201    /// The selected writer's name, as it goes on a bench row.
1202    pub fn writer_name(&self) -> &'static str {
1203        self.push.name()
1204    }
1205
1206    /// What the selected writer's `append` actually promises. Printed next to a
1207    /// throughput, because a throughput without it is not a comparison.
1208    pub fn writer_durability(&self) -> &'static str {
1209        self.push.durability()
1210    }
1211
1212    /// The selected [`Gc`] arm.
1213    pub(crate) fn gc_arm(&self) -> &dyn Gc {
1214        self.gc.as_ref()
1215    }
1216
1217    /// Where the verbatim packs are.
1218    pub fn blobs_path(&self) -> &Path {
1219        &self.blobs
1220    }
1221
1222    /// The sealed archive GC operates on.
1223    pub fn archive_path(&self) -> &Path {
1224        &self.archive
1225    }
1226
1227    pub fn root(&self) -> &Path {
1228        &self.root
1229    }
1230
1231    pub fn hash_kind(&self) -> GitHashKind {
1232        self.hash
1233    }
1234
1235    /// The `objects` table, for a caller that wants the index directly (the
1236    /// bench does).
1237    pub fn index(&self) -> &ObjectReadStack<S> {
1238        &self.absorber.objects
1239    }
1240
1241    /// Objects in the `objects` table.
1242    pub fn object_count(&self) -> usize {
1243        self.absorber.objects.len()
1244    }
1245
1246    /// Commits in the graph.
1247    pub fn commit_count(&self) -> usize {
1248        self.absorber
1249            .derived
1250            .read()
1251            .expect("derived lock")
1252            .graph
1253            .len()
1254    }
1255
1256    /// `(graph_read_from_disk, graph_recomputed)` since this store was opened —
1257    /// how many folds read the persisted `objects.graph` vs re-derived it.
1258    pub fn graph_source_stats(&self) -> (u64, u64) {
1259        self.absorber.graph_source_stats()
1260    }
1261
1262    /// `(reach_carried, reach_rebuilt)` since this store was opened — how many
1263    /// folds carried the reachability table across (renumbered, no tree
1264    /// opened) vs how many reads built it from scratch. The witness for the
1265    /// clone-after-push lever; see `REACH_CARRY_MAX_FRACTION`.
1266    pub fn reach_source_stats(&self) -> (u64, u64) {
1267        self.absorber.reach_source_stats()
1268    }
1269
1270    /// `(folded_rows, tail_rows)` of the ordinal space the last fold built — how
1271    /// many oids came off the persisted index's oid column and how many off a
1272    /// scan of the redb tail. Replaces S-082's `(read, recomputed)`: there is no
1273    /// memo to read any more, the index IS the space.
1274    pub fn ordinal_source_stats(&self) -> (u64, u64) {
1275        self.absorber.ordinal_source_stats()
1276    }
1277
1278    /// The folded ordinal space — every oid in ordinal order, hex. For the tests
1279    /// that assert a memo read reproduces the scanned space exactly (same oids,
1280    /// same order, same ordinals — the ordinal is the index into this vector).
1281    ///
1282    /// Formatted on demand from `oids_raw`: there is no resident hex `Vec` any
1283    /// more (S-082), and a test that wants hex encodes the flat buffer here
1284    /// rather than the store carrying a second copy for it.
1285    #[cfg(test)]
1286    pub(crate) fn ordinal_oids(&self) -> Vec<String> {
1287        let oid_len = self.hash.oid_len();
1288        self.absorber
1289            .derived
1290            .read()
1291            .expect("derived lock")
1292            .oids_raw
1293            .chunks_exact(oid_len)
1294            .map(hex::encode)
1295            .collect()
1296    }
1297
1298    /// **One object's resolved content** — §14's exploded table, in one lookup.
1299    ///
1300    /// Not one of the twelve and deliberately not on [`GitOps`]: the twelve are
1301    /// the wire vocabulary and they hand back **stored** bytes ([`GitOps::get`]),
1302    /// which for a delta entry is a delta. This is the other question — *what
1303    /// does this object contain* — and it is the one §14's table exists to make
1304    /// a point lookup instead of a chain walk.
1305    ///
1306    /// It absorbs any pending pack first, for the same reason
1307    /// [`GitOps::has`] does: a store with durable bytes it has not indexed cannot
1308    /// answer "absent" without lying.
1309    pub fn content(&self, oid: Oid<'_>) -> Result<Option<(GitObjectKind, Vec<u8>)>> {
1310        // No absorb here any more: a pack the worker has not reached is read
1311        // for this one oid by `Absorber::resolved`'s third arm — on this thread,
1312        // from the mmapped pack, without waiting for the worker.
1313        self.absorber.resolved(oid)
1314    }
1315
1316    /// **One object, with the bill for producing it.**
1317    ///
1318    /// [`Self::content`] plus what the read cost: `None` for a cost when the
1319    /// exploded pools answered (nothing was rebuilt), `Some(cost)` when the
1320    /// object was inflated out of the pack. What a measurement needs and a
1321    /// server does not — the cache admission decision is made inside, not by
1322    /// the caller.
1323    pub fn content_costed(
1324        &self,
1325        oid: Oid<'_>,
1326    ) -> Result<Option<(GitObjectKind, Vec<u8>, Option<crate::resolve::ReadCost>)>> {
1327        self.absorber.resolved_costed(oid)
1328    }
1329
1330    /// §14's table, counted: rows, rows written, reads it served, reads that fell
1331    /// through to re-deriving from the verbatim truth.
1332    ///
1333    /// Applied output. Both content paths return identical bytes, so `served`
1334    /// against `rederived` is the only thing that can say which one ran.
1335    pub fn exploded_stats(&self) -> ExplodedStats {
1336        self.absorber.exploded.engine_stats()
1337    }
1338
1339    /// **What the exploded tier is holding and what it is allowed to hold** —
1340    /// the row a dashboard prints (`explode 3.1G/8G`) and `Admin.StorageInfo`
1341    /// carries, per repository.
1342    pub fn explode_cache_use(&self) -> crate::exploded_cache::CacheUse {
1343        self.absorber.exploded.use_now()
1344    }
1345
1346    /// Rows in the EAGER pool only — the graph the push wrote, with nothing a
1347    /// reader has since cached. What `adopt_journal` compares against the index
1348    /// and what a test asking "did the push explode the graph" must ask,
1349    /// because [`Self::exploded_stats`]'s `rows` counts both pools and a read
1350    /// moves it.
1351    pub fn exploded_eager_rows(&self) -> Result<u64> {
1352        self.absorber.exploded.eager_rows()
1353    }
1354
1355    /// **Set this repository's explode budget** — what the control plane calls
1356    /// when an account's quota changes. Takes effect at the next rotation; it
1357    /// never refuses a read, never refuses a write and never deletes on the
1358    /// caller's thread. A bigger budget buys a bigger cache and therefore faster
1359    /// first reads; a smaller one costs re-inflation, never an error.
1360    pub fn set_explode_budget(&self, bytes: u64) {
1361        self.absorber.exploded.set_budget(bytes);
1362    }
1363
1364    /// Flush the lazy pool, rotate it and evict it back under budget — on
1365    /// **this** thread. What a test and a `gc` call; a reader never does, it
1366    /// starts a write-back thread and walks away.
1367    pub fn explode_cache_maintain(&self) -> Result<u64> {
1368        self.absorber.exploded.maintain()
1369    }
1370
1371    /// **`repo gc` reclaims the cache**: every lazy segment unlinked, the graph
1372    /// pool kept. Returns the bytes that left the filesystem.
1373    pub fn drop_explode_cache(&self) -> Result<u64> {
1374        self.absorber.exploded.drop_cache()
1375    }
1376
1377    /// Where §14's table lives. Deleting this file while no store holds it is the
1378    /// sanctioned way to drop it; the next open re-explodes every pack.
1379    pub fn exploded_path(&self) -> &Path {
1380        self.absorber.exploded.path()
1381    }
1382
1383    /// Every live row of one kind, with its payload — what the graph fold reads,
1384    /// exposed so a measurement can ask **the table** how many bytes it holds
1385    /// rather than dividing a file size by what the pack claimed.
1386    pub fn exploded_of_kind(&self, kind: GitObjectKind) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
1387        self.absorber.exploded.of_kind(kind)
1388    }
1389
1390    /// The commit graph as it stands, generations included. A snapshot: the
1391    /// caller holds no lock and the next fold replaces it wholesale.
1392    pub fn graph_snapshot(&self) -> Vec<CommitNode> {
1393        self.absorber
1394            .derived
1395            .read()
1396            .expect("derived lock")
1397            .graph
1398            .clone()
1399    }
1400
1401    /// Packs whose objects are not in the index yet — §13.12's bit, counted.
1402    pub fn unindexed_packs(&self) -> usize {
1403        self.absorber.unindexed_packs()
1404    }
1405
1406    /// This account's indexer: the thread that drains the channel a push queues
1407    /// on. `is_indexed(pack_id)` on it is §13.12's bit at the pack level, and it
1408    /// is set only once that pack's **objects** are in the index.
1409    pub fn indexer(&self) -> std::sync::Arc<crate::indexer::AccountIndexer> {
1410        self.push.indexer(&self.account)
1411    }
1412
1413    /// Block until the background drain has absorbed everything this store has
1414    /// pushed. What a maintenance tick, a seal or a test uses instead of racing
1415    /// it.
1416    pub fn wait_indexed(&self) {
1417        self.push.pool().wait_caught_up();
1418    }
1419
1420    /// **Stop this store's index work — the repository is being deleted.**
1421    ///
1422    /// Sets the cancel flag the resolver polls every few thousand objects and
1423    /// the drain checks before every pack. The pack in flight is **aborted at
1424    /// its next checkpoint** (milliseconds), not finished: its rows are keyed
1425    /// by oid and the exploded tier is droppable, and the whole directory is
1426    /// about to be removed anyway. Every pack still queued is skipped. After
1427    /// this the store can be dropped without the drop joining a worker that is
1428    /// hours into a kernel pack — which is what `Drop for AccountIndexer` would
1429    /// otherwise do.
1430    pub fn cancel_index(&self) {
1431        self.absorber.cancel.store(true, std::sync::atomic::Ordering::SeqCst);
1432        self.push.pool().cancel_all();
1433    }
1434
1435    /// Put a different memory watchdog under this store's worker — a test's
1436    /// fake `/proc/meminfo`. Takes effect from the next pack.
1437    pub fn set_mem_watchdog(&self, dog: Arc<crate::memguard::MemWatchdog>) {
1438        *self.absorber.watchdog.write().unwrap_or_else(|e| e.into_inner()) = dog;
1439    }
1440
1441    /// Has [`cancel_index`](Self::cancel_index) been called?
1442    pub fn index_cancelled(&self) -> bool {
1443        self.absorber.cancel.load(std::sync::atomic::Ordering::SeqCst)
1444    }
1445
1446    /// This store's index progress counter, for a dashboard or a stats call.
1447    pub fn index_progress(&self) -> crate::progress::Snapshot {
1448        self.absorber.progress.snapshot()
1449    }
1450
1451    /// **The indexer's half of the split**, and not one of the twelve.
1452    ///
1453    /// Runs on the account indexer's worker (§13.9), and on a read that arrives
1454    /// before that worker got there — the same function either way, which is why
1455    /// there is no such thing as a background index that behaves differently from
1456    /// the fallback one.
1457    pub fn absorb_pending(&self) -> Result<usize> {
1458        self.absorber.absorb_pending()
1459    }
1460
1461    /// **Make every durable object addressable, on this thread, now** —
1462    /// [`git_storage_trait::GitServe::index_now`]'s implementation and the ack
1463    /// path's one call. See [`Absorber::index_pending_now`] for why the
1464    /// connectivity check is the only caller allowed to wait here.
1465    pub fn index_now(&self) -> Result<usize> {
1466        self.absorber.index_pending_now()
1467    }
1468
1469    /// **The packs this store has stopped trying to index**, with the reason
1470    /// each one stopped for.
1471    ///
1472    /// Empty is the ordinary answer and the only good one. A non-empty list is
1473    /// a repository that is missing objects whose bytes are durable, and it is
1474    /// the fact an operator needs before wondering why a push was refused or a
1475    /// standby is behind — see [`MAX_ABSORB_ATTEMPTS`].
1476    pub fn unindexable_packs(&self) -> Vec<Unindexable> {
1477        self.absorber.unindexable_packs()
1478    }
1479
1480    /// **The object ids a stored pack introduced**, and not one of the twelve.
1481    ///
1482    /// [`GitOps::put_pack`] cannot answer this and never will: the walk it does
1483    /// reads pack *entries*, and a pack entry does not carry its oid — that is
1484    /// why [`crate::pack_walk::Closure`] can only name the `REF_DELTA` bases and
1485    /// says so. The oids exist once the indexer has absorbed the pack, so this
1486    /// absorbs first rather than racing the drain, and is therefore the one
1487    /// caller that deliberately pays §13.9's deferral back.
1488    ///
1489    /// **Only call it when something needs the set.** A push whose policy has no
1490    /// gate over objects must not pay this, which is the whole reason it is a
1491    /// separate call and not a field on `TxId`.
1492    ///
1493    /// The extent *is* the identity: §14 stores a pack verbatim as one
1494    /// contiguous append, so "introduced by this pack" and "indexed at an offset
1495    /// inside this pack's extent" are the same set, with no pack column to keep
1496    /// in step. Bases the push deltas against live in *other* extents and are
1497    /// excluded by construction — which is what a thin push needs, and what
1498    /// reading a written `.idx` back could never do.
1499    ///
1500    /// Cost is one pass over the index, so it is O(objects in the repository),
1501    /// not O(objects pushed). Recorded rather than hidden: a per-pack ordinal
1502    /// range would make it O(pushed), and needs the indexer to keep one.
1503    pub fn oids_in_extent(&self, extent: Extent) -> Result<Vec<Vec<u8>>> {
1504        let (start, len) = extent;
1505        // **Never waits on the indexer.** If the gate is free this thread
1506        // absorbs the pending packs (the same work, done now, by the caller
1507        // that needs it); if the worker holds it, the extent is resolved
1508        // read-only on this thread and its oids are the answer — the same
1509        // resolver, no sink, no rows written.
1510        if self.try_absorb_pending()?.is_none() {
1511            let snap = self.archive_snapshot()?;
1512            let bytes = self.extent(&snap, start, len)?;
1513            let walked = crate::pack_walk::walk(&bytes, self.hash.oid_len())?;
1514            let mut oids: Vec<Vec<u8>> = Vec::with_capacity(walked.entries.len());
1515            let limits = crate::resolve::ResolveLimits {
1516                base_budget: (crate::resolve::base_budget_from_env() / 4).max(1),
1517                ..crate::resolve::ResolveLimits::none()
1518            };
1519            crate::resolve::resolve_streaming(
1520                &bytes,
1521                &walked,
1522                self.hash,
1523                start,
1524                &*self.absorber,
1525                &crate::exploded::NoSink,
1526                limits,
1527                &mut |rows| {
1528                    oids.extend(rows.drain(..).map(|r| r.oid));
1529                    Ok(())
1530                },
1531            )
1532            .context("resolving the pushed pack read-only to enumerate its objects")?;
1533            return Ok(oids);
1534        }
1535        let end = start.saturating_add(len);
1536        let all = self.index().oids_in_order()?;
1537        let refs: Vec<&[u8]> = all.iter().map(Vec::as_slice).collect();
1538        let extents = self.index().extents_batch(&refs);
1539        Ok(all
1540            .into_iter()
1541            .zip(extents)
1542            .filter_map(|(oid, at)| match at {
1543                Some((offset, _)) if offset >= start && offset < end => Some(oid),
1544                _ => None,
1545            })
1546            .collect())
1547    }
1548
1549    /// **Everything needed to emit a pack for EXACTLY `oids`** — nothing added,
1550    /// nothing dropped.
1551    ///
1552    /// Not one of the twelve. It sits beside [`absorb_pending`](Self::absorb_pending)
1553    /// for the same reason those do: it is about how storage is *shaped*, not
1554    /// about what a caller stores or reads.
1555    ///
1556    /// # 🔴 It used to add the delta bases, and that shipped broken clones
1557    ///
1558    /// Until 2026-08-11 this closed the request over its delta bases: an
1559    /// `OFS_DELTA` names its base by position, so a base outside the set cannot
1560    /// be encoded, and *adding the base* looked like the answer that is always
1561    /// safe. It is not, and the failure is silent on the server:
1562    ///
1563    /// ```text
1564    /// git clone --bare --single-branch --branch base <url>
1565    ///     fatal: did not receive expected object 8601ec33920b7d701c9887fa04916501136d6e90
1566    ///     fatal: fetch-pack: invalid index-pack output
1567    /// event="git.upload_pack.served" objects=33773      ← the server said SUCCESS
1568    /// ```
1569    ///
1570    /// Measured on the `h2h-linear-sha1-2048c-1024f-16k` fixture: `base` reaches
1571    /// **31 805** objects, the delta-base closure added **1 968** more — 33 773,
1572    /// exactly the count the server logged — and 507 of those additions were
1573    /// **trees**. A tree that is not reachable from the wants drags its own
1574    /// children in as a *requirement*, because `git index-pack
1575    /// --check-self-contained-and-connected` (which is what a clone runs, and
1576    /// which sets `strict`) walks every received object's links and then demands
1577    /// each one exist. Those 507 trees named **204** objects the pack did not
1578    /// contain. `8601ec33…` is one of them.
1579    ///
1580    /// So a delta base pulled into a narrowed request is not free: it satisfies
1581    /// the *pack format* and violates *connectivity*. Closing over the added
1582    /// bases' children in turn does terminate — the same fixture reaches a fixed
1583    /// point at 34 534 objects — but it is an over-send by construction, and on
1584    /// a `--filter=blob:none` fetch it adds back exactly the blobs the filter
1585    /// excluded, which is the defect
1586    /// `emit_pack_emits_exactly_the_set_it_is_given_and_never_its_closure`
1587    /// exists to forbid.
1588    ///
1589    /// # What it does instead: the base decides how the entry is encoded
1590    ///
1591    /// The set is never widened. For each requested object:
1592    ///
1593    /// * **base inside the request** → the stored entry is copied byte for byte,
1594    ///   which is the whole point of this engine and is what a full clone does
1595    ///   for every single object;
1596    /// * **base outside the request** → the entry is **rebuilt** from §14's
1597    ///   resolved content and marked
1598    ///   [`EmitEntry::recompressed`](crate::pack_walk::EmitEntry::recompressed) —
1599    ///   as a computed delta against a base the request *does* carry where one
1600    ///   can be found (see [`Self::in_request_chain_ancestor`] and
1601    ///   [`crate::delta`]), and whole where it cannot.
1602    ///
1603    /// That is what stock `pack-objects` does — it reuses a stored delta only
1604    /// when the base is also being packed — and it keeps the receipt a fact:
1605    /// `copied` and `recompressed` add up to the objects written, and
1606    /// `recompressed` is **0 for a whole-repository clone** because such a
1607    /// selection contains every base. The cost lands on the boundary a narrowed
1608    /// request cuts through and nowhere else: 1 968 of 33 773 entries, 5.9 %, on
1609    /// the fixture above.
1610    ///
1611    /// `ofs_delta_ok` is the client's `ofs-delta` capability. An old client that
1612    /// cannot parse one is not refused any more: the entry is re-headed as a
1613    /// `REF_DELTA` naming its base by oid, **carrying the same compressed delta
1614    /// payload**, so that arm stays a copy too. Only a base outside the request
1615    /// costs a rebuild.
1616    ///
1617    /// # `thin_haves` — the boundary entry that does NOT have to be rebuilt
1618    ///
1619    /// `Some(tips)` says the client consented to a thin pack *and* the
1620    /// negotiation found common ground; [`crate::serve::GitStore::emit_oids`] is
1621    /// where the two halves are required together, because either alone is a
1622    /// failed fetch rather than a smaller one. Given it, a base outside the
1623    /// request is checked against [`the client's own objects`](GitStore::client_bases),
1624    /// and one the client already holds is named by oid in a `REF_DELTA` that
1625    /// carries the **stored delta stream unchanged**. That entry counts as
1626    /// `copied`, because it is: nothing was inflated and nothing was deflated.
1627    ///
1628    /// `None` — a clone, or a client that did not ask for a thin pack — behaves
1629    /// exactly as before, and a **full clone is byte-identical either way**,
1630    /// because a whole-repository request has no base outside itself for any of
1631    /// this to apply to.
1632    ///
1633    /// It does nothing for the *clone* half of the boundary cost, and cannot: a
1634    /// clone's receiver holds nothing, so there is no external base to name.
1635    /// That half is the arm below it — [`crate::delta`] computes a delta against
1636    /// something the pack **does** carry, which is the only move left when there
1637    /// is no receiver to lean on. The two do not overlap: `thin_haves` reaches a
1638    /// boundary entry first and costs nothing when it fires, so a fetch stays a
1639    /// pure copy and only a clone pays for a computed delta.
1640    ///
1641    /// # An oid this repository does not hold is REFUSED
1642    ///
1643    /// It used to be skipped, which is the same silent-under-send shape as the
1644    /// closure defect above wearing a different hat: N asked, N-1 emitted,
1645    /// `PackStats` reporting success, and the client the first to know. gunnar's
1646    /// in-memory arm has always refused it, so the skip also meant the two
1647    /// engines behind one contract disagreed about what a missing object means.
1648    /// A partial-clone filter does not depend on the skip and cannot: a filter
1649    /// *removes* oids from the selection, it never adds one the store lacks.
1650    ///
1651    /// # Cost
1652    ///
1653    /// O(objects requested). The previous shape read `oids_in_order()` and
1654    /// `lookup_batch` over the **whole repository** on every call, to build an
1655    /// offset→oid map it needed only for the closure; with no closure the
1656    /// selection's own offsets answer "is this base in the request", so that
1657    /// scan is gone and no reverse index has to exist.
1658    pub fn emit_set(
1659        &self,
1660        oids: &[Oid<'_>],
1661        ofs_delta_ok: bool,
1662        thin_haves: Option<&[Oid<'_>]>,
1663    ) -> Result<Vec<crate::pack_walk::EmitEntry>> {
1664        use crate::index_layout::ObjType;
1665        use std::collections::hash_map::Entry;
1666
1667        // If the gate is free, absorb now; if the worker holds it, the rows
1668        // come from the index plus a read-only pass over the pending packs
1669        // (`lookup_batch_never_wait` below). Never a wait.
1670        let _ = self
1671            .try_absorb_pending()
1672            .context("absorbing pending index work before emitting a pack")?;
1673
1674        // Deduplicate, preserving the caller's order. A repeated oid in the
1675        // request must not become a repeated entry in the pack.
1676        //
1677        // **A map and not a set**, keyed oid → position in `asked` (which is the
1678        // position in `rows`). The membership question is the one it always was;
1679        // what the position buys is the base's *archive offset* for a
1680        // `REF_DELTA`, which names its base by oid and can learn where that base
1681        // sits no other way. It is a `usize` per requested object on a vector
1682        // this function already builds — not a second pass and not a second
1683        // index lookup.
1684        let mut asked: Vec<&[u8]> = Vec::with_capacity(oids.len());
1685        let mut seen: HashMap<&[u8], usize> = HashMap::with_capacity(oids.len());
1686        for oid in oids {
1687            if let Entry::Vacant(slot) = seen.entry(*oid) {
1688                slot.insert(asked.len());
1689                asked.push(*oid);
1690            }
1691        }
1692
1693        let rows = self.lookup_batch_never_wait(&asked)?;
1694
1695        // **One mapping for the whole emit set.** Only the header probes below
1696        // read through it — the entries themselves are recorded as extents and
1697        // resolved by [`Self::resolve_emit_payloads`], which takes its own
1698        // snapshot of the same [`ArchiveMap`](crate::archive_map::ArchiveMap) and
1699        // therefore the same `Arc` unless a push landed in between.
1700        let snap = self.archive_snapshot()?;
1701
1702        // The two questions "is this base in the request" are asked in the two
1703        // coordinate systems the two delta forms use, and both are answered off
1704        // the request itself rather than off the index.
1705        //
1706        // **Offset → position in the request**, not a bare set of offsets. The
1707        // membership question is the same one it always was; what the position
1708        // buys is the base's *oid*, which the re-delta arm below needs in order
1709        // to read the base's content. It is the same single pass over `rows`
1710        // that built the set, so it costs a `usize` per requested object and no
1711        // extra work — and it is emphatically not the whole-repository
1712        // offset→oid scan the old delta-base closure used to do.
1713        let in_set_offsets: HashMap<u64, usize> = rows
1714            .iter()
1715            .enumerate()
1716            .filter_map(|(i, r)| r.as_ref().map(|r| (r.offset, i)))
1717            .collect();
1718        let oid_len = self.hash_kind().oid_len();
1719
1720        // The base of the last boundary entry that was re-deltified, kept so a
1721        // run of entries hanging off one ancestor inflates it once. One slot and
1722        // not an LRU: measured on `h2h-linear-sha1-2048c-1024f-16k`, 961
1723        // boundary entries name 855 distinct stored bases, so there is no reuse
1724        // worth a cache that could hold a repository's worth of objects — and
1725        // this crate is under a live RSS budget.
1726        let mut base_content: Option<(u64, Vec<u8>)> = None;
1727
1728        // What the client already holds, built **lazily and at most once**. A
1729        // clone never asks for it (`thin_haves` is `None`), and a fetch every one
1730        // of whose bases is inside the request never reaches the line that builds
1731        // it either — only a request that actually cuts a delta chain pays, which
1732        // is the only request it can help.
1733        let mut client_bases: Option<ExternalBases> = None;
1734
1735        let mut out = Vec::with_capacity(asked.len());
1736        for (oid, row) in asked.iter().zip(rows.iter()) {
1737            // **An oid this repository does not hold is REFUSED, not dropped.**
1738            //
1739            // This used to `continue`, and that is the same silent-under-send
1740            // shape as the closure defect above: the request asks for N objects,
1741            // the pack carries N-1, the receipt says success and the client is
1742            // the first to find out. It also made the two engines disagree about
1743            // what a missing object *means* — gunnar's in-memory arm has always
1744            // said *"emit_pack was asked for X, which this store does not hold;
1745            // a pack emitter that skipped it would send a pack no client can
1746            // close"* — so a benchmark across the two was comparing two
1747            // contracts.
1748            //
1749            // No legitimate caller relies on the skip. Every set that reaches
1750            // here is built out of this store's own index: `select` answers from
1751            // the reachability bitmaps, and gunnar's own walk resolves each id
1752            // against the store before it selects it. A partial-clone filter is
1753            // no exception and is the case worth naming, because it is the one
1754            // that *looks* like it might be: `--filter=blob:none` **removes**
1755            // oids from the selection, it never adds one the store lacks, so a
1756            // filtered request is a smaller set of objects that are all present
1757            // and this refusal never fires on it.
1758            let Some(row) = row.as_ref() else {
1759                bail!(
1760                    "this pack was asked for {}, which this repository does not hold; emitting \
1761                     the rest would be a short pack reported as a success, so this refuses \
1762                     instead",
1763                    hex::encode(oid)
1764                );
1765            };
1766
1767            // Which base this entry names, if any, and whether the request holds
1768            // it. A `REF_DELTA`'s `delta_base` column is 0 by construction —
1769            // `DeltaBase::as_offset` reports 0 for a ref base and 0 is also the
1770            // no-base sentinel — so its base is read out of the entry's own
1771            // bytes, directly after the type/size varint.
1772            //
1773            // A base that is *outside* the request is carried in **both**
1774            // coordinate systems rather than as a bare `false`, because that is
1775            // what the thin arm below needs: an `OFS_DELTA` knows only where its
1776            // base sits, a `REF_DELTA` knows only what it is called, and
1777            // "does the client hold it" has to be asked in whichever one the
1778            // entry speaks.
1779            let base = match row.obj_type {
1780                ObjType::OfsDelta => match in_set_offsets.get(&row.delta_base) {
1781                    Some(_) => BaseOf::Inside {
1782                        at: row.delta_base,
1783                    },
1784                    None => BaseOf::Outside {
1785                        offset: row.delta_base,
1786                        oid: None,
1787                    },
1788                },
1789                ObjType::RefDelta => {
1790                    let head = self.extent(&snap, row.offset, row.len.min(64))?;
1791                    let (_, _, n) = crate::pack_walk::type_and_size_of(&head)?;
1792                    match head.get(n..n + oid_len) {
1793                        // In the request — **and where**. `rows[i]` is `None`
1794                        // only for an oid this store does not hold, which the
1795                        // refusal above turns into an error the moment the loop
1796                        // reaches it; until then the base is treated as outside,
1797                        // which is the answer that cannot be wrong.
1798                        Some(b) => match seen
1799                            .get(b)
1800                            .and_then(|i| rows[*i].as_ref())
1801                            .map(|r| r.offset)
1802                        {
1803                            Some(at) => BaseOf::Inside { at },
1804                            None => BaseOf::Outside {
1805                                offset: 0,
1806                                oid: Some(b.to_vec()),
1807                            },
1808                        },
1809                        // A truncated entry names no base this can read. It is
1810                        // not "inside", and it is not offered to the client
1811                        // either — it falls through to the whole rebuild, which
1812                        // is the answer that cannot be wrong.
1813                        None => BaseOf::Outside {
1814                            offset: 0,
1815                            oid: None,
1816                        },
1817                    }
1818                }
1819                _ => BaseOf::Whole,
1820            };
1821
1822            if let BaseOf::Whole | BaseOf::Inside { .. } = base {
1823                let inside_at = match base {
1824                    BaseOf::Inside { at } => Some(at),
1825                    _ => None,
1826                };
1827                // **The whole-clone arm, and it reads not one byte here.** The
1828                // entry is recorded as its address in the archive; the bytes are
1829                // resolved against the mapping at emit time and go straight from
1830                // the page cache to the wire. This line used to be a `pread`
1831                // into a fresh `Vec` — 13.8 M of each on a `linux.git` clone, all
1832                // of them held at once, which is the 2314 MB peak RSS
1833                // [`crate::pack_walk::EntryBytes`] documents.
1834                let extent = crate::pack_walk::EntryBytes::Extent {
1835                    offset: row.offset,
1836                    len: row.len,
1837                };
1838                let (stored, obj_type, delta_base) = match (row.obj_type, inside_at) {
1839                    // An `OFS_DELTA` for a client that cannot read one: re-head it
1840                    // as a `REF_DELTA` naming the same base by oid. The compressed
1841                    // payload is the delta stream either way, so this is still a
1842                    // copy in the `recompressed` sense — nothing is inflated — but
1843                    // the concatenation of a new header and an old payload exists
1844                    // nowhere on disk, so it cannot be an extent and is `Owned`.
1845                    (ObjType::OfsDelta, Some(_)) if !ofs_delta_ok => {
1846                        let base_oid = self.oid_at_offset(row.delta_base, &asked, &rows)?;
1847                        let bytes = self.extent(&snap, row.offset, row.len)?;
1848                        (
1849                            crate::pack_walk::EntryBytes::Owned(Self::as_ref_delta(
1850                                &bytes, &base_oid,
1851                            )?),
1852                            ObjType::RefDelta,
1853                            0,
1854                        )
1855                    }
1856                    // 🚨 **A stored `REF_DELTA` whose base is IN THIS PACK is
1857                    // re-headed as an `OFS_DELTA`, and that is not an
1858                    // optimisation.**
1859                    //
1860                    // gitoxide — which is `gunnar_client`, `gunnar`'s own
1861                    // `receive-pack`, and every other gix-based receiver — reads
1862                    // every `OBJ_REF_DELTA` in an incoming pack as naming an
1863                    // object *the receiver already has*.
1864                    // `gix_pack::data::input::LookupRefDeltaObjectsIter` consults
1865                    // the local object database and the bases it has already
1866                    // spliced in, and **nowhere else**: never inside the pack it
1867                    // is reading. A base that is in the pack and not in the
1868                    // receiver's store is `input::Error::NotFound`, and the fetch
1869                    // dies with `The object <base> could not be decoded or wasn't
1870                    // found` — with the whole pack rejected, on a clone whose
1871                    // target is empty by definition, so *every* such entry is
1872                    // fatal. That is `S-003` / gitoxide#2882, and `gunnar-wire`'s
1873                    // `Selection::client_has` carries the same rule for gunnar's
1874                    // own emitter.
1875                    //
1876                    // Stock git resolves it happily, which is why this survived
1877                    // five green `git clone` runs against the store that could
1878                    // not serve one `gunnar_client` fetch (`gunnar.rewrite`
1879                    // round 0 and `gunnar.year` week 0, all four znippy columns,
1880                    // 2026-08-14 — the server logged the same request `served`).
1881                    //
1882                    // The entry keeps its compressed delta stream byte for byte;
1883                    // only the header changes, from "base named by oid" to "base
1884                    // named by distance", which is the naming every receiver in
1885                    // existence resolves inside the pack. This is where such an
1886                    // entry comes from in the first place: `git index-pack
1887                    // --fix-thin` appends the base into the pack and leaves the
1888                    // ref-delta naming it by oid, so any repository pushed to
1889                    // more than once holds them.
1890                    //
1891                    // # What it costs, stated rather than glossed
1892                    //
1893                    // `Owned`, so this is the one arm that gives up the zero-copy
1894                    // property for an entry it does **not** rebuild: the
1895                    // compressed delta stream is memcpy'd once, because a new
1896                    // header concatenated with an old payload exists nowhere on
1897                    // disk and no extent can address it. Bounded by the
1898                    // compressed size of the ref-deltas whose base is in the
1899                    // request — not by the repository, and not by the pack.
1900                    //
1901                    // It could be avoided: `emit_pack` re-encodes every header
1902                    // from `EmitEntry::obj_type` while `header_len` finds the
1903                    // payload from the STORED bytes' own type, so an `Extent`
1904                    // over the untouched ref-delta declared as an `OfsDelta`
1905                    // would emit the identical pack with no copy at all. That is
1906                    // deliberately not done: it makes an `EmitEntry` disagree
1907                    // with the bytes it points at, and the entry model being
1908                    // trustworthy is worth more than the memcpy. If the RSS
1909                    // budget ever says otherwise, the honest form is a third
1910                    // `EntryBytes` variant that carries the new header beside
1911                    // the extent, not a silent disagreement.
1912                    (ObjType::RefDelta, Some(at)) if ofs_delta_ok => {
1913                        let bytes = self.extent(&snap, row.offset, row.len)?;
1914                        (
1915                            crate::pack_walk::EntryBytes::Owned(Self::as_ofs_delta(
1916                                &bytes, oid_len, row.offset, at,
1917                            )?),
1918                            ObjType::OfsDelta,
1919                            at,
1920                        )
1921                    }
1922                    // Everything else copies. A `REF_DELTA` left as one here is
1923                    // the `!ofs_delta_ok` client, which cannot be handed a
1924                    // distance at all — and which is stock git older than 1.4.4,
1925                    // never a gix receiver, since gix has always advertised
1926                    // `ofs-delta`.
1927                    _ => (extent, row.obj_type, row.delta_base),
1928                };
1929                out.push(crate::pack_walk::EmitEntry {
1930                    oid: oid.to_vec(),
1931                    stored,
1932                    obj_type,
1933                    uncompressed_size: row.uncompressed_size,
1934                    delta_base,
1935                    offset: row.offset,
1936                    recompressed: false,
1937                    deltified: false,
1938                });
1939                continue;
1940            }
1941
1942            // ── the base is outside the request ──────────────────────────────
1943            //
1944            // **Thin first.** If the client consented to a thin pack and the
1945            // negotiation vouches that it already holds this base, the entry
1946            // goes out as a `REF_DELTA` naming that base by oid, carrying the
1947            // **same compressed delta stream**. Nothing is inflated, nothing is
1948            // deflated, and the pack is smaller than the whole object by exactly
1949            // the margin the delta was worth. That is what `caps.thin` has always
1950            // been the allowance for.
1951            //
1952            // An `OFS_DELTA` cannot survive this: it names its base by a
1953            // backwards distance *within the pack*, and the base is not in the
1954            // pack. Re-heading it as a `REF_DELTA` is the same header swap the
1955            // no-`ofs-delta` client path does, over an unchanged payload.
1956            if let Some(haves) = thin_haves {
1957                if client_bases.is_none() {
1958                    client_bases = Some(self.client_bases(haves)?);
1959                }
1960                let ext = client_bases
1961                    .as_ref()
1962                    .expect("the client's bases were just built");
1963                let base_oid: Option<Vec<u8>> = match &base {
1964                    // Unreachable: the arm above `continue`s on both. Matched
1965                    // rather than `unreachable!()` so that a future arm falling
1966                    // through here declines the thin path instead of panicking
1967                    // inside a serving thread.
1968                    BaseOf::Whole | BaseOf::Inside { .. } => None,
1969                    BaseOf::Outside {
1970                        oid: Some(named), ..
1971                    } => ext
1972                        .held
1973                        .contains(&OidKey::new(named)?)
1974                        .then(|| named.clone()),
1975                    BaseOf::Outside {
1976                        offset,
1977                        oid: None,
1978                    } => ext.by_offset.get(offset).map(|k| k.as_slice().to_vec()),
1979                };
1980                if let Some(base_oid) = base_oid {
1981                    // A stored `REF_DELTA` already names its base by oid, so its
1982                    // bytes go out untouched and stay an extent. An `OFS_DELTA`
1983                    // has to be re-headed, and the result is new bytes.
1984                    let stored = if row.obj_type == ObjType::OfsDelta {
1985                        let bytes = self.extent(&snap, row.offset, row.len)?;
1986                        crate::pack_walk::EntryBytes::Owned(Self::as_ref_delta(&bytes, &base_oid)?)
1987                    } else {
1988                        crate::pack_walk::EntryBytes::Extent {
1989                            offset: row.offset,
1990                            len: row.len,
1991                        }
1992                    };
1993                    out.push(crate::pack_walk::EmitEntry {
1994                        oid: oid.to_vec(),
1995                        stored,
1996                        obj_type: ObjType::RefDelta,
1997                        uncompressed_size: row.uncompressed_size,
1998                        // The base is NOT in this pack, so nothing may be ordered
1999                        // after it: `topological_order` reads 0 as "names no base
2000                        // in here", which for an external base is the truth.
2001                        delta_base: 0,
2002                        offset: row.offset,
2003                        // Copied. The payload never left the archive's bytes.
2004                        recompressed: false,
2005                        deltified: false,
2006                    });
2007                    continue;
2008                }
2009            }
2010
2011            // The base is not being sent and the client cannot supply it, so the
2012            // stored bytes cannot go out as they are. Two answers remain, and
2013            // the first is worth ~4× the second on the wire.
2014            let (kind, body) = self.content(oid)?.ok_or_else(|| {
2015                anyhow!(
2016                    "{} is a delta whose base is outside this request, so it must be sent whole, \
2017                     and this repository cannot produce its content — neither §14's exploded \
2018                     table nor a re-derivation from the verbatim packs answered",
2019                    hex::encode(oid)
2020                )
2021            })?;
2022
2023            // ── re-delta against a base the request DOES carry ────────────────
2024            //
2025            // The candidate comes from the entry's own stored delta chain: the
2026            // nearest ancestor that is inside the request. That is the packer's
2027            // original similarity judgement, already made and already written
2028            // down — no window, no sort, no scan of the repository.
2029            let candidate = if crate::delta::enabled() {
2030                self.in_request_chain_ancestor(row.delta_base, row.offset, &in_set_offsets)?
2031            } else {
2032                None
2033            };
2034            if let Some(base_at) = candidate {
2035                let at = *in_set_offsets
2036                    .get(&base_at)
2037                    .expect("the ancestor was found by lookup in this very map");
2038                // Reuse the last base if this entry hangs off the same one.
2039                if base_content.as_ref().is_none_or(|(o, _)| *o != base_at) {
2040                    let base_oid = asked[at];
2041                    let (_, base_body) = self.content(base_oid)?.ok_or_else(|| {
2042                        anyhow!(
2043                            "{} is in this request and is the delta base chosen for {}, and this \
2044                             repository cannot produce its content",
2045                            hex::encode(base_oid),
2046                            hex::encode(oid)
2047                        )
2048                    })?;
2049                    base_content = Some((base_at, base_body));
2050                }
2051                let (_, base_body) = base_content.as_ref().expect("just filled");
2052
2053                // `None` is the encoder declining — an unrelated base, or a
2054                // delta that came out no smaller than the object. Falling
2055                // through to the whole rebuild is then strictly right.
2056                if let Some(d) = crate::delta::delta(base_body, &body)? {
2057                    let stored = Self::ofs_delta_entry(&d, row.offset, base_at)?;
2058                    // A client with no `ofs-delta` is served the same computed
2059                    // delta under a `REF_DELTA` head naming the base by oid —
2060                    // the identical header swap the base-inside arm above does,
2061                    // over a payload that is already built.
2062                    // **The `Owned` exception, in its primary shape.** These
2063                    // bytes were computed here and exist on no disk, so no
2064                    // extent addresses them; carrying them on the entry is
2065                    // correct and is bounded by the boundary a narrowed request
2066                    // cuts through (961 of 33 773 entries on the fixture below),
2067                    // never by the repository.
2068                    let (stored, obj_type, delta_base) = if ofs_delta_ok {
2069                        (
2070                            crate::pack_walk::EntryBytes::Owned(stored),
2071                            ObjType::OfsDelta,
2072                            base_at,
2073                        )
2074                    } else {
2075                        (
2076                            crate::pack_walk::EntryBytes::Owned(Self::as_ref_delta(
2077                                &stored, asked[at],
2078                            )?),
2079                            ObjType::RefDelta,
2080                            0,
2081                        )
2082                    };
2083                    out.push(crate::pack_walk::EmitEntry {
2084                        oid: oid.to_vec(),
2085                        stored,
2086                        obj_type,
2087                        uncompressed_size: row.uncompressed_size,
2088                        delta_base,
2089                        offset: row.offset,
2090                        // Inflated and re-deflated, so NOT a copy — and the
2091                        // receipt says which of the two rebuilds ran.
2092                        recompressed: true,
2093                        deltified: true,
2094                    });
2095                    continue;
2096                }
2097            }
2098
2099            // Ship the object whole rather than dragging an unreachable base —
2100            // and its children — into a clone.
2101            out.push(crate::pack_walk::EmitEntry {
2102                oid: oid.to_vec(),
2103                // Inflated and re-deflated from §14's resolved content: the
2104                // `Owned` exception again, and again bounded by the boundary.
2105                stored: crate::pack_walk::EntryBytes::Owned(Self::whole_entry(kind, &body)?),
2106                obj_type: crate::serve::resolved_type(kind),
2107                uncompressed_size: body.len() as u64,
2108                // Whole: it names no base, and nothing may order it after one.
2109                delta_base: 0,
2110                offset: row.offset,
2111                recompressed: true,
2112                deltified: false,
2113            });
2114        }
2115        Ok(out)
2116    }
2117
2118    /// **PHASE 1 of emitting: resolve every entry's stored bytes, in parallel.**
2119    ///
2120    /// Emission splits in two, and the split is not a preference — it is where
2121    /// the ordering dependency actually is:
2122    ///
2123    /// | phase | what it does | why |
2124    /// |---|---|---|
2125    /// | 1, **here**, parallel | address → bytes for every entry | the archive is immutable, so N workers over disjoint (or shared) extents need **no lock at all** |
2126    /// | 2, [`crate::pack_walk::emit_pack`], serial | assign output offsets, encode `OFS_DELTA` distances, write, hash | an `OFS_DELTA` names its base by **distance back in the output pack**, so entry *n* cannot be encoded until every earlier entry's byte length is known |
2127    ///
2128    /// Phase 2 is not parallelisable and no attempt is made: the distance chain
2129    /// is a genuine serial dependency, and a running sha1 is another.
2130    ///
2131    /// # No lock, and why that is a fact rather than an intention
2132    ///
2133    /// [`Self::adopt_journal`] states the storage property this rests on — *"the
2134    /// blob file is append-only and `gc` truncates nothing, so a retired pack's
2135    /// bytes are still exactly where the journal says they are"*. Every byte a
2136    /// snapshot covers is therefore immutable for the life of the file, so the
2137    /// workers below share one `&Mapped` and read it concurrently with no
2138    /// synchronisation of any kind. There is no mutex here; if one were needed
2139    /// the data model would be wrong.
2140    ///
2141    /// # 🚨 This is a fan-out inside the serving tier, and `crate::serve`'s
2142    /// header says that tier is serial
2143    ///
2144    /// It did, and the reason it gave is still true and still respected: the
2145    /// constellation's fan-out primitive has no reentrancy detection, so an
2146    /// unbounded fan-out per request inside an admission gate that already runs
2147    /// 32 transfers spawns W² threads. That is why this is **bounded twice**:
2148    ///
2149    ///  * it does not fan out at all below [`FAN_OUT_AT`] entries, so every fetch
2150    ///    small enough for thread-spawn to dominate runs on the calling thread
2151    ///    exactly as before, and
2152    ///  * it never exceeds [`MAX_RESOLVE_WORKERS`] (override with
2153    ///    `ZNIPPY_GIT_EMIT_WORKERS`), so the worst case is 32 × 4 and not
2154    ///    32 × ncores.
2155    ///
2156    /// # What the fan-out is worth — stated honestly
2157    ///
2158    /// Resolving a mapped extent is a bounds check and a pointer, so the work
2159    /// this parallelises is **not** the resolve itself. It is the two things
2160    /// beside it: the first-touch **page fault** on each entry's header (the
2161    /// grammar is parsed here, which is what forces that fault, and it validates
2162    /// the entry before phase 2 commits a byte to the wire), and the `pread`
2163    /// fallback for any extent the mapping does not cover. On a warm page cache
2164    /// with the whole archive mapped, phase 1 is nearly free and the fan-out
2165    /// buys correspondingly little — the 20 % of cycles this change is aimed at
2166    /// is recovered by *not copying*, not by threads. I could not measure the
2167    /// fan-out's own contribution separately: oden was at loadavg 25.9 while
2168    /// this was written (2026-08-14), and a shared box cannot answer a question
2169    /// that fine.
2170    pub(crate) fn resolve_emit_payloads<'a>(
2171        &self,
2172        entries: &'a [crate::pack_walk::EmitEntry],
2173        snap: &'a crate::archive_map::Mapped,
2174    ) -> Result<Vec<std::borrow::Cow<'a, [u8]>>>
2175    where
2176        Self: Sync,
2177    {
2178        let workers = if entries.len() < FAN_OUT_AT {
2179            1
2180        } else {
2181            emit_workers()
2182        };
2183        self.resolve_with(entries, snap, workers)
2184    }
2185
2186    /// [`Self::resolve_emit_payloads`] with the worker count named rather than
2187    /// decided — `1` runs inline on the calling thread.
2188    ///
2189    /// Split out so the fan-out is **testable at a size a test fixture can
2190    /// reach**. The production threshold is 4096 entries and the corpus these
2191    /// guards run on holds 2687, so a test that went through the front door
2192    /// would exercise the serial arm every time and the gatling arm never;
2193    /// `the_fan_out_resolves_the_identical_bytes_the_serial_pass_does` calls
2194    /// this with 1 and with 4 over the same set and requires the two to agree
2195    /// byte for byte.
2196    pub(crate) fn resolve_with<'a>(
2197        &self,
2198        entries: &'a [crate::pack_walk::EmitEntry],
2199        snap: &'a crate::archive_map::Mapped,
2200        workers: usize,
2201    ) -> Result<Vec<std::borrow::Cow<'a, [u8]>>>
2202    where
2203        Self: Sync,
2204    {
2205        use znippy_zoomies::gatling_forkjoin::gatling_for_each;
2206
2207        let one = |i: usize| -> Result<std::borrow::Cow<'a, [u8]>> {
2208            let e = &entries[i];
2209            let bytes = match &e.stored {
2210                // The bytes live on the entry — the rebuilt-delta exception.
2211                // Borrowed, never cloned: `EntryBytes::Owned` is already the
2212                // only allocation this path makes and copying it would double it.
2213                crate::pack_walk::EntryBytes::Owned(v) => std::borrow::Cow::Borrowed(v.as_slice()),
2214                crate::pack_walk::EntryBytes::Extent { offset, len } => {
2215                    self.extent(snap, *offset, *len)?
2216                }
2217            };
2218            // Parse the entry's own header now. Two things fall out: a malformed
2219            // entry is refused **before** phase 2 has written any of the pack,
2220            // and the first cache line of every entry is touched here — on
2221            // whichever worker got it — rather than one at a time on the serial
2222            // writer.
2223            crate::pack_walk::type_and_size_of(&bytes).with_context(|| {
2224                format!(
2225                    "resolving the stored bytes of {} for emission",
2226                    hex::encode(&e.oid)
2227                )
2228            })?;
2229            Ok(bytes)
2230        };
2231
2232        if workers <= 1 {
2233            return (0..entries.len()).map(one).collect();
2234        }
2235        gatling_for_each(entries.len(), workers, one)
2236            .into_iter()
2237            .collect()
2238    }
2239
2240    /// **Both phases, one call** — the only way a caller in this crate turns an
2241    /// ordered emit set into pack bytes.
2242    ///
2243    /// It is three lines, and it is a function rather than three lines because
2244    /// LAW 5's *fix by construction* applies exactly here: the snapshot has to
2245    /// outlive the slices taken from it, and a caller that took its own snapshot
2246    /// per entry, or dropped it between the phases, would be writing a pack out
2247    /// of a mapping that no longer exists. Routing `emit_oids` and every test
2248    /// through one writer makes that impossible to get wrong twice.
2249    pub(crate) fn emit_ordered(
2250        &self,
2251        ordered: &[crate::pack_walk::EmitEntry],
2252        out: &mut dyn std::io::Write,
2253    ) -> Result<crate::pack_walk::EmitReport>
2254    where
2255        Self: Sync,
2256    {
2257        let snap = self
2258            .archive_snapshot()
2259            .context("mapping the archive to emit")?;
2260        let payloads = self
2261            .resolve_emit_payloads(ordered, &snap)
2262            .context("resolving the emit set's stored bytes")?;
2263        crate::pack_walk::emit_pack(
2264            ordered,
2265            self.hash_kind(),
2266            out,
2267            &|i| Ok(payloads[i].as_ref()),
2268        )
2269    }
2270
2271    /// **The nearest ancestor of `from`'s stored delta chain that is inside the
2272    /// request**, or `None` when the chain leaves the request and never comes
2273    /// back.
2274    ///
2275    /// `from` is the archive offset of a base already known to be *outside* the
2276    /// request; `entry_at` is the offset of the entry that named it.
2277    ///
2278    /// # Why this cannot make a cycle, and why that is not a comment but an
2279    /// invariant
2280    ///
2281    /// The answer is required to satisfy `answer < entry_at`. In a well-formed
2282    /// pack a base always precedes the delta that names it, so every edge in the
2283    /// stored graph already points from a higher offset to a lower one; a new
2284    /// edge with the same property keeps the emitted graph a DAG **by
2285    /// construction**, whatever else the request contains. That matters because
2286    /// the obvious better candidate — the entry's own delta *children*, one
2287    /// revision away instead of two to nine — is exactly the set that would
2288    /// close a cycle: a child's stored base is this entry, so pointing this
2289    /// entry at the child makes `topological_order` unable to order either, and
2290    /// `emit_pack` refuses the set. Measured cost of the restriction on
2291    /// `h2h-linear-sha1-2048c-1024f-16k`: ~1 470 bytes per boundary entry
2292    /// against ~314 for the cyclic candidate, and 5 800 for shipping it whole.
2293    ///
2294    /// An `OFS_DELTA` decreases the offset every hop, so the bound is free
2295    /// there; a `REF_DELTA` names its base by oid and could point anywhere, so
2296    /// the comparison is made rather than assumed.
2297    ///
2298    /// # Cost
2299    ///
2300    /// One 64-byte `pread` per hop, no inflate, no allocation sized by an
2301    /// object — the same walk and the same ceiling as
2302    /// [`crate::serve::GitStore::resolved_type_at`], for the same reason: a
2303    /// pushed pack's back-references are not under this server's control, so a
2304    /// cycle must terminate rather than spin. Measured hop counts on the fixture
2305    /// above: 2 for 392 of the 859 that resolve, 3 for 260, and 9 at the worst.
2306    fn in_request_chain_ancestor(
2307        &self,
2308        from: u64,
2309        entry_at: u64,
2310        in_request: &HashMap<u64, usize>,
2311    ) -> Result<Option<u64>> {
2312        use crate::index_layout::{ObjType, ObjectIndex as _};
2313
2314        /// git's own `pack.depth` ceiling, doubled — the same bound
2315        /// `resolved_type_at` carries and for the same reason.
2316        const MAX_LINKS: usize = 100;
2317        /// Enough of an entry to read its type/size varint plus either an
2318        /// `OFS_DELTA` distance or a `REF_DELTA` oid.
2319        const HEADER_PROBE: u64 = 64;
2320
2321        let oid_len = self.hash_kind().oid_len();
2322        let snap = self.archive_snapshot()?;
2323        let mut at = from;
2324        for _ in 0..MAX_LINKS {
2325            if at != 0 && at < entry_at && in_request.contains_key(&at) {
2326                return Ok(Some(at));
2327            }
2328            if at == 0 {
2329                return Ok(None);
2330            }
2331            // Clamped, not `read_extent`: a short entry at the archive's end
2332            // has fewer than HEADER_PROBE bytes after it (FORGE-SHALLOW).
2333            let head = self.header_probe(&snap, at, HEADER_PROBE)?;
2334            let (t, _, n) = crate::pack_walk::type_and_size_of(&head)?;
2335            at = match t {
2336                // A whole object with no base: the chain ends here, and it was
2337                // not in the request.
2338                ObjType::Commit | ObjType::Tree | ObjType::Blob | ObjType::Tag => {
2339                    return Ok(None)
2340                }
2341                ObjType::OfsDelta => {
2342                    let (distance, _) = crate::pack_walk::ofs_distance_of(&head[n..])?;
2343                    match at.checked_sub(distance) {
2344                        Some(next) if next != 0 => next,
2345                        // A base before the start of the archive is a corrupt
2346                        // back-reference. The whole rebuild is always a correct
2347                        // answer, so this declines rather than failing a clone
2348                        // over an entry it was only ever going to optimise.
2349                        _ => return Ok(None),
2350                    }
2351                }
2352                ObjType::RefDelta => {
2353                    let Some(base_oid) = head.get(n..n + oid_len) else {
2354                        return Ok(None);
2355                    };
2356                    match self.index().lookup(base_oid) {
2357                        Some(row) => row.offset,
2358                        None => return Ok(None),
2359                    }
2360                }
2361            };
2362        }
2363        Ok(None)
2364    }
2365
2366    /// An `OFS_DELTA` pack entry carrying `d`, deflated.
2367    ///
2368    /// The distance written here is the **archive** distance, which is the same
2369    /// coordinate every stored entry's distance is in;
2370    /// [`crate::pack_walk::emit_pack`] rewrites it into output coordinates for
2371    /// every entry it emits, copied or not, so this only has to be well-formed
2372    /// and self-consistent — [`crate::pack_walk::header_len`] parses it back to
2373    /// find where the payload starts.
2374    ///
2375    /// `Compression::fast` for the same reason [`Self::whole_entry`] uses it,
2376    /// and the trade is far better here: the buffer being deflated is the delta,
2377    /// which on the measured fixture is ~1.4 KB against the object's 16 KiB, so
2378    /// this path deflates roughly a **tenth** of the bytes the whole rebuild it
2379    /// replaces does.
2380    fn ofs_delta_entry(d: &[u8], entry_at: u64, base_at: u64) -> Result<Vec<u8>> {
2381        use crate::index_layout::ObjType;
2382        use std::io::Write as _;
2383
2384        let distance = entry_at.checked_sub(base_at).ok_or_else(|| {
2385            anyhow!(
2386                "a computed delta at archive offset {entry_at} names a base at {base_at}, which \
2387                 is after it — an ofs-delta distance is backwards and this would not encode"
2388            )
2389        })?;
2390        let mut out = Vec::with_capacity(d.len() / 2 + 32);
2391        crate::pack_walk::encode_type_and_size(&mut out, ObjType::OfsDelta, d.len() as u64);
2392        crate::pack_walk::encode_ofs_distance(&mut out, distance);
2393        let mut z = flate2::write::ZlibEncoder::new(out, flate2::Compression::fast());
2394        z.write_all(d)?;
2395        Ok(z.finish()?)
2396    }
2397
2398    /// **The objects the receiver already held before this transfer**, in both
2399    /// coordinate systems a delta can name a base in.
2400    ///
2401    /// `haves` is the negotiated common **tips**, exactly as
2402    /// [`git_storage_trait::GitServe::emit_pack`] defines them, and the closure
2403    /// over them is this store's own — the contract deliberately passes tips
2404    /// rather than the walk's exclusion set, because the closure is
2405    /// repository-sized and the engine can rebuild it from its own bitmaps.
2406    ///
2407    /// # This is a VOUCHER, and the caller has to be able to stand behind it
2408    ///
2409    /// Every object named here may be pointed at by a delta the pack does not
2410    /// carry. If the receiver turns out **not** to hold one, its
2411    /// `index-pack --fix-thin` dies with `pack has N unresolved deltas`, so a
2412    /// caller that cannot vouch must not pass `caps.thin`. The case that matters
2413    /// is a **partial clone**: a `blob:none` client's `have` tips reach blobs it
2414    /// was deliberately never sent, so the closure over-states what it holds and
2415    /// a filtered fetch must not be marked thin.
2416    ///
2417    /// # Cost
2418    ///
2419    /// One bitmap union plus one point lookup per held object, and it happens at
2420    /// most once per request — and only on a request that has a base outside
2421    /// itself to place. The `by_offset` map exists because an `OFS_DELTA` knows
2422    /// its base only as an archive offset; building it over what the *client*
2423    /// holds rather than over the repository is what keeps this off the
2424    /// whole-repository scan the closure used to do.
2425    ///
2426    /// **And it is spent in raw bytes now, end to end.** This used to ask
2427    /// [`GitOps::reachable`] — hex out of the ordinal space, `hex::decode`d
2428    /// straight back to bytes, then a third `to_vec` per object to key the
2429    /// offset map, three heap allocations per held object for a set the size of
2430    /// the client's history. It asks [`GitStore::reachable_raw`] instead: one
2431    /// buffer for the closure, one hash table of [`OidKey`]s over it, and
2432    /// nothing per object.
2433    fn client_bases(&self, haves: &[Oid<'_>]) -> Result<ExternalBases> {
2434        use crate::index_layout::ObjectIndex as _;
2435
2436        let held = self.reachable_raw(haves, &[]).context(
2437            "closing over the negotiated common tips, to learn which bases the client can supply \
2438             for itself",
2439        )?;
2440        let refs: Vec<&[u8]> = held.iter().collect();
2441        let rows = self.index().lookup_batch(&refs);
2442        let mut by_offset = HashMap::with_capacity(refs.len());
2443        let mut keys = std::collections::HashSet::with_capacity(refs.len());
2444        for (oid, row) in refs.iter().zip(rows.iter()) {
2445            let key = OidKey::new(oid)?;
2446            if let Some(row) = row {
2447                by_offset.insert(row.offset, key);
2448            }
2449            keys.insert(key);
2450        }
2451        Ok(ExternalBases {
2452            by_offset,
2453            held: keys,
2454        })
2455    }
2456
2457    /// The oid of the entry at `offset`, looked up **in the request** — which is
2458    /// the only place it can be, because this is called for a base the request
2459    /// was just shown to contain.
2460    ///
2461    /// Linear over the request rather than a map, because it runs only on the
2462    /// `ofs-delta`-less client path: a capability no git since 1.4.4 omits, and
2463    /// one that has to be *advertised absent* to reach here at all.
2464    fn oid_at_offset(
2465        &self,
2466        offset: u64,
2467        asked: &[&[u8]],
2468        rows: &[Option<crate::index_layout::IndexRow>],
2469    ) -> Result<Vec<u8>> {
2470        asked
2471            .iter()
2472            .zip(rows)
2473            .find(|(_, r)| r.as_ref().is_some_and(|r| r.offset == offset))
2474            .map(|(oid, _)| oid.to_vec())
2475            .ok_or_else(|| {
2476                anyhow!(
2477                    "the entry at archive offset {offset} was checked to be in this request and \
2478                     then could not be found in it"
2479                )
2480            })
2481    }
2482
2483    /// Re-head a stored `OFS_DELTA` as a `REF_DELTA` naming `base_oid`.
2484    ///
2485    /// The compressed delta stream is copied unchanged; only the header differs,
2486    /// because the two forms differ in **how the base is named** and in nothing
2487    /// else. This is why a client without `ofs-delta` costs bytes and not CPU.
2488    fn as_ref_delta(stored: &[u8], base_oid: &[u8]) -> Result<Vec<u8>> {
2489        use crate::index_layout::ObjType;
2490        let (t, stated_size, n) = crate::pack_walk::type_and_size_of(stored)?;
2491        if t != ObjType::OfsDelta {
2492            bail!("only an ofs-delta can be re-headed as a ref-delta, this entry is {t:?}");
2493        }
2494        let (_, d) = crate::pack_walk::ofs_distance_of(stored.get(n..).unwrap_or(&[]))?;
2495        let mut out = Vec::with_capacity(stored.len() + base_oid.len());
2496        crate::pack_walk::encode_type_and_size(&mut out, ObjType::RefDelta, stated_size);
2497        out.extend_from_slice(base_oid);
2498        out.extend_from_slice(stored.get(n + d..).unwrap_or(&[]));
2499        Ok(out)
2500    }
2501
2502    /// Re-head a stored `REF_DELTA` as an `OFS_DELTA` naming the base that sits
2503    /// at `base_at` — **the inverse of [`Self::as_ref_delta`]**, and the fix for
2504    /// the one pack shape gitoxide cannot read.
2505    ///
2506    /// The compressed delta stream is copied unchanged; only the header differs,
2507    /// because the two forms differ in how the base is *named* and in nothing
2508    /// else. The reason this has to exist is in the caller: a `REF_DELTA` whose
2509    /// base is inside the same pack is resolved by stock git and refused by
2510    /// every gix-based receiver.
2511    ///
2512    /// # The distance written here is a placeholder, and that is by design
2513    ///
2514    /// Exactly as [`Self::ofs_delta_entry`] documents: the distance an
2515    /// `OFS_DELTA` carries is relative to a position in the pack it is *in*, so
2516    /// [`crate::pack_walk::emit_pack`] re-encodes it for every entry from the
2517    /// output offsets it is assigning, and [`crate::pack_walk::header_len`]
2518    /// parses whatever is written here only to find where the payload starts. It
2519    /// therefore has to be well-formed and self-consistent, and nothing more.
2520    ///
2521    /// It cannot simply be `entry_at - base_at`: a `REF_DELTA` is allowed to
2522    /// name a base that comes **after** it in the pack, and `git index-pack
2523    /// --fix-thin` produces exactly that — it appends the base at the end. So a
2524    /// forward reference encodes as `1`, and `emit_pack` writes the real
2525    /// backwards distance once `topological_order` has put the base first.
2526    fn as_ofs_delta(
2527        stored: &[u8],
2528        oid_len: usize,
2529        entry_at: u64,
2530        base_at: u64,
2531    ) -> Result<Vec<u8>> {
2532        use crate::index_layout::ObjType;
2533        let (t, stated_size, n) = crate::pack_walk::type_and_size_of(stored)?;
2534        if t != ObjType::RefDelta {
2535            bail!("only a ref-delta can be re-headed as an ofs-delta, this entry is {t:?}");
2536        }
2537        let payload = stored.get(n + oid_len..).ok_or_else(|| {
2538            anyhow!(
2539                "a ref-delta entry at archive offset {entry_at} is {} bytes, which is not even its \
2540                 header plus a {oid_len}-byte base oid",
2541                stored.len()
2542            )
2543        })?;
2544        let mut out = Vec::with_capacity(payload.len() + 32);
2545        crate::pack_walk::encode_type_and_size(&mut out, ObjType::OfsDelta, stated_size);
2546        crate::pack_walk::encode_ofs_distance(
2547            &mut out,
2548            entry_at.checked_sub(base_at).filter(|d| *d > 0).unwrap_or(1),
2549        );
2550        out.extend_from_slice(payload);
2551        Ok(out)
2552    }
2553
2554    /// A whole (non-delta) pack entry for `body`: type/size header, then the
2555    /// body deflated.
2556    ///
2557    /// The **only** place in this crate that deflates on a serving path, and the
2558    /// only reason it exists is a delta whose base the request does not contain.
2559    /// `Compression::fast` deliberately: these bytes are re-compressed because
2560    /// they *cannot* be copied, so the trade is wire size against a clone's
2561    /// latency, and the entries this runs on are a small boundary fraction of any
2562    /// real request.
2563    fn whole_entry(kind: crate::object::GitObjectKind, body: &[u8]) -> Result<Vec<u8>> {
2564        use std::io::Write as _;
2565        let mut out = Vec::with_capacity(body.len() / 2 + 32);
2566        crate::pack_walk::encode_type_and_size(
2567            &mut out,
2568            crate::serve::resolved_type(kind),
2569            body.len() as u64,
2570        );
2571        let mut z = flate2::write::ZlibEncoder::new(out, flate2::Compression::fast());
2572        z.write_all(body)?;
2573        Ok(z.finish()?)
2574    }
2575
2576    /// Take the absorb gate and hold it.
2577    ///
2578    /// The only way to observe "a read that arrived before the drain" as a
2579    /// **state** rather than as a race: with this held, the worker is parked at
2580    /// the top of its absorb, so the pack is provably durable, provably queued
2581    /// and provably not indexed. Its guard is in this module's tests, and nothing
2582    /// on a serving path takes it.
2583    #[cfg(test)]
2584    pub(crate) fn hold_absorb_gate(&self) -> std::sync::MutexGuard<'_, ()> {
2585        self.absorber.gate.lock().expect("absorb gate poisoned")
2586    }
2587
2588    /// `pread` an extent out of the verbatim archive, into a fresh `Vec`.
2589    ///
2590    /// **The fallback and the owning path.** A caller that can hold a borrow
2591    /// wants [`Self::archive_snapshot`] plus [`Self::extent`] instead: this one
2592    /// costs a syscall, an allocation and a kernel→user copy, which is the shape
2593    /// [`crate::archive_map`] exists to get off the emit path.
2594    pub(crate) fn read_extent(&self, offset: u64, len: u64) -> Result<Vec<u8>> {
2595        self.absorber.read_extent(offset, len)
2596    }
2597
2598    /// One mapping of the verbatim archive, to be taken **once per operation**
2599    /// and sliced N times. See [`crate::archive_map::ArchiveMap::snapshot`].
2600    pub(crate) fn archive_snapshot(&self) -> Result<Arc<crate::archive_map::Mapped>> {
2601        self.absorber.map.snapshot()
2602    }
2603
2604    /// **The one resolver**: an extent's bytes, borrowed from `snap` when it
2605    /// covers them and `pread` into a fresh buffer when it does not.
2606    ///
2607    /// `Cow::Borrowed` is the ordinary answer and the whole point — no syscall,
2608    /// no allocation, no copy, just a bounds-checked slice of the page cache.
2609    /// `Cow::Owned` happens for an extent past the mapped end, which means the
2610    /// blob file grew after the snapshot was taken (a push landed mid-clone).
2611    /// `Mapped::get` returning `None` is *"go and pread it"* and never *"there
2612    /// are no bytes"*, so the two arms return the **same bytes** and differ only
2613    /// in what they cost — which is the property
2614    /// `an_extent_past_the_mapping_falls_back_and_returns_the_same_bytes`
2615    /// asserts.
2616    pub(crate) fn extent<'m>(
2617        &self,
2618        snap: &'m crate::archive_map::Mapped,
2619        offset: u64,
2620        len: u64,
2621    ) -> Result<std::borrow::Cow<'m, [u8]>> {
2622        match snap.get(offset, len) {
2623            Some(b) => Ok(std::borrow::Cow::Borrowed(b)),
2624            None => Ok(std::borrow::Cow::Owned(self.read_extent(offset, len)?)),
2625        }
2626    }
2627
2628    /// **A header probe: AT MOST `len` bytes at `offset`, fewer at the archive's end.**
2629    ///
2630    /// [`Self::extent`] is for an extent the index vouches for and refuses a
2631    /// short read, which is right there: a short extent is a truncated archive.
2632    /// A header probe is not such an extent. It asks for a fixed ceiling
2633    /// ([`crate::serve`]'s `HEADER_PROBE`) to cover the longest header, and an
2634    /// entry shorter than that at the END of the archive has fewer bytes after
2635    /// it. That is not truncation and refusing it cost a live repository every
2636    /// shallow clone (FORGE-SHALLOW, git.gunnar.rs 2026-10-28: edda's 41-byte
2637    /// ref-delta, `short read at (16843881, 64)`). The parsers read a grammar,
2638    /// not a fixed width, so they take what is there. The same clamp
2639    /// [`ArchiveWindow`] gives [`crate::resolve::materialize_at`].
2640    pub(crate) fn header_probe<'m>(
2641        &self,
2642        snap: &'m crate::archive_map::Mapped,
2643        offset: u64,
2644        len: u64,
2645    ) -> Result<std::borrow::Cow<'m, [u8]>> {
2646        clamped_window(&self.absorber, snap, offset, len)
2647    }
2648
2649    /// Record a durable pack as pending index work.
2650    ///
2651    /// The **bit going clear**, and the ack path's last act before it returns.
2652    pub(crate) fn queue(&self, pack_id: u64, extent: Extent) -> Result<()> {
2653        self.absorber.queue(pack_id, extent)
2654    }
2655
2656    fn reach_bitmaps(&self) -> Result<Arc<Vec<ReachEntry>>> {
2657        self.absorber.reach_bitmaps()
2658    }
2659
2660    /// [`Absorber::reach_bitmaps_with`], for the differential guard.
2661    pub(crate) fn reach_bitmaps_with(
2662        &self,
2663        policy: crate::reach::ReachPolicy,
2664        cache: bool,
2665    ) -> Result<Arc<Vec<ReachEntry>>> {
2666        self.absorber.reach_bitmaps_with(policy, cache)
2667    }
2668
2669    /// [`Absorber::reach_tables_with`] — the reach table paired with its oid
2670    /// index, for the negotiation.
2671    pub(crate) fn reach_tables_with(
2672        &self,
2673        policy: crate::reach::ReachPolicy,
2674        cache: bool,
2675    ) -> Result<(Arc<Vec<ReachEntry>>, Arc<HashMap<String, u32>>)> {
2676        self.absorber.reach_tables_with(policy, cache)
2677    }
2678
2679    /// The commit oid set, raw, shared. See [`Derived::commit_raw`].
2680    pub(crate) fn commit_oids_raw(
2681        &self,
2682    ) -> Result<Option<Arc<std::collections::HashSet<Vec<u8>>>>> {
2683        self.absorber.commit_oids_raw()
2684    }
2685
2686    pub(crate) fn refold(&self) -> Result<()> {
2687        self.absorber.refold()
2688    }
2689}
2690
2691impl<S: ObjectIndex> Absorber<S> {
2692    /// `shared` is the writer's descriptor on the same file
2693    /// ([`crate::archive_write::ArchiveWrite::blobs_shared`]); `None` opens one,
2694    /// which is what a caller with no writer does.
2695    fn open(
2696        blobs: &Path,
2697        shared: Option<Arc<File>>,
2698        hash: GitHashKind,
2699        objects: ObjectReadStack<S>,
2700        exploded: Arc<ExplodedCache>,
2701    ) -> Result<Self> {
2702        let reader = match shared {
2703            Some(f) => f,
2704            None => Arc::new(
2705                File::open(blobs)
2706                    .with_context(|| format!("opening {} for reads", blobs.display()))?,
2707            ),
2708        };
2709        let label = blobs
2710            .parent()
2711            .map(|p| p.display().to_string())
2712            .unwrap_or_else(|| blobs.display().to_string());
2713        let graph_cache = crate::arms::graph_cache_from_env()?;
2714        if !graph_cache {
2715            // A memo left by an earlier open with the cache on: this store keeps none.
2716            let stale = blobs.with_file_name(GRAPH_CACHE_FILE);
2717            match std::fs::remove_file(&stale) {
2718                Ok(()) => {}
2719                Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
2720                Err(e) => tracing::warn!(
2721                    target: "znippy.git",
2722                    path = %stale.display(),
2723                    error = %e,
2724                    "the graph cache is off but a stale objects.graph could not be removed"
2725                ),
2726            }
2727        }
2728        Ok(Self {
2729            blobs: blobs.to_path_buf(),
2730            reader,
2731            map: crate::archive_map::ArchiveMap::new(blobs),
2732            hash,
2733            objects,
2734            exploded,
2735            derived: RwLock::new(Derived::default()),
2736            graph_read: std::sync::atomic::AtomicU64::new(0),
2737            graph_recomputed: std::sync::atomic::AtomicU64::new(0),
2738            graph_cache: std::sync::atomic::AtomicBool::new(graph_cache),
2739            reach_carried: std::sync::atomic::AtomicU64::new(0),
2740            reach_rebuilt: std::sync::atomic::AtomicU64::new(0),
2741            ordinal_folded: std::sync::atomic::AtomicU64::new(0),
2742            ordinal_tail: std::sync::atomic::AtomicU64::new(0),
2743            packs: Mutex::new(PackState::default()),
2744            gate: Mutex::new(()),
2745            progress: crate::progress::IndexProgress::new(&label),
2746            cancel: std::sync::atomic::AtomicBool::new(false),
2747            label,
2748            watchdog: RwLock::new(crate::memguard::global()),
2749            scanned: Mutex::new(HashMap::new()),
2750            scan_build: Mutex::new(()),
2751            unindexable: Mutex::new(HashMap::new()),
2752        })
2753    }
2754
2755    /// **Tree payloads, on demand** — [`crate::reach::TreeSource`] over this
2756    /// store.
2757    ///
2758    /// One lookup per tree the walk actually visits, instead of a resident map
2759    /// of every tree in the repository. Where the bytes come from is
2760    /// [`Absorber::resolved`]'s business and nobody else's: the exploded cache
2761    /// if the tree is in it, its own delta chain out of the mmapped pack if it
2762    /// is not — and the read then writes it back, so a second walk finds it
2763    /// warm.
2764    ///
2765    /// It must not take the `derived` lock, and it does not: `resolved` reads
2766    /// the exploded pools, the `objects` index and the pending packs, none of
2767    /// which is `derived`. Both callers build one of these **inside** their
2768    /// `derived.write()` section, and that is only sound because of this
2769    /// sentence.
2770    fn tree_source(&self) -> TreesFromStore<'_, S> {
2771        TreesFromStore { abs: self }
2772    }
2773
2774    /// Packs whose objects are not in the index yet — §13.12's bit, counted.
2775    fn unindexed_packs(&self) -> usize {
2776        self.packs.lock().expect("pack state lock").unabsorbed
2777    }
2778
2779    /// **Index everything durable, here, now — the ack path's half of §13.9.**
2780    ///
2781    /// # The two hours, in one function
2782    ///
2783    /// MEASURED by lane P2 on the oden rig 2026-08-30. `git.receive_pack.checking`
2784    /// fired at 07:43:49; its very first `peel` of a pushed tip missed the
2785    /// exploded table and missed the `objects` index (the worker was minutes
2786    /// into a 13 948 244-object pack), and fell into [`Self::scan_pending`],
2787    /// which ran **a second, read-only, throw-away resolve of the whole 8.12 GiB
2788    /// pack on the connection thread**:
2789    ///
2790    /// ```text
2791    /// 09:20:21  index.scanned  "a reader scanned a pending pack for itself:
2792    ///                           13948244 objects in 5792442 ms"
2793    /// ```
2794    ///
2795    /// 1h36m32s on the 16 GB box and 1h44m14s on the 8 GB one, for rows it then
2796    /// threw away with the comment *"keeping them would be the index, and the
2797    /// worker is building that"*. That sentence is the defect: two resolvers
2798    /// over the same bytes, each holding its own O(pack) `PackWalk::entries` and
2799    /// oid table, on four vCPUs — which is also **why the watchdog paused**. The
2800    /// reader's second copy is what ate the `MemAvailable` the indexer tripped
2801    /// over, and neither pass ever finished the check.
2802    ///
2803    /// # Who may call this
2804    ///
2805    /// **The ack path, and nothing else.** It takes [`Absorber::gate`], so it
2806    /// waits for whoever is absorbing — and *"nobody waits on the indexer"* is a
2807    /// law with its own red test
2808    /// (`a_read_before_the_drain_is_served_from_the_pack_and_never_waits`): a
2809    /// clone, an `ls-remote`, a `control status` must be answered while the
2810    /// drain is parked, and they still are, through [`Self::scan_pending`].
2811    ///
2812    /// The connectivity check is the one caller for which that is the wrong
2813    /// trade. It has to prove that every object reachable from 2874 new tips is
2814    /// present, which means reading them; an entry's oid exists only once its
2815    /// delta chain has been applied; so the pack must be resolved before the
2816    /// check can answer, and the only question is whether it is resolved **once
2817    /// or twice**. This makes it once. `git receive-pack` makes the same choice
2818    /// and calls it `index-pack`.
2819    ///
2820    /// If the account worker already holds the gate, this blocks on it and then
2821    /// finds `is_absorbed` set and returns: whoever got there first did the work.
2822    ///
2823    /// Returns how many packs this call absorbed; `0` means it could not (it is
2824    /// nested inside an absorb of the only pending pack, or there was nothing
2825    /// to do).
2826    fn index_pending_now(&self) -> Result<usize> {
2827        if self.unindexed_packs() == 0 {
2828            return Ok(0);
2829        }
2830        // **Nested**: this thread is already inside `absorb_gated` and holds the
2831        // gate, so taking it again is a deadlock, not a wait. The other pending
2832        // packs are still ours to absorb inline — that is lane U's case, a thin
2833        // push whose base sits in a pack pushed a moment earlier — and the one
2834        // we are in the middle of is not.
2835        if let Some(current) = IN_ABSORB.with(|c| c.get()) {
2836            let jobs: Vec<PendingPack> = {
2837                let s = self.packs.lock().map_err(|_| anyhow!("pack state poisoned"))?;
2838                s.pending()
2839            };
2840            return self.absorb_each(&jobs, Some(current));
2841        }
2842        let _gate = self.gate.lock().map_err(|_| anyhow!("absorb gate poisoned"))?;
2843        let jobs: Vec<PendingPack> = {
2844            let s = self.packs.lock().map_err(|_| anyhow!("pack state poisoned"))?;
2845            s.pending()
2846        };
2847        // `absorb_gated` is a no-op for a pack somebody absorbed while we were
2848        // queued on the gate, so the loser of the race pays nothing.
2849        self.absorb_each(&jobs, None)
2850    }
2851
2852    /// **Absorb the pending packs only if nobody else is absorbing.**
2853    ///
2854    /// `Some(n)` = the gate was free and this caller did the work; `None` = the
2855    /// worker (or another reader) holds the gate — possibly paused by the
2856    /// memory watchdog for as long as the box is short — and this caller did
2857    /// **not** wait for it. The owner's rule, 2026-08-30: *nobody waits on the
2858    /// indexer.* A read that needs the index now answers from the pack for
2859    /// itself ([`Self::scan_pending`]) and a read that needs the whole graph
2860    /// says so by name rather than blocking behind a kernel pack.
2861    fn try_absorb_pending(&self) -> Result<Option<usize>> {
2862        if self.unindexed_packs() == 0 {
2863            return Ok(Some(0));
2864        }
2865        let _gate = match self.gate.try_lock() {
2866            Ok(g) => g,
2867            Err(std::sync::TryLockError::WouldBlock) => return Ok(None),
2868            Err(std::sync::TryLockError::Poisoned(_)) => bail!("absorb gate poisoned"),
2869        };
2870        let jobs: Vec<PendingPack> = {
2871            let s = self
2872                .packs
2873                .lock()
2874                .map_err(|_| anyhow!("pack state poisoned"))?;
2875            s.pending()
2876        };
2877        self.absorb_each(&jobs, None).map(Some)
2878    }
2879
2880    /// **A read served from the pack on the reader's own request, never by
2881    /// waiting for the worker.**
2882    ///
2883    /// Every oid in `wants` is looked up in the `objects` table first — the
2884    /// rows the worker has already landed, batch by batch, are visible there
2885    /// with no lock in common with it. The ones still missing, if any pack is
2886    /// still un-indexed, are found by **resolving the pending packs read-only
2887    /// on this thread**: the same resolver the worker runs
2888    /// ([`crate::resolve::resolve_streaming`]), over the same mmapped bytes,
2889    /// with a bounded base set, but with **no sink, no gate and no rows
2890    /// written** — it produces rows for the caller and nothing else. The pass
2891    /// stops the moment every wanted oid has been seen.
2892    ///
2893    /// `capture` names one oid whose **content** the caller wants as well (a
2894    /// [`GitStore::content`] read); it rides through a
2895    /// [`crate::exploded::CaptureOne`] sink and comes back in the second slot.
2896    ///
2897    /// # Cost, stated
2898    ///
2899    /// An oid that is in a pending pack costs one read-only pass up to where it
2900    /// sits; an oid that is **absent** costs a full pass over every pending
2901    /// pack — for a kernel pack, minutes of one core. That is the price of
2902    /// never answering "absent" for an object whose bytes are durable, paid by
2903    /// the reader that asked and by nobody else. The rows it computes are not
2904    /// kept: keeping them would be the index, and the worker is building that.
2905    fn scan_pending(
2906        &self,
2907        wants: &[&[u8]],
2908        capture: Option<&[u8]>,
2909    ) -> Result<(Vec<Option<crate::index_layout::IndexRow>>, Option<(GitObjectKind, Vec<u8>)>)> {
2910        use crate::index_layout::IndexRow;
2911        let mut out: Vec<Option<IndexRow>> = vec![None; wants.len()];
2912        let mut missing: HashMap<&[u8], Vec<usize>> = HashMap::with_capacity(wants.len());
2913        for (i, w) in wants.iter().enumerate() {
2914            missing.entry(*w).or_default().push(i);
2915        }
2916        if missing.is_empty() && capture.is_none() {
2917            return Ok((out, None));
2918        }
2919        // **No nested BUILD.** The resolver asks this absorber (as its
2920        // `BaseSource`) for a thin pack's external base, and that base may
2921        // itself be un-indexed — a second *scan build* started from inside the
2922        // first, and a third from inside that, without end.
2923        //
2924        // The guard used to answer `None` outright when re-entered, and that
2925        // was wider than the recursion it was written for: a base sitting in
2926        // **another pack that is already scanned** is one binary search away,
2927        // and refusing it made a thin push on top of a just-acked one fail
2928        // `[remote rejected] … missing necessary objects` — lane U's rig
2929        // finding, 2026-08-30. So a nested call now consults the scans that
2930        // exist and builds none: a lookup terminates, a build is what does not.
2931        thread_local! { static SCANNING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) }; }
2932        let nested = SCANNING.with(|s| s.get());
2933        struct Unset(bool);
2934        impl Drop for Unset {
2935            fn drop(&mut self) {
2936                if !self.0 {
2937                    SCANNING.with(|s| s.set(false));
2938                }
2939            }
2940        }
2941        SCANNING.with(|s| s.set(true));
2942        let _unset = Unset(nested);
2943        let pending: Vec<PendingPack> = {
2944            let s = self
2945                .packs
2946                .lock()
2947                .map_err(|_| anyhow!("pack state poisoned"))?;
2948            s.pending()
2949        };
2950        if pending.is_empty() {
2951            return Ok((out, None));
2952        }
2953        let snap = self.map.snapshot()?;
2954        let mut content: Option<(GitObjectKind, Vec<u8>)> = None;
2955        for job in &pending {
2956            if missing.is_empty() && (capture.is_none() || content.is_some()) {
2957                break;
2958            }
2959            let scanned = match self.cached_scan(job.offset) {
2960                Some(s) => s,
2961                // Inside another scan: no build, so the recursion the guard
2962                // exists for cannot happen, and a pack nobody has scanned yet
2963                // is simply not consulted for this base.
2964                None if nested => continue,
2965                None => self.scanned_pack(&snap, job)?,
2966            };
2967            missing.retain(|oid, slots| match scanned.row(oid) {
2968                Some(r) => {
2969                    for &i in slots.iter() {
2970                        out[i] = Some(r.clone());
2971                    }
2972                    false
2973                }
2974                None => true,
2975            });
2976            if let Some(want) = capture
2977                && content.is_none()
2978                && let Some((entry, _)) = scanned.find(want)
2979            {
2980                let bytes: std::borrow::Cow<'_, [u8]> = match snap.get(job.offset, job.len) {
2981                    Some(b) => std::borrow::Cow::Borrowed(b),
2982                    None => std::borrow::Cow::Owned(self.read_extent(job.offset, job.len)?),
2983                };
2984                let sp = scanned.clone();
2985                let offset_of = move |oid: &[u8]| sp.find(oid).map(|(e, _)| sp.walk.entries[e].offset);
2986                content = Some(crate::resolve::materialize_entry(
2987                    &bytes,
2988                    &scanned.walk,
2989                    entry,
2990                    self.hash.oid_len(),
2991                    &offset_of,
2992                    self,
2993                )?);
2994            }
2995        }
2996        Ok((out, content))
2997    }
2998
2999    /// The scan of one pending pack **if it is already built** — a lookup, and
3000    /// nothing that could recurse. What a nested `scan_pending` is allowed.
3001    fn cached_scan(&self, offset: u64) -> Option<Arc<ScannedPack>> {
3002        self.scanned
3003            .lock()
3004            .unwrap_or_else(|e| e.into_inner())
3005            .get(&offset)
3006            .cloned()
3007    }
3008
3009    /// The scan of one pending pack: cached, or built now by one read-only
3010    /// pass (the same resolver, bounded bases, no sink, no rows written).
3011    fn scanned_pack(&self, snap: &crate::archive_map::Mapped, job: &PendingPack) -> Result<Arc<ScannedPack>> {
3012        if let Some(s) = self.scanned.lock().unwrap_or_else(|e| e.into_inner()).get(&job.offset) {
3013            return Ok(s.clone());
3014        }
3015        let _one_at_a_time = self.scan_build.lock().unwrap_or_else(|e| e.into_inner());
3016        if let Some(s) = self.scanned.lock().unwrap_or_else(|e| e.into_inner()).get(&job.offset) {
3017            return Ok(s.clone());
3018        }
3019        let t0 = std::time::Instant::now();
3020        let bytes: std::borrow::Cow<'_, [u8]> = match snap.get(job.offset, job.len) {
3021            Some(b) => std::borrow::Cow::Borrowed(b),
3022            None => std::borrow::Cow::Owned(self.read_extent(job.offset, job.len)?),
3023        };
3024        let walk = crate::pack_walk::walk(&bytes, self.hash.oid_len())?;
3025        let mut rows: Vec<(Vec<u8>, u32, u64)> = Vec::with_capacity(walk.entries.len());
3026        // A reader's own budget: a quarter of the worker's, no watchdog —
3027        // the pause is the worker's and only the worker's.
3028        let limits = crate::resolve::ResolveLimits {
3029            base_budget: (crate::resolve::base_budget_from_env() / 4).max(1),
3030            cancel: Some(&self.cancel),
3031            label: &self.label,
3032            ..crate::resolve::ResolveLimits::none()
3033        };
3034        // Entry index = position in offset order; the rows come out in
3035        // resolution order, so map offset → index by binary search.
3036        let report = crate::resolve::resolve_streaming(
3037            &bytes,
3038            &walk,
3039            self.hash,
3040            job.offset,
3041            self,
3042            &crate::exploded::NoSink,
3043            limits,
3044            &mut |batch| {
3045                for r in batch.drain(..) {
3046                    let pack_off = r.offset - job.offset;
3047                    let idx = walk
3048                        .entries
3049                        .binary_search_by_key(&pack_off, |e| e.offset)
3050                        .map_err(|_| anyhow!("resolved row at {pack_off} has no walk entry"))?;
3051                    rows.push((r.oid, idx as u32, r.uncompressed_size));
3052                }
3053                Ok(())
3054            },
3055        )
3056        .context("resolving a pending pack read-only for lookups")?;
3057        rows.sort_unstable_by(|a, b| a.0.cmp(&b.0));
3058        let scanned = Arc::new(ScannedPack {
3059            walk,
3060            rows,
3061            archive_offset: job.offset,
3062        });
3063        tracing::info!(
3064            target: "gunnar::index",
3065            event = "index.scanned",
3066            repo = %self.label,
3067            pack_id = job.pack_id,
3068            objects = scanned.rows.len(),
3069            reinflations = report.reinflations,
3070            elapsed_ms = t0.elapsed().as_millis() as u64,
3071            "a reader scanned a pending pack for itself: {} objects in {} ms",
3072            scanned.rows.len(),
3073            t0.elapsed().as_millis()
3074        );
3075        self.scanned
3076            .lock()
3077            .unwrap_or_else(|e| e.into_inner())
3078            .insert(job.offset, scanned.clone());
3079        Ok(scanned)
3080    }
3081
3082    /// **Drop everything this store holds that can be rebuilt** — what the
3083    /// watchdog's flush reaches: the readers' scanned packs (`scanned`, ~1.2
3084    /// GB on linux, rebuilt by the next lookup's pass) and the exploded
3085    /// table's oid tree and batch cache. The rows on disk are untouched.
3086    ///
3087    /// **Returns the rows it actually dropped**, and that number is the point.
3088    /// Lane P2's rig logged this event 190 and 627 times with
3089    /// `scanned_rows_dropped=0` on every single one and kept waiting anyway,
3090    /// because the caller could not tell a spill that freed a gigabyte from one
3091    /// that freed nothing. Three zeros in a row now end the pause
3092    /// ([`crate::memguard::PauseEnd::NothingToGive`]).
3093    ///
3094    /// The exploded table's own caches are an mmap index and a small batch
3095    /// cache; they are dropped here too and deliberately not counted, because
3096    /// they are not where a kernel-sized pack's memory is.
3097    fn spill(&self) -> u64 {
3098        let dropped = {
3099            let mut s = self.scanned.lock().unwrap_or_else(|e| e.into_inner());
3100            let n: usize = s.values().map(|p| p.rows.len()).sum();
3101            s.clear();
3102            n
3103        };
3104        self.exploded.drop_caches();
3105        tracing::info!(
3106            target: "gunnar::index",
3107            event = "index.spilled",
3108            repo = %self.label,
3109            scanned_rows_dropped = dropped,
3110            "the store dropped its rebuildable caches for the memory watchdog"
3111        );
3112        dropped as u64
3113    }
3114
3115    /// This store's watchdog (see the field).
3116    fn watchdog(&self) -> Arc<crate::memguard::MemWatchdog> {
3117        self.watchdog.read().unwrap_or_else(|e| e.into_inner()).clone()
3118    }
3119
3120    /// One oid, never waiting: the index, then the pending packs.
3121    fn lookup_never_wait(&self, oid: &[u8]) -> Result<Option<crate::index_layout::IndexRow>> {
3122        if let Some(r) = self.objects.lookup(oid) {
3123            return Ok(Some(r));
3124        }
3125        // **Check-then-act, and the answer is to look again, not to wait.**
3126        // The index miss above and the bit read below are two reads with the
3127        // drain running between them: the pack's rows can land AND its bit
3128        // clear after the miss, and "no pending pack, so absent" was then a
3129        // lie about a durable object — the one answer §13.12 forbids
3130        // (`no_read_is_told_absent_while_the_drain_is_running` caught it,
3131        // 1 absent answer in 1352, 2 of 10 full-suite runs under load,
3132        // 2026-09-28). The bit clears only AFTER the rows go in, so once it
3133        // reads clear a second index read is authoritative.
3134        if self.unindexed_packs() == 0 {
3135            return Ok(self.objects.lookup(oid));
3136        }
3137        let (mut rows, _) = self.scan_pending(&[oid], None)?;
3138        if let Some(r) = rows.pop().flatten() {
3139            return Ok(Some(r));
3140        }
3141        // The same window one step later: the pack was absorbed and left the
3142        // pending list between the bit read and the scan.
3143        Ok(self.objects.lookup(oid))
3144    }
3145
3146    /// Many oids, never waiting: one index pass, then one read-only pass over
3147    /// the pending packs for whatever the index did not have.
3148    fn lookup_batch_never_wait(&self, oids: &[&[u8]]) -> Result<Vec<Option<crate::index_layout::IndexRow>>> {
3149        let mut rows = self.objects.lookup_batch(oids);
3150        if rows.iter().all(Option::is_some) {
3151            return Ok(rows);
3152        }
3153        // See `lookup_never_wait`: a miss followed by a clear bit is re-read,
3154        // because the drain may have landed the rows between the two.
3155        if self.unindexed_packs() == 0 {
3156            return Ok(self.objects.lookup_batch(oids));
3157        }
3158        let missing: Vec<&[u8]> = oids
3159            .iter()
3160            .zip(&rows)
3161            .filter(|(_, r)| r.is_none())
3162            .map(|(o, _)| *o)
3163            .collect();
3164        let (found, _) = self.scan_pending(&missing, None)?;
3165        let mut by_oid: HashMap<&[u8], crate::index_layout::IndexRow> = HashMap::with_capacity(found.len());
3166        for (o, r) in missing.iter().zip(found) {
3167            if let Some(r) = r {
3168                by_oid.insert(*o, r);
3169            }
3170        }
3171        for (o, slot) in oids.iter().zip(rows.iter_mut()) {
3172            if slot.is_none()
3173                && let Some(r) = by_oid.get(*o)
3174            {
3175                *slot = Some(r.clone());
3176            }
3177        }
3178        // Absorbed between the bit read and the scan: whatever is still
3179        // missing gets one more index read.
3180        if rows.iter().any(Option::is_none) {
3181            for (o, slot) in oids.iter().zip(rows.iter_mut()) {
3182                if slot.is_none() {
3183                    *slot = self.objects.lookup(o);
3184                }
3185            }
3186        }
3187        Ok(rows)
3188    }
3189
3190    /// The bit going clear: this pack is durable and its objects are not in.
3191    ///
3192    /// Skips a pack the drain already absorbed — the ack path queues *after* it
3193    /// has handed the extent to the channel, so the worker can legitimately be
3194    /// finished before this runs, and clearing a bit for a pack that is already
3195    /// indexed would leave it clear for ever.
3196    fn queue(&self, pack_id: u64, extent: Extent) -> Result<()> {
3197        let mut s = self
3198            .packs
3199            .lock()
3200            .map_err(|_| anyhow!("pack state poisoned"))?;
3201        s.note(pack_id, extent);
3202        Ok(())
3203    }
3204
3205    /// **The bit, derived from the two durable facts, on open.**
3206    ///
3207    /// `journal` is every row this archive's journal holds, in append order.
3208    /// [`acked_packs`](crate::archive_write::acked_packs) is what turns that into
3209    /// the packs: its index *is* the pack ordinal — dense by construction, and
3210    /// re-derived the same way by every later open. For each one, the question is
3211    /// only *does this pack have any rows in the index*, which
3212    /// [`ObjectReadStack::extents_with_rows`] answers in one early-terminating
3213    /// scan rather than per pack.
3214    ///
3215    /// Returns the packs whose bit came up **clear**: durable bytes, no rows. The
3216    /// caller re-queues them on the account's channel, which is what makes a
3217    /// pack that was mid-absorb when the process died get absorbed by the next
3218    /// one instead of being durable-but-invisible for ever.
3219    ///
3220    /// The verbatim extents are also handed to [`Derived::packs`] — *all* of
3221    /// them, not only the unabsorbed ones. That is what lets [`BaseSource`]
3222    /// re-derive a delta base's content from the truth after a restart; without
3223    /// it a reopened store knows the rows of a pack it can no longer find the
3224    /// bytes of.
3225    ///
3226    /// # The one case where "no rows" does not mean "never absorbed" — and the
3227    /// third durable fact that now tells them apart
3228    ///
3229    /// [`GitOps::gc`] is the only thing that removes rows. A pack **all** of
3230    /// whose objects it finds dead therefore ends with a journal extent and no
3231    /// rows, which is byte for byte the state a pack that was in flight when the
3232    /// machine died leaves behind. This diff cannot tell them apart, and it must
3233    /// not guess: re-queueing the first resurrects every dead object, and *not*
3234    /// re-queueing the second loses a pack whose bytes were acked.
3235    ///
3236    /// So `gc` says which it is, durably, before it drops a row:
3237    /// [`retire_packs`](crate::archive_write::retire_packs) appends a tombstone
3238    /// naming the retired pack's offset, and a pack named by one is never
3239    /// re-queued here. The bit is still derived from durable facts only — there
3240    /// are now three of them (the journal's pack rows, the journal's tombstones,
3241    /// the index's rows) and none of them is a cache of another.
3242    ///
3243    /// A **partly** dead pack is not tombstoned and still has rows, so it takes
3244    /// the `absorbed` branch exactly as it always did.
3245    ///
3246    /// A tombstoned pack that *does* still have rows is a `gc` that was killed
3247    /// between the tombstone and the drop. It is treated as absorbed, which is
3248    /// what its rows say: nothing is lost, and the next `gc` finishes the job.
3249    ///
3250    /// Its extent still goes to [`Derived::packs`] either way. The blob file is
3251    /// append-only and `gc` truncates nothing, so a retired pack's bytes are
3252    /// still exactly where the journal says they are — and a live object may
3253    /// legitimately be a delta against a base that is itself unreachable, so
3254    /// dropping the extent from that list would break the resolve rather than
3255    /// tidy it.
3256    ///
3257    /// # The second durable fact: §14's exploded table
3258    ///
3259    /// A pack counts as absorbed only if the exploded table is **whole**, which
3260    /// is `exploded.rows() >= objects.len()` — one row per object, because §14's
3261    /// table is eager and skips nothing. If it is short, the table has been
3262    /// dropped (or a process died between the two commits) and **no** pack counts
3263    /// as absorbed: every one is re-queued and re-exploded from the verbatim
3264    /// truth. That is the whole of "droppable": deleting the file is a supported
3265    /// operation whose only cost is one re-ingest, and §13.12's rule holds
3266    /// unchanged over it — absent means fall back, never means wrong.
3267    ///
3268    /// `>=` and not `==` on purpose. A resolve that failed part way leaves rows
3269    /// for the objects it got to, and a `gc` retires both tables together, so the
3270    /// table can legitimately hold *more* than the index names; only being
3271    /// **short** is evidence of a gap.
3272    fn adopt_journal(&self, journal: &[Extent]) -> Result<Vec<PendingPack>> {
3273        if journal.is_empty() {
3274            return Ok(Vec::new());
3275        }
3276        // The packs, in ordinal order, and the offsets a `gc` retired. A
3277        // tombstone is a row and not a pack, so it holds no ordinal.
3278        let packs = crate::archive_write::acked_packs(journal);
3279        let retired = crate::archive_write::retired_offsets(journal);
3280        if packs.is_empty() {
3281            return Ok(Vec::new());
3282        }
3283        // Under `Graph` the table is short **by design** — commits and trees
3284        // only — so the row-count comparison would call every store dropped and
3285        // re-queue every pack on every open. It is weakened to "not empty",
3286        // which still catches the case this rule exists for (the file deleted),
3287        // and is stated rather than hidden: making it exact needs a durable
3288        // commit-and-tree count, which the index does not publish today.
3289        // Only `Full` explodes one row per object, so only `Full` can compare
3290        // counts. Under `Commits` (the shipped default) and `Graph` the eager
3291        // pool is short **by design**, and the comparison would call every store
3292        // dropped and re-queue every pack on every open — a Linux-sized re-index
3293        // per restart. The weakened test still catches what the rule exists for
3294        // (the file deleted) and is stated rather than hidden: making it exact
3295        // needs a durable per-kind count, which the index does not publish.
3296        //
3297        // The LAZY pool is deliberately not consulted. Its size is a function of
3298        // what has been READ, so it says nothing about whether a pack was
3299        // absorbed, and an eviction must never look like a lost index.
3300        let exploded_is_whole = match self.exploded.policy() {
3301            crate::exploded_arrow::ExplodePolicy::Full => {
3302                self.exploded.eager_rows()? >= self.objects.len() as u64
3303            }
3304            crate::exploded_arrow::ExplodePolicy::Off => true,
3305            _ => self.objects.len() == 0 || self.exploded.eager_rows()? > 0,
3306        };
3307        let has_rows = if exploded_is_whole {
3308            self.objects.extents_with_rows(&packs)?
3309        } else {
3310            vec![false; packs.len()]
3311        };
3312        {
3313            let mut d = self
3314                .derived
3315                .write()
3316                .map_err(|_| anyhow!("derived poisoned"))?;
3317            d.packs = packs.clone();
3318        }
3319        let mut s = self
3320            .packs
3321            .lock()
3322            .map_err(|_| anyhow!("pack state poisoned"))?;
3323        let mut requeue = Vec::new();
3324        for (i, (&extent, &absorbed)) in packs.iter().zip(&has_rows).enumerate() {
3325            let pack_id = i as u64;
3326            if absorbed {
3327                s.mark_absorbed(pack_id, extent);
3328            } else if retired.contains(&extent.0) {
3329                // A `gc` dropped every row this pack had and said so durably
3330                // *before* dropping them. There is no index work owed for it,
3331                // ever: re-queueing it is precisely how its dead objects would
3332                // come back. The bit goes **up** rather than being left clear,
3333                // because a clear bit is what makes a read fall back to
3334                // absorbing — which is the same resurrection by another door.
3335                s.mark_absorbed(pack_id, extent);
3336            } else {
3337                s.note(pack_id, extent);
3338                requeue.push(PendingPack {
3339                    pack_id,
3340                    offset: extent.0,
3341                    len: extent.1,
3342                    resumed: true,
3343                });
3344            }
3345        }
3346        Ok(requeue)
3347    }
3348
3349    /// **The indexer's half of the split**, and not one of the twelve.
3350    ///
3351    /// Reads each pending pack's verbatim bytes back out of the archive,
3352    /// resolves them to oids ([`crate::resolve`]), appends the rows to the
3353    /// `objects` table and re-folds the derived tables. This is what §13.9-12
3354    /// puts behind the channel; it runs after the ack, never before one.
3355    ///
3356    /// Two callers, one function: the account indexer's worker (through
3357    /// [`ObjectAbsorb`]) and a read that arrived before that worker did. The
3358    /// second is §13.12's fallback in its literal form — *slower, never wrong*.
3359    fn absorb_pending(&self) -> Result<usize> {
3360        // Nothing to do, and — this is the point — **no gate taken**: the common
3361        // case is a store whose drain has kept up, and it must not queue behind
3362        // one that is running.
3363        if self.unindexed_packs() == 0 {
3364            return Ok(0);
3365        }
3366        let _gate = self
3367            .gate
3368            .lock()
3369            .map_err(|_| anyhow!("absorb gate poisoned"))?;
3370        let jobs: Vec<PendingPack> = {
3371            let s = self
3372                .packs
3373                .lock()
3374                .map_err(|_| anyhow!("pack state poisoned"))?;
3375            s.pending()
3376        };
3377        self.absorb_each(&jobs, None)
3378    }
3379
3380    /// **Every pack it can, skipping the ones it has already given up on.**
3381    ///
3382    /// The one loop all four absorb entry points run (LAW 5): they were four
3383    /// copies of `for job in &jobs { self.absorb_gated(job)?; }`, and that `?`
3384    /// is the bug this function exists to remove. Two rules, and both are
3385    /// consequences of the same sentence — *work that can never succeed must
3386    /// stop and say why*:
3387    ///
3388    /// 1. **A failure does not abandon the packs behind it.** The loop runs to
3389    ///    the end and reports the FIRST failure afterwards, so a pack that
3390    ///    cannot be indexed no longer stops the push that arrived after it from
3391    ///    being indexed and acked.
3392    /// 2. **A pack that has been given up on is not attempted again.** After
3393    ///    [`MAX_ABSORB_ATTEMPTS`] it is recorded in
3394    ///    [`Absorber::unindexable`] with the reason, logged once as
3395    ///    `index.unindexable`, and skipped from then on. Its objects are then
3396    ///    genuinely absent from the index, which every read path already knows
3397    ///    how to say — a push that needs one is refused by name at
3398    ///    `external_bases_exist`, and a clone that needs one fails the
3399    ///    connectivity check. What no longer happens is the store re-resolving
3400    ///    it on every request for the rest of the process's life.
3401    ///
3402    /// `skip` is the pack this thread is standing inside an absorb of
3403    /// ([`IN_ABSORB`]).
3404    fn absorb_each(&self, jobs: &[PendingPack], skip: Option<u64>) -> Result<usize> {
3405        let mut absorbed = 0usize;
3406        let mut first: Option<anyhow::Error> = None;
3407        for job in jobs {
3408            if skip == Some(job.pack_id) || self.given_up_on(job.pack_id) {
3409                continue;
3410            }
3411            match self.absorb_gated(job) {
3412                Ok(()) => absorbed += 1,
3413                Err(e) => {
3414                    // Cancellation is the repository being deleted: there is no
3415                    // queue left worth draining and nothing to record.
3416                    if self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3417                        return Err(e);
3418                    }
3419                    self.note_absorb_failure(job, &e);
3420                    if first.is_none() {
3421                        first = Some(e);
3422                    }
3423                }
3424            }
3425        }
3426        match first {
3427            None => Ok(absorbed),
3428            Some(e) => Err(e),
3429        }
3430    }
3431
3432    /// Every pack this store has given up on, ordinal order.
3433    fn unindexable_packs(&self) -> Vec<Unindexable> {
3434        let mut v: Vec<Unindexable> = self
3435            .unindexable
3436            .lock()
3437            .unwrap_or_else(|e| e.into_inner())
3438            .values()
3439            .cloned()
3440            .collect();
3441        v.sort_by_key(|u| u.pack_id);
3442        v
3443    }
3444
3445    /// Has this store stopped trying to index that pack?
3446    fn given_up_on(&self, pack_id: u64) -> bool {
3447        self.unindexable
3448            .lock()
3449            .unwrap_or_else(|e| e.into_inner())
3450            .contains_key(&pack_id)
3451    }
3452
3453    /// Count one failed absorb, and stop at [`MAX_ABSORB_ATTEMPTS`].
3454    ///
3455    /// The reason recorded is `root_cause`, not the outermost wrapper: that is
3456    /// the sentence the backend actually has to offer, and lane Z's rule is that
3457    /// the client gets it verbatim rather than a layer's paraphrase of it.
3458    fn note_absorb_failure(&self, job: &PendingPack, err: &anyhow::Error) {
3459        let giving_up;
3460        {
3461            let mut map = self.unindexable.lock().unwrap_or_else(|e| e.into_inner());
3462            // A pack already given up on is not attempted, so an entry here is
3463            // always a fresh count.
3464            let e = map.entry(job.pack_id).or_insert_with(|| Unindexable {
3465                pack_id: job.pack_id,
3466                extent: (job.offset, job.len),
3467                attempts: 0,
3468                reason: String::new(),
3469            });
3470            e.attempts += 1;
3471            e.reason = format!("{}", err.root_cause());
3472            if e.attempts < MAX_ABSORB_ATTEMPTS {
3473                let attempts = e.attempts;
3474                map.remove(&job.pack_id);
3475                tracing::warn!(
3476                    target: "znippy.git",
3477                    store = %self.label,
3478                    pack = job.pack_id,
3479                    attempts,
3480                    max = MAX_ABSORB_ATTEMPTS,
3481                    error = %err.root_cause(),
3482                    "a pack could not be indexed; it stays pending and will be tried again"
3483                );
3484                return;
3485            }
3486            giving_up = Some(e.clone());
3487        }
3488        if let Some(u) = giving_up {
3489            tracing::error!(
3490                target: "znippy.git",
3491                store = %self.label,
3492                event = "index.unindexable",
3493                pack = u.pack_id,
3494                offset = u.extent.0,
3495                len = u.extent.1,
3496                attempts = u.attempts,
3497                error = %u.reason,
3498                "this store has STOPPED trying to index this pack after {} attempts. Its bytes \
3499                 are durable and untouched, and every object it introduced is absent from the \
3500                 index — a push that deltas against one is refused by name and a clone that \
3501                 needs one fails its connectivity check, both immediately. Nothing will retry \
3502                 it: re-push the pack, or delete and re-create the repository.",
3503                u.attempts
3504            );
3505        }
3506    }
3507
3508    /// One pack, **with [`Absorber::gate`] already held**.
3509    ///
3510    /// The order at the end is the correctness argument: the rows are in the
3511    /// index *before* the bit is cleared. A read that finds the bit still clear
3512    /// blocks on the gate and then finds the rows; a read that finds it set finds
3513    /// the rows too. There is no interleaving in which a durable object answers
3514    /// "absent" — which during negotiation would make a client withhold objects
3515    /// and lose data.
3516    fn absorb_gated(&self, job: &PendingPack) -> Result<()> {
3517        if self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3518            // The repository is being deleted: no pack still queued is worth a
3519            // byte of work, and the drain must get through its queue fast so
3520            // the store can be dropped and the directory removed.
3521            return Err(crate::resolve::ResolveError::cancelled());
3522        }
3523        {
3524            let s = self
3525                .packs
3526                .lock()
3527                .map_err(|_| anyhow!("pack state poisoned"))?;
3528            if s.is_absorbed(job.pack_id) {
3529                // Already in, by whoever reached it first. Re-resolving is now
3530                // *correct* — the exploded table is keyed by oid and the graph is
3531                // folded from it rather than pushed at, so a second absorb of one
3532                // pack changes nothing (that idempotence is what makes dropping
3533                // the table and re-queueing every pack a safe operation). It is
3534                // still a whole pack re-read, re-inflated and re-hashed for
3535                // nothing, so it is skipped.
3536                return Ok(());
3537            }
3538        }
3539        // From here to the end of `absorb_one` this thread holds the gate, and
3540        // the resolver is about to ask this same store for delta bases. See
3541        // [`IN_ABSORB`]: the re-entry must not take the gate again and must not
3542        // try to absorb the pack it is standing in.
3543        let _in = InAbsorb::enter(job.pack_id);
3544        self.absorb_one(job).map_err(|e| {
3545            // The entry stays pending by construction — it was never removed —
3546            // so the bytes are durable, the pack is still un-indexed, and reads
3547            // keep falling back rather than answering absent.
3548            e.context(format!(
3549                "absorbing pack {} at ({}, {}) — its bytes are durable and it stays \
3550                 un-indexed; reads will keep falling back rather than answer absent",
3551                job.pack_id, job.offset, job.len
3552            ))
3553        })?;
3554        let mut s = self
3555            .packs
3556            .lock()
3557            .map_err(|_| anyhow!("pack state poisoned"))?;
3558        s.mark_absorbed(job.pack_id, (job.offset, job.len));
3559        drop(s);
3560        // The index answers for it now; the reader's scan is dead weight.
3561        self.scanned.lock().unwrap_or_else(|e| e.into_inner()).remove(&job.offset);
3562        Ok(())
3563    }
3564
3565    /// One pack: verbatim bytes in, `objects` rows **and** §14's exploded rows
3566    /// out, from **one** walk and **one** resolve.
3567    ///
3568    /// The exploded rows are not a second pass and not a second resolver (LAW 5):
3569    /// [`crate::resolve::resolve_walked`] hands every object it produces to the
3570    /// sink as it produces it, including the blob payloads it is about to drop,
3571    /// so the side table falls out of the pass that was already being made.
3572    ///
3573    /// Ordering is the correctness argument, and it is the same one the drain
3574    /// makes one level up: **the exploded rows are committed before the fold that
3575    /// reads them**, and the fold runs before this returns, so there is no
3576    /// interleaving in which the graph is folded over a table that does not yet
3577    /// hold this pack's commits.
3578    fn absorb_one(&self, job: &PendingPack) -> Result<()> {
3579        let t0 = std::time::Instant::now();
3580        self.progress.begin(job.pack_id, job.len, job.resumed);
3581        // **A live row every 10 s for as long as this pack takes**, carrying
3582        // `peak_rss` and the re-inflation count. Those two rode only on
3583        // `index.finished`, so lane P2's two rig boxes — which ran for 2h14m
3584        // and 2h12m and never finished a pack — produced neither, in exactly
3585        // the failure they were built for. The ticker is a thread and not a
3586        // hook inside the resolve loop on purpose: it prints while the resolver
3587        // is PAUSED too, which is when an operator most needs it.
3588        let _tick = crate::progress::ProgressTicker::start(self.progress.clone());
3589        let result = self.absorb_one_inner(job, t0);
3590        self.progress.sample_memory();
3591        self.progress.finish();
3592        result
3593    }
3594
3595    /// The body of [`absorb_one`](Self::absorb_one), so `finish` runs on every
3596    /// exit and the progress row never sticks at 99 %.
3597    fn absorb_one_inner(&self, job: &PendingPack, t0: std::time::Instant) -> Result<()> {
3598        use crate::progress::Phase;
3599        let progress = &self.progress;
3600        // **The pack is read where it already is.** `read_extent` was a
3601        // `pread` of the whole extent into a fresh `Vec` — 8.12 GiB of
3602        // anonymous heap for torvalds/linux, before one byte was inflated, on a
3603        // 16 GB box. The mapping costs nothing the kernel cannot reclaim
3604        // ([`crate::archive_map`]), and `MemAvailable` — what the watchdog
3605        // reads — knows that.
3606        let snap = self.map.snapshot()?;
3607        let bytes: std::borrow::Cow<'_, [u8]> = match snap.get(job.offset, job.len) {
3608            Some(b) => std::borrow::Cow::Borrowed(b),
3609            // Past the mapping: the file grew after the snapshot. Same bytes,
3610            // one `pread` — the exception, not the shape.
3611            None => std::borrow::Cow::Owned(self.read_extent(job.offset, job.len)?),
3612        };
3613        progress.set_phase(Phase::Walk);
3614        let walked = crate::pack_walk::walk(&bytes, self.hash.oid_len())?;
3615        let t_walk = t0.elapsed();
3616        let dog = self.watchdog();
3617        let spill = || self.spill();
3618        let limits = crate::resolve::ResolveLimits::for_indexer(
3619            &self.label,
3620            &self.cancel,
3621            progress,
3622            dog,
3623            &spill,
3624        );
3625        let mut batches = 0u64;
3626        let mut appended = 0u64;
3627        let mut entries: Vec<crate::index_layout::IndexEntry> = Vec::with_capacity(limits.row_batch);
3628        let report = crate::resolve::resolve_streaming(
3629            &bytes,
3630            &walked,
3631            self.hash,
3632            job.offset,
3633            self,
3634            &*self.exploded,
3635            limits,
3636            &mut |rows| {
3637                // **One batch of rows lands, then the next.** The exploded
3638                // tier already flushed itself every 64 MiB; the index rows
3639                // used to wait for the whole pack. Now the batch is the unit:
3640                // rows go to the `objects` table as they are produced, and the
3641                // pack's own "indexed" bit still goes up only after the LAST
3642                // batch — `mark_absorbed` is the caller's, after this returns.
3643                entries.clear();
3644                entries.extend(rows.iter().map(|r| r.index_entry()));
3645                rows.clear();
3646                self.objects.append(&entries)?;
3647                appended += entries.len() as u64;
3648                batches += 1;
3649                Ok(())
3650            },
3651        )?;
3652        drop(entries);
3653        let t_resolve = t0.elapsed() - t_walk;
3654        progress.set_phase(Phase::Flush);
3655        // Down before anything folds over it.
3656        self.exploded.flush()?;
3657        // A push is what fills the disk, so the push is what asks the cache to
3658        // give space back (the disk line, `exploded_cache::DISK_LINE_PERCENT`).
3659        // Until 2026-09-28 only a reader's write-back started the maintenance
3660        // thread, so a store nobody read never looked at the disk. `kick`
3661        // spawns and returns: the indexer does not wait for an unlink.
3662        self.exploded.kick();
3663
3664        {
3665            let mut d = self
3666                .derived
3667                .write()
3668                .map_err(|_| anyhow!("derived poisoned"))?;
3669            // A reopen already listed every acked extent (`adopt_journal`), so
3670            // this is the first absorb of a pack pushed in *this* process — or a
3671            // re-queued one that is already listed. `find` is what reads this
3672            // list, so a duplicate would not be wrong, only unbounded.
3673            if !d.packs.contains(&(job.offset, job.len)) {
3674                d.packs.push((job.offset, job.len));
3675            }
3676        }
3677        progress.set_phase(Phase::Fold);
3678        let t_fold0 = std::time::Instant::now();
3679        // The graph and the tree payloads are folded from the exploded table, not
3680        // pushed at from here. That is what makes a re-absorb idempotent — which
3681        // it has to be, because dropping the table re-queues every pack.
3682        self.refold()?;
3683        let _ = self.objects.maybe_rebuild()?;
3684        let t_fold = t_fold0.elapsed();
3685        // **The audit line**: every number is applied output of this absorb.
3686        // `peak_rss` is the process's VmHWM off /proc/self/status — the high
3687        // water mark of the whole server, which on the appliance is the box.
3688        tracing::info!(
3689            target: "gunnar::index",
3690            event = "index.finished",
3691            repo = %self.label,
3692            pack_id = job.pack_id,
3693            pack_bytes = job.len,
3694            peak_rss = crate::memguard::peak_rss_bytes(),
3695            mem_available = self.watchdog().available().unwrap_or(0),
3696            objects = report.rows,
3697            batches = batches,
3698            rows_appended = appended,
3699            reinflations = report.reinflations,
3700            evictions = report.evictions,
3701            watchdog_drops = report.watchdog_drops,
3702            peak_base_bytes = report.peak_base_bytes,
3703            pauses = report.pauses,
3704            paused_ms = report.paused_ms,
3705            walk_ms = t_walk.as_millis() as u64,
3706            resolve_ms = t_resolve.as_millis() as u64,
3707            fold_ms = t_fold.as_millis() as u64,
3708            total_ms = t0.elapsed().as_millis() as u64,
3709            resumed = job.resumed,
3710            "indexed {} objects in {} batches: walk {} ms, resolve {} ms, fold {} ms; peak bases {}, {} evictions, {} re-inflations, {} pauses ({} ms); peak rss {}",
3711            report.rows,
3712            batches,
3713            t_walk.as_millis(),
3714            t_resolve.as_millis(),
3715            t_fold.as_millis(),
3716            crate::memguard::fmt_bytes(report.peak_base_bytes),
3717            report.evictions,
3718            report.reinflations,
3719            report.pauses,
3720            report.paused_ms,
3721            crate::memguard::fmt_bytes(crate::memguard::peak_rss_bytes())
3722        );
3723        Ok(())
3724    }
3725
3726    /// Recompute everything derived from the object set: the commit graph, the
3727    /// tree payloads, the ordinal space, the generation numbers, and (lazily) the
3728    /// bitmaps.
3729    ///
3730    /// **The graph is folded from §14's exploded table, not accumulated.** That
3731    /// is the whole fix for the clean-reopen hole: commit and tree payloads are
3732    /// on disk, so this call produces the same graph after a restart as before
3733    /// one, and it is called at the end of [`GitStore::open_with_arms`] for
3734    /// exactly that reason. It also makes a re-absorb idempotent — the old
3735    /// accumulate-into-a-`Vec` shape doubled every commit if a pack was absorbed
3736    /// twice, which is why dropping the table and re-queueing everything was not
3737    /// a safe operation before this.
3738    ///
3739    /// **Generations are stored and must be recomputed in any fold** (§13,
3740    /// decided). A generation number is `1 + max(parents)`, so it is invalidated
3741    /// the moment history's shape changes — which is exactly what absorbing a
3742    /// pack does. [`assign_generations`] is the one writer of that number and it
3743    /// is called here, over the whole graph, rather than incrementally: an
3744    /// incremental update would have to know which descendants moved, and being
3745    /// wrong about that produces an ancestry cutoff that skips real ancestors.
3746    ///
3747    /// # Cost
3748    ///
3749    /// One range scan of the exploded table's commits and one of its trees, per
3750    /// absorbed pack. It is the same order as the `tail_oids` scan below, which
3751    /// this function already made on every absorb, and it reads **no blob
3752    /// payload** — that is what the kind index in
3753    /// [`crate::exploded`] is for.
3754    ///
3755    /// ## And that order is the whole repository, on every push
3756    ///
3757    /// Said plainly, because "the same order as the scan below" reads like a
3758    /// reassurance and is not one: **nothing here is proportional to the push.**
3759    /// Every call rebuilds every tree payload, every commit node, every
3760    /// generation number, a `HashMap<String, u32>` over every oid in the
3761    /// repository and a sorted `Vec<String>` of the same — for a push of one
3762    /// commit exactly as for a push of ten thousand.
3763    ///
3764    /// MEASURED on oden 2026-08-11, release, quiet box (`some avg10` 0.00). The
3765    /// **same** 100-commit increment, pushed onto the same history seeded to
3766    /// four different depths, server CPU for the push:
3767    ///
3768    /// | objects already in the repository | CPU |
3769    /// |---:|---:|
3770    /// | 9 614 | 0.12 s |
3771    /// | 35 350 | 0.23 s |
3772    /// | 103 988 | 0.45 s |
3773    /// | 206 925 | 0.77 s |
3774    ///
3775    /// That is a straight line: **≈88 ms fixed, plus ≈3.3 µs per object already
3776    /// stored**, and the fit predicts the four points to within 20 ms. It is
3777    /// also why a push is slower than the ack path suggests — §13.9 puts the
3778    /// index behind a channel, but `put_refs` checks every ref target with
3779    /// `has`, `has` calls `lookup_one`, and `lookup_one` absorbs pending packs
3780    /// first. The drain is off the *ack* and squarely on the *push*.
3781    ///
3782    /// This is not fixed here and the numbers are recorded so the next attempt
3783    /// starts from them rather than from a guess. The obvious lever — fold
3784    /// incrementally — is the one the paragraph above rules out for
3785    /// generations, so it needs its own argument and its own gate.
3786    fn refold(&self) -> Result<()> {
3787        // Everything derived is computed **before** the write lock, which is then
3788        // held only for the swap: the fold is the long part and nothing that
3789        // reads `derived` should queue behind it.
3790        //
3791        // Ordinals first — **read, not re-scanned, when the tail has not moved.**
3792        // S-082's fix, mirroring the commit-graph memo one paragraph down:
3793        // `objects.ordinal` beside the pack holds `oids_raw` (every oid in the
3794        // store, in ordinal = sorted-raw order), and a cold open reads its couple
3795        // of megabytes back sequentially in milliseconds instead of walking the
3796        // whole redb B-tree — REFOLD measured that scan at ~65% (0.45 s) of a
3797        // 136k-object reopen, and proved a flat buffer measured the same, so the
3798        // cost is the SCAN, not the allocation. It is a memo, not a second truth:
3799        // the redb tail stays authoritative and the file is discarded unless its
3800        // (epoch, count) both match the live tail, so a push, a gc or a torn
3801        // write all fall back to the scan and re-persist.
3802        //
3803        // The raw concatenation is BOTH the ordinal space and the validity token
3804        // for the commit graph below, so the graph is read whenever the object
3805        // set is unchanged; the hex form and the ordinal map fold out of the same
3806        // bytes, so ordinal `o` names the same object in every representation by
3807        // construction rather than by agreeing computations (LAW 5). The tail
3808        // hands oids in oid order, which is byte order over fixed-width oids and
3809        // therefore already the sorted concatenation the memo stores.
3810        //
3811        // **2026-09-04: the memo is gone, because the scan it spared is gone.**
3812        // The read stack persists its projection now (`objects.index`), and the
3813        // sorted oid list IS that index's oid column — one contiguous copy out
3814        // of a mapped buffer, plus a merge of the few rows redb still holds
3815        // since the last fold. No file of its own, no header, no epoch to
3816        // validate: the projection is current by construction. What is kept is
3817        // the provenance — how many oids came off the index and how many off
3818        // the tail — because that is the only thing that can prove the space
3819        // was not a scan (both paths yield identical bytes).
3820        let space = self.objects.oids_raw()?;
3821        self.ordinal_folded
3822            .store(space.folded_rows, std::sync::atomic::Ordering::Relaxed);
3823        self.ordinal_tail
3824            .store(space.tail_rows, std::sync::atomic::Ordering::Relaxed);
3825        let oids_raw = space.raw;
3826        let token = graph_token(&oids_raw);
3827
3828        // The commit graph — **read, not recomputed, when the object set has not
3829        // moved.** S-065's fix: `objects.graph` beside the pack holds the folded
3830        // graph with generations, written by this same call at derive time and
3831        // read here. It is not a second truth — it is a memoized projection
3832        // validated on every read against the authoritative object set (the
3833        // token is a blake3 over every oid in the store), and an absent file, a
3834        // stale token or a decode failure all fall back to the fold and
3835        // re-persist. Same shape as lane REFCAS persisting a tag's peeled target.
3836        //
3837        // On a fold, commit payloads are read out of §14's exploded table (no
3838        // tree payload — that was lane Q's 28.24 GB and most of the 50.9 GiB RSS
3839        // on `torvalds/linux`), parsed, and generation-numbered. That is the
3840        // whole of the O(commits) open cost this persistence removes.
3841        let cached = if self.graph_cache.load(std::sync::atomic::Ordering::Relaxed) {
3842            self.load_persisted_graph()?
3843        } else {
3844            None
3845        };
3846        let graph: Vec<CommitNode> = match cached {
3847            Some((tok, g)) if tok == token => {
3848                self.graph_read.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3849                g
3850            }
3851            _ => {
3852                self.graph_recomputed
3853                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3854                let commits = self.exploded.of_kind(GitObjectKind::Commit)?;
3855                let g: Vec<CommitNode> = commits
3856                    .iter()
3857                    .map(|(oid, payload)| {
3858                        let h = crate::object::parse_commit(payload);
3859                        CommitNode {
3860                            oid: hex::encode(oid),
3861                            parents: h.parents,
3862                            tree: h.tree,
3863                            committer_time: h.committer_time,
3864                            generation: 0,
3865                        }
3866                    })
3867                    .collect();
3868                let g = assign_generations(g);
3869                // Persist for the next open. A write failure is not fatal to the
3870                // fold — the graph is correct in memory and the next open simply
3871                // recomputes — so it is logged, not propagated.
3872                if !self.graph_cache.load(std::sync::atomic::Ordering::Relaxed) {
3873                    // Switched off: no memo on this disk.
3874                } else if let Err(e) = self.persist_graph(&token, &g) {
3875                    tracing::warn!(store = %self.label, "could not persist objects.graph: {e:#}");
3876                }
3877                g
3878            }
3879        };
3880
3881        // The same commit set `select` refuses a `want` against, in the raw form
3882        // it tests. Folded once here instead of once per request; see the field.
3883        // A row that does not decode makes the whole set `None`, which is the
3884        // decline `select` used to reach by returning early out of its own loop.
3885        let commit_raw = graph
3886            .iter()
3887            .map(|n| hex::decode(&n.oid).ok())
3888            .collect::<Option<std::collections::HashSet<Vec<u8>>>>()
3889            .map(Arc::new);
3890        // No hex `Vec<String>` and no `HashMap<String, u32>` are folded here any
3891        // more (S-082): `oids_raw` is the whole ordinal space, and oid ⇄ ordinal
3892        // is served off it by [`crate::reach::OrdinalSpace`] — a slice one way, a
3893        // binary search the other. That removed the ~0.13 s + ~0.08 s this fold
3894        // spent building those two per-oid `String` tables, and their resident
3895        // cost with them.
3896        // The graph-by-oid lookup, folded here so `reachable` borrows it instead
3897        // of rebuilding it per negotiation. Keyed by hex, the form the walk
3898        // keys by. Same lifecycle as `commit_raw`: rebuilt on every fold, so it
3899        // cannot outlive the graph it indexes.
3900        let graph_index: HashMap<String, u32> = graph
3901            .iter()
3902            .enumerate()
3903            .map(|(i, n)| (n.oid.clone(), i as u32))
3904            .collect();
3905
3906        let mut d = self
3907            .derived
3908            .write()
3909            .map_err(|_| anyhow!("derived poisoned"))?;
3910        // ── carry the reachability table across the fold ──────────────────
3911        //
3912        // The table describes immutable commits; the fold only renumbers the
3913        // ordinal space. Carry it (renumbered) while the graph has not outgrown
3914        // the last full build by more than the carry rule allows — else drop it
3915        // and let the next reader rebuild, which is what every fold used to do.
3916        // See `REACH_CARRY_MAX_FRACTION` for the measurement behind this.
3917        let carried: Option<Vec<ReachEntry>> = if d.reach.is_empty() {
3918            None
3919        } else {
3920            let grown = graph.len().saturating_sub(d.reach_full_len);
3921            let allowed = (d.reach_full_len / REACH_CARRY_MAX_FRACTION).max(REACH_CARRY_MIN_COMMITS);
3922            if grown > allowed {
3923                None
3924            } else {
3925                let oid_len = self.hash.oid_len();
3926                let old_space = crate::reach::OrdinalSpace::new(&d.oids_raw, oid_len);
3927                let new_space = crate::reach::OrdinalSpace::new(&oids_raw, oid_len);
3928                crate::reach::carry_reach(&d.reach, &old_space, &new_space, |c| graph_index.contains_key(c))
3929            }
3930        };
3931        tracing::info!(
3932            store = %self.label,
3933            reach_len = d.reach.len(),
3934            reach_full_len = d.reach_full_len,
3935            graph_len = graph.len(),
3936            carried = carried.as_ref().map(Vec::len),
3937            "fold: reachability table carry decision"
3938        );
3939        d.graph = graph;
3940        d.graph_index = graph_index;
3941        d.commit_raw = commit_raw;
3942        d.oids_raw = oids_raw;
3943        match carried {
3944            Some(entries) => {
3945                let index: HashMap<String, u32> = entries
3946                    .iter()
3947                    .enumerate()
3948                    .map(|(i, e)| (e.commit.clone(), i as u32))
3949                    .collect();
3950                d.reach = Arc::new(entries);
3951                d.reach_index = Arc::new(index);
3952                self.reach_carried
3953                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3954            }
3955            None => {
3956                // Over the ordinal space that just changed and not carriable;
3957                // both are rebuilt lazily by `reach_bitmaps_with`.
3958                d.reach = Arc::default();
3959                d.reach_index = Arc::default();
3960            }
3961        }
3962        Ok(())
3963    }
3964
3965    /// `(reach_carried, reach_rebuilt)` — how many folds since open carried the
3966    /// reachability table across (renumbered) vs how many reads built it from
3967    /// scratch. See [`tests::a_push_carries_the_reach_table_across_the_fold`].
3968    fn reach_source_stats(&self) -> (u64, u64) {
3969        (
3970            self.reach_carried.load(std::sync::atomic::Ordering::Relaxed),
3971            self.reach_rebuilt.load(std::sync::atomic::Ordering::Relaxed),
3972        )
3973    }
3974
3975    /// Where the folded commit graph is cached, beside the verbatim pack:
3976    /// `<root>/objects.graph`.
3977    fn graph_cache_path(&self) -> PathBuf {
3978        self.blobs.with_file_name(GRAPH_CACHE_FILE)
3979    }
3980
3981    /// The persisted graph and the token it was written under, or `None` when
3982    /// the cache is absent, truncated, an unknown version, or fails to decode —
3983    /// every one of which is a signal to recompute and re-persist rather than an
3984    /// error, because the cache is a memo and the exploded table is the truth.
3985    fn load_persisted_graph(&self) -> Result<Option<([u8; 32], Vec<CommitNode>)>> {
3986        let bytes = match std::fs::read(self.graph_cache_path()) {
3987            Ok(b) => b,
3988            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
3989            Err(e) => {
3990                return Err(e).with_context(|| {
3991                    format!("reading {}", self.graph_cache_path().display())
3992                });
3993            }
3994        };
3995        // Frame: b"GRPH" (4) + version (1) + token (32) + Arrow IPC graph.
3996        if bytes.len() < GRAPH_CACHE_HEADER || &bytes[..4] != GRAPH_CACHE_MAGIC || bytes[4] != 1 {
3997            return Ok(None);
3998        }
3999        let mut token = [0u8; 32];
4000        token.copy_from_slice(&bytes[5..GRAPH_CACHE_HEADER]);
4001        match crate::graph::decode_graph(&bytes[GRAPH_CACHE_HEADER..]) {
4002            Ok(g) => Ok(Some((token, g))),
4003            Err(_) => Ok(None),
4004        }
4005    }
4006
4007    /// Write the folded graph and its token durably: encode, write a temp file,
4008    /// fsync it, then rename over the cache — the rename is the atomic publish,
4009    /// so a reader never sees a half-written graph.
4010    fn persist_graph(&self, token: &[u8; 32], nodes: &[CommitNode]) -> Result<()> {
4011        use std::io::Write as _;
4012        let ipc = crate::graph::encode_graph(nodes)?;
4013        let mut out = Vec::with_capacity(GRAPH_CACHE_HEADER + ipc.len());
4014        out.extend_from_slice(GRAPH_CACHE_MAGIC);
4015        out.push(1);
4016        out.extend_from_slice(token);
4017        out.extend_from_slice(&ipc);
4018
4019        let path = self.graph_cache_path();
4020        let tmp = path.with_extension("graph.tmp");
4021        {
4022            let mut f = File::create(&tmp)
4023                .with_context(|| format!("creating {}", tmp.display()))?;
4024            f.write_all(&out)
4025                .with_context(|| format!("writing {}", tmp.display()))?;
4026            f.sync_all()
4027                .with_context(|| format!("fsync {}", tmp.display()))?;
4028        }
4029        std::fs::rename(&tmp, &path)
4030            .with_context(|| format!("publishing {}", path.display()))?;
4031        Ok(())
4032    }
4033
4034    /// `(graph_read_from_disk, graph_recomputed)` — how many folds since open
4035    /// read the persisted graph vs recomputed it. The only thing that can prove
4036    /// the persisted path ran; see
4037    /// [`tests::a_reopen_reads_the_persisted_graph_it_does_not_recompute`].
4038    fn graph_source_stats(&self) -> (u64, u64) {
4039        (
4040            self.graph_read.load(std::sync::atomic::Ordering::Relaxed),
4041            self.graph_recomputed
4042                .load(std::sync::atomic::Ordering::Relaxed),
4043        )
4044    }
4045
4046    /// `(folded_rows, tail_rows)` of the ordinal space the last fold built — how
4047    /// many oids came straight off the persisted index's oid column and how many
4048    /// off a scan of the redb tail. The only thing that can prove the space was
4049    /// not a scan (LAW 2), because both sources yield identical bytes; see
4050    /// [`tests::the_ordinal_space_comes_off_the_index_not_a_scan`].
4051    fn ordinal_source_stats(&self) -> (u64, u64) {
4052        (
4053            self.ordinal_folded.load(std::sync::atomic::Ordering::Relaxed),
4054            self.ordinal_tail.load(std::sync::atomic::Ordering::Relaxed),
4055        )
4056    }
4057
4058    /// The bitmaps, built on first use after a fold and cached until the next.
4059    fn reach_bitmaps(&self) -> Result<Arc<Vec<ReachEntry>>> {
4060        self.reach_bitmaps_with(live_reach_policy(), true)
4061    }
4062
4063    /// [`Absorber::reach_bitmaps`] with the policy **named** and the cache
4064    /// optional. The bitmaps alone; [`Absorber::reach_tables_with`] is the one
4065    /// builder and this drops the index it also returns, for callers that seal
4066    /// the table or compare two of them and never key into one by oid.
4067    fn reach_bitmaps_with(
4068        &self,
4069        policy: crate::reach::ReachPolicy,
4070        cache: bool,
4071    ) -> Result<Arc<Vec<ReachEntry>>> {
4072        Ok(self.reach_tables_with(policy, cache)?.0)
4073    }
4074
4075    /// **The reach table and its oid index, as one pair that is always in
4076    /// step.** [`crate::reach::accumulate`] keys the table by commit oid, and
4077    /// building that index per negotiation is exactly the O(commits) waste this
4078    /// lane removes — so it is built once, here, beside the table, and handed
4079    /// back with it. The two travel together (both cached, both returned) so a
4080    /// caller can never pair a fresh index against a stale table or the reverse.
4081    ///
4082    /// `cache` is false for the differential guard, which builds the table at two
4083    /// very different caps over one store and requires the two to answer
4084    /// identically; letting either poison `d.reach` would make the second arm
4085    /// read the first one's table and the comparison would be of a thing against
4086    /// itself.
4087    fn reach_tables_with(
4088        &self,
4089        policy: crate::reach::ReachPolicy,
4090        cache: bool,
4091    ) -> Result<(Arc<Vec<ReachEntry>>, Arc<HashMap<String, u32>>)> {
4092        {
4093            let d = self
4094                .derived
4095                .read()
4096                .map_err(|_| anyhow!("derived poisoned"))?;
4097            if cache && !d.reach.is_empty() {
4098                // Two refcount bumps. This was `d.reach.clone()` — one deep copy
4099                // of every commit's roaring bitmap, per call, twice per request
4100                // — plus, before this lane, a whole `HashMap<&str,_>` rebuilt
4101                // from the table on every call at the negotiation site.
4102                return Ok((Arc::clone(&d.reach), Arc::clone(&d.reach_index)));
4103            }
4104            if d.graph.is_empty() {
4105                return Ok((Arc::default(), Arc::default()));
4106            }
4107        }
4108        let mut d = self
4109            .derived
4110            .write()
4111            .map_err(|_| anyhow!("derived poisoned"))?;
4112        // Built before the facts so it outlives them, and deliberately NOT
4113        // holding the `derived` guard: `Absorber::resolved` does not take that
4114        // lock, which is what makes a live tree lookup legal from inside this
4115        // write section at all.
4116        let trees = self.tree_source();
4117        let oid_len = self.hash.oid_len();
4118        let facts = crate::reach::ObjectFacts {
4119            ordinal: crate::reach::OrdinalSpace::new(&d.oids_raw, oid_len),
4120            trees: &trees,
4121            oid_len,
4122        };
4123        let started = std::time::Instant::now();
4124        let built = Arc::new(crate::reach::build_reach(&d.graph, &facts, policy));
4125        self.reach_rebuilt
4126            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4127        tracing::info!(
4128            store = %self.label,
4129            entries = built.len(),
4130            commits = d.graph.len(),
4131            elapsed_ms = started.elapsed().as_millis() as u64,
4132            cache,
4133            "built the reachability table from scratch"
4134        );
4135        // The commit count this build covered — the denominator the next fold's
4136        // carry rule divides by (`REACH_CARRY_MAX_FRACTION`).
4137        d.reach_full_len = d.graph.len();
4138        // The index over the table just built — commit oid → row — so the
4139        // negotiation looks a bitmap up by a hash rather than by rebuilding this
4140        // map first.
4141        let index: Arc<HashMap<String, u32>> = Arc::new(
4142            built
4143                .iter()
4144                .enumerate()
4145                .map(|(i, e)| (e.commit.clone(), i as u32))
4146                .collect(),
4147        );
4148        if cache {
4149            // `Arc::new` once each, then a refcount bump into the cache — the
4150            // second full copy the table line used to make
4151            // (`d.reach = built.clone()`) is gone, and the index is cached in
4152            // lockstep so the read arm above can hand out both.
4153            d.reach = Arc::clone(&built);
4154            d.reach_index = Arc::clone(&index);
4155        }
4156        Ok((built, index))
4157    }
4158
4159    /// The commit oid set, raw, as folded. See [`Derived::commit_raw`].
4160    fn commit_oids_raw(&self) -> Result<Option<Arc<std::collections::HashSet<Vec<u8>>>>> {
4161        Ok(self
4162            .derived
4163            .read()
4164            .map_err(|_| anyhow!("derived poisoned"))?
4165            .commit_raw
4166            .clone())
4167    }
4168
4169    // `ordinal_space()` lived here: `d.oids.clone()`, a fresh `Vec<String>` of
4170    // every oid in the store, handed to `reachable_oids` twice per request. It
4171    // is gone, and so is `d.oids` itself (S-082): the serve path borrows
4172    // `d.oids_raw` as an `OrdinalSpace` under the read guard it was already
4173    // going to take, and reads oid ⇄ ordinal straight off it.
4174
4175    /// `pread` an extent out of the verbatim archive, **into uninitialised
4176    /// capacity** — no zero-fill before the read.
4177    ///
4178    /// This used to be `vec![0u8; len]` + `read_exact_at`, and under emit
4179    /// concurrency the zeroing was not a detail: profiled on oden 2026-08-12
4180    /// (gunnar serving 32 concurrent clones, perf on the server pid), ~44 % of
4181    /// serve CPU was memory zeroing/copying — `memset` 18.1 % plus kernel
4182    /// page-zeroing 7.8 % — against ~12 % for one clone, because 32 threads
4183    /// churning pack-scale buffers recycle mimalloc freelist blocks that must
4184    /// each be memset before `pread` immediately overwrites every byte.
4185    ///
4186    /// Soundness: no reference to uninitialised memory is ever formed. The
4187    /// bytes are written through a raw pointer into the `Vec`'s spare
4188    /// capacity, `EINTR` retries, a short read refuses, and `set_len` runs
4189    /// only after every one of `len` bytes has been written.
4190    fn read_extent(&self, offset: u64, len: u64) -> Result<Vec<u8>> {
4191        use std::os::fd::AsRawFd;
4192        let want = usize::try_from(len).context("extent length overflows usize")?;
4193        let mut buf: Vec<u8> = Vec::with_capacity(want);
4194        let fd = self.reader.as_raw_fd();
4195        let mut filled = 0usize;
4196        while filled < want {
4197            let n = unsafe {
4198                libc::pread(
4199                    fd,
4200                    buf.as_mut_ptr().add(filled).cast(),
4201                    want - filled,
4202                    i64::try_from(offset + filled as u64).context("extent offset overflows off_t")?,
4203                )
4204            };
4205            if n < 0 {
4206                let err = std::io::Error::last_os_error();
4207                if err.kind() == std::io::ErrorKind::Interrupted {
4208                    continue;
4209                }
4210                return Err(err).with_context(|| {
4211                    format!("reading ({offset}, {len}) out of {}", self.blobs.display())
4212                });
4213            }
4214            if n == 0 {
4215                bail!(
4216                    "short read at ({offset}, {len}) out of {}: {filled} of {want} bytes before \
4217                     EOF — the archive is truncated relative to its index",
4218                    self.blobs.display()
4219                );
4220            }
4221            filled += n as usize;
4222        }
4223        // Every byte of `want` is now initialised.
4224        unsafe { buf.set_len(want) };
4225        Ok(buf)
4226    }
4227}
4228
4229/// **The drain's object-level ingress**, and the reason a push ends in object
4230/// rows without anybody reading first.
4231///
4232/// The account indexer holds this as an `Arc<dyn ObjectAbsorb>` and calls it once
4233/// per drained job, on its own thread, after the ack. It is the *same* absorb a
4234/// falling-back read takes, gate and all (LAW 5): there is no background copy of
4235/// this logic to drift.
4236impl<S: ObjectIndex> ObjectAbsorb for Absorber<S> {
4237    fn absorb(&self, job: IndexJob) -> Result<()> {
4238        // **The memory watchdog, BEFORE the pack and BEFORE the gate**
4239        // ([`crate::memguard`]). Inside the pack the resolver checks again
4240        // every few thousand objects; this is the check that keeps a burst of
4241        // packs from starting one more absorb on a box that is already short.
4242        // Taken before the gate on purpose: a paused worker holds nothing —
4243        // not the gate, not a row, not a base — so a reader that wants to
4244        // absorb for itself can, and every other reader scans the pack.
4245        if self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4246            return Err(crate::resolve::ResolveError::cancelled());
4247        }
4248        let dog = self.watchdog();
4249        if dog.breached().is_some() {
4250            self.progress.begin(job.pack_id, job.len, false);
4251            self.progress.set_phase(crate::progress::Phase::Paused);
4252            self.progress.add_pause();
4253            let r = dog.pause_until_ok(&self.label, Some(&self.cancel), &mut || Ok(self.spill()));
4254            self.progress.finish();
4255            let paused = r?;
4256            // The pause before the pack has the same rule as the one inside it:
4257            // a give-up is not a resume. Starting an 8 GiB pack on a box that
4258            // just told us it has nothing left is how the parked hours began.
4259            if paused.end.gave_up() {
4260                return Err(crate::resolve::ResolveError::out_of_memory(
4261                    paused.end.as_str(),
4262                    paused.available,
4263                    paused.budget,
4264                    0,
4265                    0,
4266                ));
4267            }
4268        }
4269        let _gate = self
4270            .gate
4271            .lock()
4272            .map_err(|_| anyhow!("absorb gate poisoned"))?;
4273        self.absorb_gated(&PendingPack::from_job(job))
4274    }
4275}
4276
4277impl<S: ObjectIndex + 'static> GitStore<S> {
4278    /// The push path, for the two `put` halves.
4279    pub(crate) fn push_path(&self) -> &PushPath {
4280        &self.push
4281    }
4282
4283    pub(crate) fn account(&self) -> &str {
4284        &self.account
4285    }
4286
4287    pub(crate) fn ref_log(&self) -> &RefLog {
4288        &self.refs
4289    }
4290
4291    pub(crate) fn ref_gate(&self) -> &Mutex<()> {
4292        &self.ref_gate
4293    }
4294
4295    /// The closure check's index half: a `REF_DELTA`'s base has to exist —
4296    /// **in the store, or in this very pack**.
4297    ///
4298    /// This is the one part of receive-pack that reads `objects.oid` (§13's
4299    /// table), and on the path a real push takes it reads nothing else.
4300    ///
4301    /// # The second half of that sentence used to be missing, and it refused
4302    /// packs git itself writes
4303    ///
4304    /// [`crate::pack_walk::PackWalk::closure`] cannot do better on its own:
4305    /// a walk reports what the bytes say, and the bytes of a `REF_DELTA` name a
4306    /// base by **oid**, which is a fact about resolved content that no walk can
4307    /// know. So every ref base came back on `external_refs` and every one of
4308    /// them was required to be in the store already.
4309    ///
4310    /// A `REF_DELTA` naming a base *inside the same pack* is ordinary and git
4311    /// writes it constantly: `git index-pack --fix-thin` completes a pushed thin
4312    /// pack by **appending the base object to the pack** and leaving the delta
4313    /// naming it by oid. Every such pack was refused with *"this pack deltas
4314    /// against X, which this repository does not have"*, on a store that was
4315    /// being handed a perfectly ordinary, self-contained packfile — the base was
4316    /// entry 1 of 25 in the pack's own `.idx` (`gunnar.multi_pack_serve`,
4317    /// 2026-08-14). The same missing check is `S-003` / gitoxide#2882 in
4318    /// `LookupRefDeltaObjectsIter`; it is the same defect wearing the receiving
4319    /// hat instead of the sending one.
4320    ///
4321    /// # What it costs, and where
4322    ///
4323    /// **Nothing on any push that was going to succeed before.** Every base
4324    /// found in the store short-circuits exactly as it did, and a pack with no
4325    /// ref-delta at all never gets past the first loop.
4326    ///
4327    /// The pack is resolved only when the alternative is *refusing it*, and then
4328    /// the resolve is the cheapest honest answer available: an entry's oid is
4329    /// knowable only by applying its delta chain, which is precisely what
4330    /// [`crate::resolve::resolve_walked`] does — the same resolver the absorb is
4331    /// about to run over the same pack anyway, reused rather than twinned
4332    /// (LAW 5). A push that is genuinely thin against a base nobody has still
4333    /// fails, one resolve later, and still names the oid.
4334    pub(crate) fn external_bases_exist(&self, bytes: &[u8], w: &PackWalk) -> Result<()> {
4335        let c = w.closure();
4336        if !c.broken_offsets.is_empty() {
4337            bail!(
4338                "this pack is corrupt: {} delta base offset(s) do not land on an entry — first {}",
4339                c.broken_offsets.len(),
4340                c.broken_offsets[0]
4341            );
4342        }
4343        // The fast path, unchanged: a base the store already holds is settled
4344        // without reading one byte of the pack.
4345        let mut unheld: Vec<&[u8]> = Vec::new();
4346        for oid in &c.external_refs {
4347            if self.lookup_one(oid)?.is_none() {
4348                unheld.push(oid.as_slice());
4349            }
4350        }
4351        if unheld.is_empty() {
4352            return Ok(());
4353        }
4354
4355        // Only now, and only because the alternative is refusing the push: does
4356        // the pack supply these itself? `resolve_walked` answers with the
4357        // store behind it, so a base that really is external is still found the
4358        // cheap way and only a genuinely absent one fails.
4359        let resolved = crate::resolve::resolve_walked(
4360            bytes,
4361            w,
4362            self.hash_kind(),
4363            0,
4364            &*self.absorber,
4365            &crate::exploded::NoSink,
4366        )
4367        .with_context(|| {
4368            format!(
4369                "this pack deltas against {} object(s) this repository does not have, so it was \
4370                 resolved to find out whether the pack carries them itself — first {}",
4371                unheld.len(),
4372                hex::encode(unheld[0])
4373            )
4374        })?;
4375        let in_pack: std::collections::HashSet<&[u8]> =
4376            resolved.iter().map(|r| r.oid.as_slice()).collect();
4377        for oid in unheld {
4378            if !in_pack.contains(oid) {
4379                bail!(
4380                    "this pack deltas against {}, which this repository does not have and which \
4381                     the pack does not carry either — the push is refused rather than stored with \
4382                     a dangling base",
4383                    hex::encode(oid)
4384                );
4385            }
4386        }
4387        Ok(())
4388    }
4389
4390    /// One oid, on the **serial** path. See [`lookup_path`].
4391    pub(crate) fn lookup_one(&self, oid: Oid<'_>) -> Result<Option<crate::index_layout::IndexRow>> {
4392        // **Never waits on the indexer.** The rows the worker has landed so far
4393        // are in the index; an oid it has not reached is resolved from the
4394        // pending pack on this thread (`Absorber::scan_pending`).
4395        self.absorber.lookup_never_wait(oid)
4396    }
4397
4398    /// Many oids, on the **batch** path, never waiting on the indexer.
4399    pub(crate) fn lookup_batch_never_wait(
4400        &self,
4401        oids: &[Oid<'_>],
4402    ) -> Result<Vec<Option<crate::index_layout::IndexRow>>> {
4403        self.absorber.lookup_batch_never_wait(oids)
4404    }
4405
4406    /// Absorb the pending packs on this thread **if the gate is free**;
4407    /// `None` = the worker holds it and this caller did not wait. See
4408    /// `Absorber::try_absorb_pending`.
4409    pub fn try_absorb_pending(&self) -> Result<Option<usize>> {
4410        self.absorber.try_absorb_pending()
4411    }
4412
4413    /// The named refusal for a request that needs the whole derived graph
4414    /// while a pack is still being indexed: `reachable` cannot be answered
4415    /// from a graph that does not hold the pack's commits yet, and waiting for
4416    /// the worker is forbidden. The message carries the progress row's
4417    /// numbers so the client knows when to come back.
4418    fn index_in_progress(&self) -> anyhow::Error {
4419        let p = self.index_progress();
4420        anyhow!(
4421            "index in progress for this repository: pack {} {}% ({}/{} objects, {}) — reachability \
4422             needs the whole graph; retry when the index row reaches 100 %",
4423            p.pack_id,
4424            p.percent(),
4425            p.objects_done,
4426            p.objects_total,
4427            p.phase.as_str()
4428        )
4429    }
4430
4431    /// The current ref namespace, as the log folds to it. The whole-snapshot
4432    /// accessor; [`ref_get`](Self::ref_get) is the keyed one.
4433    pub(crate) fn ref_state(&self) -> Result<BTreeMap<String, crate::refs::RefState>> {
4434        self.refs.current()
4435    }
4436
4437    /// One ref by name, O(log r), without cloning the namespace — for `head`
4438    /// and a single-ref CAS. See [`crate::refs::RefLog::get`].
4439    pub(crate) fn ref_get(&self, name: &str) -> Result<Option<crate::refs::RefState>> {
4440        self.refs.get(name)
4441    }
4442
4443    /// Reachability over the graph, as roaring bitmaps, in **this store's**
4444    /// ordinal space.
4445    ///
4446    /// Returns `(want ∪ closure) − (have ∪ closure)` as oid hex. Every object —
4447    /// commit, tree and blob — is in the bitmaps, because
4448    /// [`crate::reach::build_reach`] walks the trees; that is why this can answer
4449    /// with objects rather than only with commits.
4450    ///
4451    /// **Hex is for the maintenance path, not the serving one.** The answer is
4452    /// computed in raw bytes by [`GitStore::reachable_raw_with`] and encoded
4453    /// here, at one `String` per object; every caller that serves a request asks
4454    /// for the raw form instead. See [`Derived::oids_raw`] for what that cost.
4455    pub(crate) fn reachable_oids(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> Result<Vec<String>> {
4456        Ok(self
4457            .reachable_raw_with(want, have, live_reach_policy(), true)?
4458            .iter()
4459            .map(hex::encode)
4460            .collect())
4461    }
4462
4463    /// [`GitStore::reachable_oids`] with the bitmap policy **named** and the
4464    /// cache optional.
4465    ///
4466    /// One body, not two (LAW 5): both are [`GitStore::reachable_raw_with`], so
4467    /// the guarded path and the shipped path are the same code.
4468    #[cfg(test)]
4469    pub(crate) fn reachable_oids_with(
4470        &self,
4471        want: &[Oid<'_>],
4472        have: &[Oid<'_>],
4473        policy: crate::reach::ReachPolicy,
4474        cache: bool,
4475    ) -> Result<Vec<String>> {
4476        Ok(self
4477            .reachable_raw_with(want, have, policy, cache)?
4478            .iter()
4479            .map(hex::encode)
4480            .collect())
4481    }
4482
4483    /// `(want ∪ closure) − (have ∪ closure)`, in **raw oid bytes**, in one
4484    /// allocation.
4485    ///
4486    /// The answer every serving caller wants and the one the bitmaps naturally
4487    /// produce: an ordinal indexes straight into [`Derived::oids_raw`], so
4488    /// nothing is encoded, decoded or allocated per object on the way out.
4489    pub(crate) fn reachable_raw(
4490        &self,
4491        want: &[Oid<'_>],
4492        have: &[Oid<'_>],
4493    ) -> Result<git_storage_trait::OidList> {
4494        self.reachable_raw_with(want, have, live_reach_policy(), true)
4495    }
4496
4497    /// [`GitStore::reachable_raw`] with the bitmap policy **named** and the
4498    /// cache optional.
4499    ///
4500    /// It exists because the answer is defined to be **independent of the
4501    /// policy** — [`crate::reach::accumulate`] covers whatever the table does
4502    /// not — and a property like that can only be tested by driving the policy.
4503    /// A test that could not reach it would be asserting that one code path
4504    /// equals itself. `cache: false` keeps one arm's table from being read by
4505    /// the next, which is the same trap in a different hat.
4506    pub(crate) fn reachable_raw_with(
4507        &self,
4508        want: &[Oid<'_>],
4509        have: &[Oid<'_>],
4510        policy: crate::reach::ReachPolicy,
4511        cache: bool,
4512    ) -> Result<git_storage_trait::OidList> {
4513        // The graph is folded by the absorb, so a pending pack has to be
4514        // absorbed before reachability can be right. If the gate is free this
4515        // thread does it; if the worker holds it — hours into a kernel pack,
4516        // or paused by the watchdog — this is refused by name, not waited on.
4517        if self.try_absorb_pending()?.is_none() {
4518            return Err(self.index_in_progress());
4519        }
4520        // `reach_bitmaps` may take the derived WRITE lock to build, so it runs
4521        // to completion before the read guard below is taken. Getting that order
4522        // wrong is a self-deadlock, not a slow path.
4523        // The reach table AND its oid index, one pair from the same fold. Both
4524        // used to be rebuilt at this site on every call: the bitmap map
4525        // (`e.commit → &bitmap`) here, and the graph map below — O(commits)
4526        // apiece, per request, for tables that cannot change between folds. They
4527        // are folded once now and borrowed through a [`crate::reach::ReachView`].
4528        let (bitmaps, reach_index) = self.reach_tables_with(policy, cache)?;
4529        // **One read guard, held across the whole answer, and nothing cloned out
4530        // of it.** This used to be `ordinal_space()?` (a `Vec<String>` of every
4531        // oid in the store) plus `.ordinal.clone()` (a `HashMap<String, u32>` of
4532        // the same, again) — ~2 × 12 455 `String` allocations per call and two
4533        // calls per request, for two tables that cannot change between folds.
4534        // Borrowing them is not a lock held longer than the work: the guard is a
4535        // reader, folds take the writer, and a fold that lands mid-answer would
4536        // invalidate the ordinals this loop is translating.
4537        let d = self
4538            .absorber
4539            .derived
4540            .read()
4541            .map_err(|_| anyhow!("derived poisoned"))?;
4542        let space = &d.oids_raw;
4543        let oid_len = self.hash.oid_len();
4544        // oid ⇄ ordinal off the flat buffer — a binary search, no map to clone
4545        // out of the guard. `want`/`have` arrive as raw bytes, so those lookups
4546        // take the raw form directly (`ordinal.ordinal_of_raw`) and never encode.
4547        let ordinal = crate::reach::OrdinalSpace::new(space, oid_len);
4548
4549        // The graph-by-oid lookup for [`crate::reach::accumulate`]'s commit walk
4550        // is `d.graph_index`, folded beside `d.graph` and borrowed here — not
4551        // rebuilt per call. The reach table's index (`reach_index`) is paired
4552        // with `bitmaps` above by the same fold, so this view never mixes a
4553        // table with an index from a different one.
4554        let view = crate::reach::ReachView {
4555            reach: &bitmaps,
4556            reach_index: &reach_index,
4557            graph: &d.graph,
4558            graph_index: &d.graph_index,
4559        };
4560        let trees = self.absorber.tree_source();
4561        let facts = crate::reach::ObjectFacts {
4562            ordinal,
4563            trees: &trees,
4564            oid_len,
4565        };
4566
4567        let mut union = roaring::RoaringBitmap::new();
4568        for oid in want {
4569            let hex = hex::encode(oid);
4570            match view.bitmap(hex.as_str()) {
4571                // A commit with a bitmap: its whole closure, in one OR.
4572                Some(bm) => union |= bm,
4573                // **No bitmap. This is the case that used to be a wrong answer.**
4574                //
4575                // It contributed the object itself and nothing else — see
4576                // `crate::reach::accumulate`'s header for what that cost. Now a
4577                // commit the graph holds is WALKED, bounded by the first
4578                // bitmapped commit behind it, and only a `want` the store does
4579                // not hold at all is still refused.
4580                None => {
4581                    if view.is_commit(hex.as_str()) {
4582                        crate::reach::accumulate(hex.as_str(), &view, &facts, &mut union);
4583                    } else if let Some(o) = ordinal.ordinal_of_raw(oid) {
4584                        // Not a commit at all — a TAG or a blob asked for
4585                        // directly.
4586                        union.insert(o);
4587                        // **A tag contributes what it points AT, and until
4588                        // 2026-08-31 it did not.**
4589                        //
4590                        // The bitmaps are built over the commit graph and no
4591                        // tree points at a tag, so a tag is in nobody's closure
4592                        // and this arm was the only place its target could
4593                        // enter. It did not, so an annotated tag shipped as one
4594                        // object with its commit missing, and the far end said
4595                        // `missing necessary objects … (peeling a tag)`. That
4596                        // is exactly what gunnar's replication hit on six
4597                        // repositories: 25 tags on one commit no branch carried.
4598                        //
4599                        // gunnar's own serve paths never had the bug because
4600                        // they peel BEFORE they ask a store
4601                        // (`gunnar-wire`'s `select_objects`, step 1). A store
4602                        // that answers `reachable` has to peel here, because
4603                        // its caller cannot know which oids are tags.
4604                        //
4605                        // A tag object is only ever reachable as a REF TIP —
4606                        // nothing inside a commit or a tree points at one — so
4607                        // peeling in the `want` loop is complete, not partial.
4608                        peel_tag_into(
4609                            self.absorber.as_ref(),
4610                            &hex,
4611                            &view,
4612                            &facts,
4613                            &mut union,
4614                        );
4615                    } else {
4616                        bail!(
4617                            "want {hex} is not in this repository — a negotiation must not be \
4618                             answered from a partial set"
4619                        );
4620                    }
4621                }
4622            }
4623        }
4624        let mut had = roaring::RoaringBitmap::new();
4625        for oid in have {
4626            let hex = hex::encode(oid);
4627            if let Some(bm) = view.bitmap(hex.as_str()) {
4628                had |= bm;
4629            } else if view.is_commit(hex.as_str()) {
4630                // The same walk on the exclude side, and it must be the same
4631                // walk: an under-counted `have` over-sends (harmless, wasteful)
4632                // but an under-counted `want` under-sends, and answering the two
4633                // sides by different rules is how a `want − have` stops being a
4634                // subtraction of like for like.
4635                crate::reach::accumulate(hex.as_str(), &view, &facts, &mut had);
4636            } else if let Some(o) = ordinal.ordinal_of_raw(oid) {
4637                had.insert(o);
4638            }
4639            // An unknown `have` is normal: the client may have objects we do not.
4640            // It contributes nothing, which is the safe direction — we send more,
4641            // never less.
4642        }
4643
4644        let delta = union - had;
4645        // One allocation for the whole answer, sized before the loop. The
4646        // ordinal space is a flat buffer, so each object is a `copy_from_slice`
4647        // of `oid_len` bytes into it and nothing else.
4648        let mut out = git_storage_trait::OidList::with_capacity(delta.len() as usize, oid_len);
4649        for o in delta {
4650            let at = o as usize * oid_len;
4651            let raw = space
4652                .get(at..at + oid_len)
4653                .ok_or_else(|| anyhow!("ordinal {o} is outside this store's ordinal space"))?;
4654            out.push(raw)?;
4655        }
4656        Ok(out)
4657    }
4658
4659    /// Every object reachable from every ref: what a GC must keep.
4660    pub(crate) fn live_set(&self) -> Result<std::collections::HashSet<String>> {
4661        let refs = self.ref_state()?;
4662        let tips: Vec<Vec<u8>> = refs
4663            .values()
4664            .filter_map(|s| s.target.as_deref())
4665            .filter_map(|t| hex::decode(t).ok())
4666            .collect();
4667        if tips.is_empty() {
4668            bail!(
4669                "this repository has no ref pointing at an object, so every object in it would be \
4670                 dead. A GC that would delete everything is refused: name a ref first"
4671            );
4672        }
4673        let borrowed: Vec<Oid<'_>> = tips.iter().map(|t| t.as_slice()).collect();
4674        let mut live: std::collections::HashSet<String> =
4675            self.reachable_oids(&borrowed, &[])?.into_iter().collect();
4676        // The peeled targets of annotated tags are reachable too, and a tag
4677        // object is not a commit so it has no bitmap of its own.
4678        for s in refs.values() {
4679            if let Some(p) = &s.peeled {
4680                live.insert(p.clone());
4681            }
4682            if let Some(t) = &s.target {
4683                live.insert(t.clone());
4684            }
4685        }
4686        Ok(live)
4687    }
4688
4689    /// Drop every row the live set does not name. The index is append-only for
4690    /// everything except this.
4691    ///
4692    /// **Both tables, in one call.** §14's exploded table is retired with the
4693    /// `objects` table because its row count against that one's is what says
4694    /// whether it is whole ([`Absorber::adopt_journal`]): dropping from one and
4695    /// not the other would leave every later open re-exploding the entire
4696    /// repository. The count returned is still the index's — that is what a
4697    /// [`GcReport`] means by a dropped row.
4698    /// Drop exactly the rows in `dead` — a set the GC computed as "seen at its
4699    /// start and not reachable", never "not in `live`". The distinction is a
4700    /// row that arrived after the GC took its snapshot: it is in neither set,
4701    /// and the predicate below keeps it. See [`dead_oids`].
4702    pub(crate) fn drop_dead_rows(&self, dead: &std::collections::HashSet<Vec<u8>>) -> Result<u64> {
4703        let dropped = self
4704            .absorber
4705            .objects
4706            .retain(&|oid: &[u8]| !dead.contains(oid))?;
4707        self.absorber
4708            .exploded
4709            .retain(&|oid: &[u8]| !dead.contains(oid))?;
4710        // **Everything folded from those two tables, refolded.** The commit
4711        // graph, the tree payloads, the ordinal space and the bitmaps are all
4712        // derivations of the rows that just went, and a derivation that still
4713        // names a dropped oid is *wrong* rather than merely stale — a `reachable`
4714        // over a graph holding dead commits selects objects the index can no
4715        // longer serve. This is the same [`Absorber::refold`] every absorb and
4716        // every open already calls, over the table this call just changed;
4717        // nothing about how the graph is rebuilt is touched here.
4718        //
4719        // The bitmaps go with it: they address **ordinals**, and dropping rows
4720        // renumbers the ordinal space, so `refold` clears them and the next
4721        // `reach_bitmaps()` builds them over the new numbering. Carrying them
4722        // across would be carrying an index into a different array.
4723        self.absorber.refold()?;
4724        Ok(dropped)
4725    }
4726
4727    /// **The journal's half of a GC: retire every pack whose objects are now all
4728    /// dead, before a single row is dropped.**
4729    ///
4730    /// Returns the offsets retired.
4731    ///
4732    /// # Why this exists
4733    ///
4734    /// §13.12's `indexed` bit is derived on open as *extent in the journal, rows
4735    /// not in the index* ([`Absorber::adopt_journal`]). Dropping every row of a
4736    /// pack produces that state exactly, so without this call the next open
4737    /// re-queues the pack and re-absorbs the objects a GC just decided were dead.
4738    /// A **partly** dead pack keeps rows and was never affected; this is only
4739    /// about the all-dead case.
4740    ///
4741    /// # What it does not do
4742    ///
4743    /// It does not truncate `objects.pack`. The blob file is append-only and a
4744    /// pack in the middle of it cannot be cut out without moving every extent
4745    /// after it — which would invalidate every index row in the archive. The dead
4746    /// pack's bytes stay; what changes is that nothing will ever index them
4747    /// again. Reclaiming those bytes is a rewrite of the blob file, and it is not
4748    /// this function.
4749    ///
4750    /// An arm with no journal ([`FastWriter`](crate::archive_write::FastWriter))
4751    /// has nothing to retire and nothing that re-queues, so it returns empty.
4752    ///
4753    /// # Cost
4754    ///
4755    /// One [`ObjectReadStack::extents_with_rows`] scan plus one batch lookup of
4756    /// the live set. Both are already the shape a GC pays elsewhere, and both run
4757    /// once per `gc()`, not per pack.
4758    /// Where the journal's acked extents end right now — the GC's snapshot of
4759    /// "packs that existed when I started". A pack acked past this point during
4760    /// the GC belongs to a push in flight and is not the GC's to judge.
4761    pub(crate) fn acked_end(&self) -> Result<u64> {
4762        let Some(journal) = self.arms.writer.journal(&self.blobs) else {
4763            return Ok(u64::MAX);
4764        };
4765        if !journal.exists() {
4766            return Ok(0);
4767        }
4768        let rows = crate::archive_write::read_journal(&journal)?;
4769        Ok(crate::archive_write::acked_packs(&rows)
4770            .iter()
4771            .map(|(o, l)| o + l)
4772            .max()
4773            .unwrap_or(0))
4774    }
4775
4776    /// Tombstone every acked pack that holds no live object — among the packs
4777    /// whose extent ends at or before `acked_end`, the GC's snapshot from
4778    /// [`acked_end`](Self::acked_end). A pack pushed after that snapshot has
4779    /// objects `live` never saw, and without the bound it would be retired for
4780    /// being unknown, which is how a GC would delete a push in flight.
4781    pub(crate) fn retire_dead_packs(
4782        &self,
4783        live: &std::collections::HashSet<String>,
4784        acked_end: u64,
4785    ) -> Result<Vec<u64>> {
4786        let Some(journal) = self.arms.writer.journal(&self.blobs) else {
4787            return Ok(Vec::new());
4788        };
4789        if !journal.exists() {
4790            return Ok(Vec::new());
4791        }
4792        let rows = crate::archive_write::read_journal(&journal)?;
4793        let packs: Vec<(u64, u64)> = crate::archive_write::acked_packs(&rows)
4794            .into_iter()
4795            .filter(|(o, l)| o + l <= acked_end)
4796            .collect();
4797        let already = crate::archive_write::retired_offsets(&rows);
4798        if packs.is_empty() {
4799            return Ok(Vec::new());
4800        }
4801
4802        // Which packs hold any row at all right now. A pack with none is either
4803        // already retired or was never absorbed — neither is this call's
4804        // business, and calling the second one dead would lose it.
4805        let occupied = self.absorber.objects.extents_with_rows(&packs)?;
4806
4807        // Which packs hold a row that survives. Placed by binary search over the
4808        // pack starts, the same way the scan above places a row.
4809        let mut order: Vec<usize> = (0..packs.len()).filter(|&i| packs[i].1 > 0).collect();
4810        order.sort_unstable_by_key(|&i| packs[i].0);
4811        let starts: Vec<u64> = order.iter().map(|&i| packs[i].0).collect();
4812        let live_oids: Vec<Vec<u8>> = live.iter().filter_map(|h| hex::decode(h).ok()).collect();
4813        let borrowed: Vec<&[u8]> = live_oids.iter().map(|o| o.as_slice()).collect();
4814        let mut has_live = vec![false; packs.len()];
4815        for (offset, _) in self
4816            .absorber
4817            .objects
4818            .extents_batch(&borrowed)
4819            .into_iter()
4820            .flatten()
4821        {
4822            let p = starts.partition_point(|&s| s <= offset);
4823            if p == 0 {
4824                continue;
4825            }
4826            let i = order[p - 1];
4827            if offset < packs[i].0 + packs[i].1 {
4828                has_live[i] = true;
4829            }
4830        }
4831
4832        let dead: Vec<u64> = (0..packs.len())
4833            .filter(|&i| occupied[i] && !has_live[i] && !already.contains(&packs[i].0))
4834            .map(|i| packs[i].0)
4835            .collect();
4836        // Durable before the rows go. The one ordering this function has.
4837        crate::archive_write::retire_packs(&journal, &dead)?;
4838        Ok(dead)
4839    }
4840
4841    /// **Write generation 0** at [`archive_path`](Self::archive_path), carrying
4842    /// the verbatim packs this store has acked plus `reserved`.
4843    ///
4844    /// The whole of the decision-making lives in
4845    /// [`seal_generation_zero`](crate::archive_write::seal_generation_zero); this
4846    /// is the three paths it needs, taken off the same fields the push path and
4847    /// the GC read, so a seal cannot address a different blob file or a different
4848    /// journal from the one that acked the packs.
4849    pub(crate) fn seal_archive(
4850        &self,
4851        reserved: Vec<ReservedSection>,
4852    ) -> Result<crate::archive_write::SealReport> {
4853        crate::archive_write::seal_generation_zero(
4854            &self.blobs,
4855            self.arms.writer.journal(&self.blobs).as_deref(),
4856            &self.archive,
4857            reserved,
4858        )
4859    }
4860
4861    /// The sections a sealed archive carries: the ref log, the commit graph and
4862    /// the bitmaps.
4863    pub(crate) fn reserved_sections(&self) -> Result<Vec<ReservedSection>> {
4864        use znippy_common::{GUNNAR_GRAPH_MODULE, GUNNAR_REACH_MODULE};
4865        let mut out = vec![self.refs.seal_section()?];
4866        let d = self
4867            .absorber
4868            .derived
4869            .read()
4870            .map_err(|_| anyhow!("derived poisoned"))?;
4871        if !d.graph.is_empty() {
4872            out.push(ReservedSection::arrow(
4873                GUNNAR_GRAPH_MODULE,
4874                crate::graph::graph_schema(),
4875                vec![crate::graph::build_graph_batch(&d.graph)?],
4876            ));
4877        }
4878        drop(d);
4879        let reach = self.reach_bitmaps()?;
4880        if !reach.is_empty() {
4881            out.push(ReservedSection::arrow(
4882                GUNNAR_REACH_MODULE,
4883                crate::reach::reach_schema(),
4884                vec![crate::reach::build_reach_batch(&reach)?],
4885            ));
4886        }
4887        Ok(out)
4888    }
4889}
4890
4891// ── the runtime selector ──────────────────────────────────────────────────────
4892
4893/// **A [`GitStore`] whose index arm was chosen at run time.**
4894///
4895/// The [`ObjectIndex`] arm is a *type*, so a value can only choose it by naming
4896/// every monomorphisation. This enum is that list, and it is an enum rather than
4897/// a `Box<dyn GitOps>` for two reasons:
4898///
4899/// * a boxed trait object can only offer the twelve, and the arms differ in
4900///   things the twelve deliberately do not expose — how many Arrow bytes the
4901///   projection actually materialised, which writer is on the ack path. A bench
4902///   or an operator dump needs those, and a guard needs them to assert that the
4903///   selector selected **on applied output** rather than on a label;
4904/// * there is no allocation and no vtable: the `match` is one predictable branch
4905///   at the top of a call, and everything under it — the `stree` probe, the
4906///   Arrow gather, the redb tail — is the same fully static code
4907///   [`GitStore::<S>::open_with_arms`](GitStore::open_with_arms) produces,
4908///   because `S` is known inside each arm.
4909///
4910/// # What it costs, stated rather than hidden
4911///
4912/// One branch per [`GitOps`] call — **not per object**. `extents(&[1000 oids])`
4913/// is one branch and a thousand monomorphised lookups behind it. The binary
4914/// carries three copies of the store, which is the price of picking a type at
4915/// run time and there is no version of this that does not pay it.
4916///
4917/// A caller that knows its arm at compile time should **not** come through here:
4918/// [`GitStore::open`] and [`GitStore::open_with_arms`] hand back a concrete
4919/// store with no branch at all, and that includes every caller that wants the
4920/// default.
4921pub enum SelectedStore {
4922    OneTableFourColumns(GitStore<OneTableFourColumns>),
4923    FourTables(GitStore<crate::index_layout::FourTables>),
4924    PackedPayload(GitStore<crate::index_layout::PackedPayload>),
4925}
4926
4927/// One expression, evaluated against whichever concrete store this is. The
4928/// delegation below is generated from it so there is no second copy of any
4929/// method body (LAW 5).
4930macro_rules! on_arm {
4931    ($self:ident, $s:ident => $body:expr) => {
4932        match $self {
4933            SelectedStore::OneTableFourColumns($s) => $body,
4934            SelectedStore::FourTables($s) => $body,
4935            SelectedStore::PackedPayload($s) => $body,
4936        }
4937    };
4938}
4939
4940impl SelectedStore {
4941    /// The arms this store was built with.
4942    pub fn arms(&self) -> StoreConfig {
4943        on_arm!(self, s => s.arms())
4944    }
4945
4946    /// **The explode band** — bytes held in each pool, the budget, and what has
4947    /// been evicted since this store was opened. `Admin.StorageInfo` and the
4948    /// dashboard's `explode 3.1G/8G` cell.
4949    pub fn explode_cache_use(&self) -> crate::exploded_cache::CacheUse {
4950        on_arm!(self, s => s.explode_cache_use())
4951    }
4952
4953    /// **This repository's explode quota, set live.** What the control plane
4954    /// calls when an account's plan changes. It never refuses a read and never
4955    /// deletes on the caller's thread: over budget evicts at the next rotation,
4956    /// and an account at its limit still serves every object — from the pack,
4957    /// which is slower and never wrong.
4958    pub fn set_explode_budget(&self, bytes: u64) {
4959        on_arm!(self, s => s.set_explode_budget(bytes))
4960    }
4961
4962    /// **`repo gc` reclaims the cache**: every lazy segment unlinked, the graph
4963    /// pool kept. Returns the bytes that left the filesystem.
4964    pub fn drop_explode_cache(&self) -> anyhow::Result<u64> {
4965        on_arm!(self, s => s.drop_explode_cache())
4966    }
4967
4968    /// The selected writer's name, as it goes on a bench row.
4969    pub fn writer_name(&self) -> &'static str {
4970        on_arm!(self, s => s.writer_name())
4971    }
4972
4973    /// What the selected writer's `append` promises.
4974    pub fn writer_durability(&self) -> &'static str {
4975        on_arm!(self, s => s.writer_durability())
4976    }
4977
4978    /// **Arrow IPC bytes the selected index arm actually materialised.**
4979    ///
4980    /// The applied output that distinguishes one layout from another: a packed
4981    /// 25-byte column, four columns in one section and four independent
4982    /// sections are three different numbers for the same objects. This is what
4983    /// a guard reads to prove the selector selected, because a *name* would
4984    /// prove only that a name was copied.
4985    pub fn index_ipc_bytes(&self) -> usize {
4986        on_arm!(self, s => s.index().ipc_bytes())
4987    }
4988
4989    /// **The name the built projection reports about itself** —
4990    /// `OneTableFourColumns`, `FourTables` or `PackedPayload`.
4991    ///
4992    /// The other half of the applied-output pair [`index_ipc_bytes`] starts:
4993    /// the byte count separates the arms once objects are in the store, and
4994    /// this separates them from the instant it is built, including on an empty
4995    /// store where all three materialise nothing. It is the value
4996    /// [`crate::arms::IndexArm::projection_name`] exists to be compared with —
4997    /// what the layout calls itself, never what the selector asked for — so a
4998    /// server logging it is stating what it built rather than repeating its own
4999    /// environment back.
5000    ///
5001    /// [`index_ipc_bytes`]: SelectedStore::index_ipc_bytes
5002    pub fn index_name(&self) -> &'static str {
5003        on_arm!(self, s => s.index().projection_name())
5004    }
5005
5006    /// The oids a pushed extent introduced — [`GitStore::oids_in_extent`],
5007    /// delegated.
5008    ///
5009    /// Not one of the twelve (a pack's extent is a znippy concept), so it does
5010    /// not arrive with the [`GitOps`] impl below and has to be forwarded by
5011    /// hand. A server that reports the objects a push introduced needs it, and
5012    /// a server that picked its index arm at run time still needs it.
5013    pub fn oids_in_extent(&self, extent: Extent) -> Result<Vec<Vec<u8>>> {
5014        on_arm!(self, s => s.oids_in_extent(extent))
5015    }
5016
5017    /// Objects in the `objects` table.
5018    pub fn object_count(&self) -> usize {
5019        on_arm!(self, s => s.object_count())
5020    }
5021
5022    /// Block until the background drain has absorbed everything pushed.
5023    pub fn wait_indexed(&self) {
5024        on_arm!(self, s => s.wait_indexed())
5025    }
5026
5027    /// Stop this store's index work; see [`GitStore::cancel_index`].
5028    pub fn cancel_index(&self) {
5029        on_arm!(self, s => s.cancel_index())
5030    }
5031
5032    /// This store's index progress; see [`GitStore::index_progress`].
5033    pub fn index_progress(&self) -> crate::progress::Snapshot {
5034        on_arm!(self, s => s.index_progress())
5035    }
5036
5037    /// Rebuild the projection, so the layout under test is what answers rather
5038    /// than the redb tail.
5039    pub fn rebuild_projection(&self) -> Result<()> {
5040        on_arm!(self, s => s.index().rebuild())
5041    }
5042
5043    /// The twelfth method — inherent, not on [`GitOps`], because it returns
5044    /// Arrow `ReservedSection`s a gix backend has no analog for.
5045    pub fn seal(&self) -> Result<Vec<ReservedSection>> {
5046        on_arm!(self, s => s.seal())
5047    }
5048}
5049
5050impl GitOps for SelectedStore {
5051    fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> Result<TxId> {
5052        on_arm!(self, s => s.put(pack, refs))
5053    }
5054    fn put_pack(&self, bytes: &[u8]) -> Result<TxId> {
5055        on_arm!(self, s => s.put_pack(bytes))
5056    }
5057    // Delegated like every other method, and that is the point: without these
5058    // two lines a `SelectedStore` would silently inherit the trait's *default*
5059    // `put_pack_observed` — the one that publishes nothing — and every store
5060    // gunnar actually opens goes through this enum. The count would have been
5061    // implemented in `store.rs` and unreachable in production.
5062    fn put_pack_observed(
5063        &self,
5064        bytes: &[u8],
5065        progress: &dyn git_storage_trait::PackProgress,
5066    ) -> Result<TxId> {
5067        on_arm!(self, s => s.put_pack_observed(bytes, progress))
5068    }
5069    fn publishes_pack_progress(&self) -> bool {
5070        on_arm!(self, s => s.publishes_pack_progress())
5071    }
5072    fn put_refs(&self, updates: &[RefUpdate]) -> Result<TxId> {
5073        on_arm!(self, s => s.put_refs(updates))
5074    }
5075    fn get(&self, oid: Oid<'_>) -> Result<Option<Stored>> {
5076        on_arm!(self, s => s.get(oid))
5077    }
5078    fn has(&self, oid: Oid<'_>) -> Result<bool> {
5079        on_arm!(self, s => s.has(oid))
5080    }
5081    fn size(&self, oid: Oid<'_>) -> Result<Option<u64>> {
5082        on_arm!(self, s => s.size(oid))
5083    }
5084    fn extents(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<Extent>>> {
5085        on_arm!(self, s => s.extents(oids))
5086    }
5087    fn refs(&self) -> Result<Vec<RefRow>> {
5088        on_arm!(self, s => s.refs())
5089    }
5090    fn update_ref(&self, name: &str, old: Option<Oid<'_>>, new: Option<Oid<'_>>) -> Result<TxId> {
5091        on_arm!(self, s => s.update_ref(name, old, new))
5092    }
5093    fn put_refs_cas(&self, edits: &[git_storage_trait::RefCas<'_>]) -> Result<TxId> {
5094        on_arm!(self, s => s.put_refs_cas(edits))
5095    }
5096    fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> Result<Vec<Vec<u8>>> {
5097        on_arm!(self, s => s.reachable(want, have))
5098    }
5099    fn gc(&self) -> Result<GcReport> {
5100        on_arm!(self, s => s.gc())
5101    }
5102}
5103
5104/// The reading contract, delegated the same way [`GitOps`] is.
5105///
5106/// Every method is one predictable branch over a monomorphised body — the same
5107/// arrangement, and the same cost argument, [`SelectedStore`] makes for the
5108/// eleven. It is here rather than in [`crate::serve`] only because the macro
5109/// that generates it lives in this module.
5110impl crate::serve::GitServe for SelectedStore {
5111    fn read(&self, oid: Oid<'_>) -> Result<Option<(crate::index_layout::ObjType, Vec<u8>)>> {
5112        on_arm!(self, s => crate::serve::GitServe::read(s, oid))
5113    }
5114    fn header(&self, oid: Oid<'_>) -> Result<Option<(crate::index_layout::ObjType, u64)>> {
5115        on_arm!(self, s => crate::serve::GitServe::header(s, oid))
5116    }
5117    fn sizes(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<u64>>> {
5118        on_arm!(self, s => crate::serve::GitServe::sizes(s, oids))
5119    }
5120    fn index_now(&self) -> Result<usize> {
5121        on_arm!(self, s => crate::serve::GitServe::index_now(s))
5122    }
5123    fn head(&self) -> Result<Option<RefRow>> {
5124        on_arm!(self, s => crate::serve::GitServe::head(s))
5125    }
5126    fn set_head(&self, target: &str) -> Result<TxId> {
5127        on_arm!(self, s => crate::serve::GitServe::set_head(s, target))
5128    }
5129    /// `objects` is the **exact set to emit**, not tips to close over — the
5130    /// contract's word, and this forwarder uses it so that the name cannot drift
5131    /// back into `want` one arm at a time. See [`crate::serve::GitServe::emit_pack`].
5132    fn emit_pack(
5133        &self,
5134        objects: &[Oid<'_>],
5135        have: &[Oid<'_>],
5136        caps: &git_storage_trait::Caps,
5137        out: &mut dyn std::io::Write,
5138    ) -> Result<git_storage_trait::PackStats> {
5139        on_arm!(self, s => crate::serve::GitServe::emit_pack(s, objects, have, caps, out))
5140    }
5141    fn select(
5142        &self,
5143        want: &[Oid<'_>],
5144        have: &[Oid<'_>],
5145    ) -> Result<Option<git_storage_trait::ReachSet>> {
5146        on_arm!(self, s => crate::serve::GitServe::select(s, want, have))
5147    }
5148}
5149
5150/// **Open a store with all three arms chosen at run time.**
5151///
5152/// The one place [`crate::arms::IndexArm`] — a value — is turned into `S` — a
5153/// type. See [`SelectedStore`] for what the resulting handle costs.
5154pub fn open_selected(
5155    root: &Path,
5156    account: &str,
5157    hash: GitHashKind,
5158    arms: StoreConfig,
5159) -> Result<SelectedStore> {
5160    use crate::arms::IndexArm;
5161    use crate::index_layout::{FourTables, PackedPayload};
5162    Ok(match arms.index {
5163        IndexArm::OneTableFourColumns => {
5164            SelectedStore::OneTableFourColumns(GitStore::<OneTableFourColumns>::open_with_arms(
5165                root, account, hash, arms,
5166            )?)
5167        }
5168        IndexArm::FourTables => SelectedStore::FourTables(GitStore::<FourTables>::open_with_arms(
5169            root, account, hash, arms,
5170        )?),
5171        IndexArm::PackedPayload => SelectedStore::PackedPayload(
5172            GitStore::<PackedPayload>::open_with_arms(root, account, hash, arms)?,
5173        ),
5174    })
5175}
5176
5177/// **The env-driven front door**: read the three variables **once**, here, and
5178/// build the store they name.
5179///
5180/// This is the whole runtime selector a server or a bench needs — one call, one
5181/// triple of `getenv`s, and from then on the store holds built objects and reads
5182/// nothing. There is deliberately no per-operation lookup of any kind: a
5183/// `std::env::var` inside a serving loop would be one syscall per object served,
5184/// which is a real defect that was found and fixed in gunnar the day before this
5185/// was written. [`crate::arms::env_reads`] counts every read this crate makes so
5186/// that "once, at construction" is an assertion rather than a claim.
5187///
5188/// An unset variable takes the shipping default, so a process with a clean
5189/// environment gets exactly [`GitStore::open`]'s combination.
5190pub fn open_from_env(root: &Path, account: &str, hash: GitHashKind) -> Result<SelectedStore> {
5191    let arms = StoreConfig::from_env()?;
5192    open_selected(root, account, hash, arms)
5193}
5194
5195impl<S: ObjectIndex> Absorber<S> {
5196    /// **One object's resolved content, and §14's table is what answers.**
5197    ///
5198    /// Two paths, and they are not interchangeable even though they return
5199    /// identical bytes:
5200    ///
5201    /// 1. **the exploded table** — one point lookup, no inflate, no delta chain;
5202    /// 2. **re-derived from the verbatim truth** — find which pack holds the
5203    ///    object, read the whole pack back, resolve it. Correct and slow, and it
5204    ///    exists because the table is droppable: a store whose table has been
5205    ///    deleted, or whose re-explosion has not caught up, must still answer.
5206    ///
5207    /// The counters are the only way to tell them apart. Identical bytes come
5208    /// back either way, so a byte comparison proves nothing about which path ran
5209    /// — that is the identity-value trap in its exact form, and it is why
5210    /// [`crate::exploded::ExplodedStats::rederived`] is bumped here rather than
5211    /// inferred.
5212    ///
5213    /// On the [`Absorber`] rather than on [`GitStore`] because that is where the
5214    /// absorb runs: the background worker has no store handle, and a base lookup
5215    /// that needed one would put the whole store behind the drain.
5216    fn resolved(&self, oid: &[u8]) -> Result<Option<(GitObjectKind, Vec<u8>)>> {
5217        Ok(self
5218            .resolved_costed(oid)?
5219            .map(|(kind, payload, _)| (kind, payload)))
5220    }
5221
5222    /// [`Self::resolved`] and the bill. `None` for the cost means a pool
5223    /// answered and nothing was rebuilt.
5224    #[allow(clippy::type_complexity)]
5225    fn resolved_costed(
5226        &self,
5227        oid: &[u8],
5228    ) -> Result<Option<(GitObjectKind, Vec<u8>, Option<crate::resolve::ReadCost>)>> {
5229        // (1) §14's table.
5230        if let Some((kind, payload)) = self.exploded.content(oid)? {
5231            return Ok(Some((kind, payload, None)));
5232        }
5233        // (2) the verbatim truth, re-derived.
5234        let Some(row) = self.objects.lookup(oid) else {
5235            // (3) not indexed yet: the pending packs, read-only, on this
5236            // reader's own thread — never by waiting for the worker.
5237            //
5238            // **This arm is the ack path's trap and it is not the ack path's
5239            // fix.** `a_read_before_the_drain_is_served_from_the_pack_and_never_waits`
5240            // is the law here: a clone must be answered while the drain is
5241            // parked, so a read may not take the absorb gate. What must not
5242            // happen is the ACK arriving here, and it no longer does —
5243            // `GitServe::index_now` runs before the connectivity check, so by
5244            // the time the check reads there is nothing pending to scan. See
5245            // [`Absorber::index_pending_now`].
5246            if self.unindexed_packs() == 0 {
5247                return Ok(None);
5248            }
5249            self.exploded.note_rederived();
5250            let (_, content) = self.scan_pending(&[], Some(oid))?;
5251            // The pending scan resolves through a `PackWalk` rather than a
5252            // chain walk, so it does not produce a bill. `None` is honest here
5253            // and is not "it was free".
5254            return Ok(content.map(|(k, p)| (k, p, None)));
5255        };
5256        // ── ONE CHAIN, not one pack ─────────────────────────────────────
5257        //
5258        // The row's `offset` addresses the entry in the archive directly, so the
5259        // object is materialised by following its own delta chain out of the
5260        // mapping — `d` inflates and `d − 1` delta applications, nothing else
5261        // touched. This used to `read_extent` the WHOLE containing pack and
5262        // re-resolve it: for `torvalds/linux` an 8.12 GiB read and a 13.9 M
5263        // object resolve **per object served**, which is the cost that forced
5264        // the exploded tier to be built eagerly at push time — and that tier
5265        // MEASURED 141.7 GiB for an 8.12 GiB pack (17×). Lazy explode is only
5266        // affordable because of this function
5267        // (`ideas/lazy-explode-index-only-at-push.md`).
5268        //
5269        // It also does not care whether the containing pack has been ABSORBED:
5270        // the bytes were fsynced before the ack, so an object whose row is in
5271        // the index is readable the instant the row is. The pending-pack scan
5272        // below is for the window before the row exists at all.
5273        self.exploded.note_rederived();
5274        let snap = self.map.snapshot()?;
5275        let src = ArchiveWindow { abs: self, snap: &snap };
5276        let offset_of = |base: &[u8]| self.objects.lookup(base).map(|r| r.offset);
5277        match crate::resolve::materialize_at(
5278            &src,
5279            row.offset,
5280            self.hash.oid_len(),
5281            &offset_of,
5282            self,
5283        ) {
5284            Ok((kind, payload, cost)) => {
5285                self.keep_for_the_next_reader(oid, kind, &payload, cost);
5286                Ok(Some((kind, payload, Some(cost))))
5287            }
5288            Err(e) => {
5289                // A chain that cannot be followed out of the mapping is not a
5290                // wrong answer to hand back: fall through to the pending scan,
5291                // which re-derives from the pack the row came in on.
5292                //
5293                // **And if that finds nothing either, the ORIGINAL error is
5294                // what comes back — never `Ok(None)`.** The row says this store
5295                // holds the object; a failed read of an object we hold is a
5296                // store defect, and answering "absent" for it turned that defect
5297                // into a refusal of somebody else's push (lane AI, 2026-08-30 —
5298                // see `crate::resolve::MAX_DELTA_LINKS`). Absence is a fact the
5299                // index states, not a shape an error decays into.
5300                if self.unindexed_packs() == 0 {
5301                    return Err(e);
5302                }
5303                match self.scan_pending(&[], Some(oid))? {
5304                    (_, Some((k, p))) => Ok(Some((k, p, None))),
5305                    (_, None) => Err(e),
5306                }
5307            }
5308        }
5309    }
5310}
5311
5312impl<S: ObjectIndex> Absorber<S> {
5313    /// **The write-back, and it is off the reader's path.**
5314    ///
5315    /// The reader has its bytes already — this buffers a copy into the lazy
5316    /// pool's newest segment (a `memcpy` under one lock, no I/O, no fsync, no
5317    /// eviction) and, once 4 MiB have accumulated, starts a thread to do the
5318    /// writing. That is the owner's law from the design note in one function:
5319    /// *nobody waits*. A failure here is not the read's failure — the bytes
5320    /// were correct before the cache was consulted and are correct after it
5321    /// refuses — so it is counted and swallowed rather than propagated.
5322    ///
5323    /// **It does not always write.** A shallow, small object is cheaper to
5324    /// inflate again than to keep — a whole object is one `inflate` of one zlib
5325    /// stream at one offset — so the cache declines it and counts the read as
5326    /// `direct_reads` (`exploded_cache::worth_caching`). That is the owner's
5327    /// refinement, 2026-08-30: *do not explode uniformly*. The reader that just
5328    /// paid the bill is the only thing that knows what it was, which is why the
5329    /// cost comes back out of [`crate::resolve::materialize_at`] rather than
5330    /// being predicted from an index column.
5331    ///
5332    /// Two readers racing on one oid is benign: identical bytes, and the table
5333    /// keeps the first row it filed under that key.
5334    fn keep_for_the_next_reader(
5335        &self,
5336        oid: &[u8],
5337        kind: GitObjectKind,
5338        payload: &[u8],
5339        cost: crate::resolve::ReadCost,
5340    ) {
5341        match self.exploded.write_back(oid, kind, payload, cost) {
5342            Ok(true) => self.exploded.kick(),
5343            Ok(false) => {}
5344            Err(e) => {
5345                tracing::debug!(
5346                    target: "znippy.git",
5347                    store = %self.label,
5348                    error = %e,
5349                    "the exploded cache would not take a write-back; the read is unaffected"
5350                );
5351            }
5352        }
5353    }
5354}
5355
5356/// [`crate::reach::TreeSource`] over a live store — see
5357/// [`Absorber::tree_source`].
5358/// The oid a tag object points at, out of its header.
5359///
5360/// A tag object's payload begins `object <hex>\n` — that is the format, not a
5361/// heuristic, and it is the whole parse. Deliberately NOT reached for with a
5362/// git object library: this crate is a shipped archiver and `gix-object` is a
5363/// dev-dependency here, so pulling it into the read path to read one line would
5364/// put the pack pipeline back in an archive-only build (S-071).
5365fn tag_target_hex(payload: &[u8], oid_len: usize) -> Option<String> {
5366    let line = payload.split(|b| *b == b'\n').next()?;
5367    let rest = line.strip_prefix(b"object ")?;
5368    // Exactly the hash width this store uses, and hex. A shorter or longer
5369    // field is a malformed tag, and guessing at one would put a wrong oid into
5370    // a reachability answer — the one place a wrong answer is silent.
5371    if rest.len() != oid_len * 2 || !rest.iter().all(u8::is_ascii_hexdigit) {
5372        return None;
5373    }
5374    std::str::from_utf8(rest).ok().map(str::to_owned)
5375}
5376
5377/// Add what a tag points at — and what THAT reaches — to a reachability union.
5378///
5379/// See the call site in `reachable_raw_with` for why this exists and why doing
5380/// it in the `want` loop is complete rather than partial.
5381///
5382/// A tag of a tag is legal git, so this follows the chain; `MAX_TAG_HOPS`
5383/// bounds it because a corrupt archive could point one at itself and a
5384/// reachability walk must terminate on bad data, not hang on it.
5385fn peel_tag_into<S: ObjectIndex>(
5386    abs: &Absorber<S>,
5387    tag_hex: &str,
5388    view: &crate::reach::ReachView<'_>,
5389    facts: &crate::reach::ObjectFacts<'_>,
5390    union: &mut roaring::RoaringBitmap,
5391) {
5392    const MAX_TAG_HOPS: usize = 8;
5393    let mut cursor = tag_hex.to_owned();
5394    for _ in 0..MAX_TAG_HOPS {
5395        let Ok(raw) = hex::decode(&cursor) else { return };
5396        // Not a tag, unreadable, or absent: nothing to peel. An unreadable
5397        // object makes the closure SHORT, and a short closure is refused
5398        // downstream by name (`missing necessary objects`) rather than served
5399        // as though it were complete — the same contract `TreesFromStore::tree`
5400        // documents.
5401        let Ok(Some((GitObjectKind::Tag, payload))) = abs.resolved(&raw) else { return };
5402        let Some(target) = tag_target_hex(&payload, facts.oid_len) else { return };
5403        if let Some(bm) = view.bitmap(target.as_str()) {
5404            *union |= bm;
5405            return;
5406        }
5407        if view.is_commit(target.as_str()) {
5408            crate::reach::accumulate(target.as_str(), view, facts, union);
5409            return;
5410        }
5411        let Some(o) = facts.ordinal.ordinal_of_hex(target.as_str()) else { return };
5412        union.insert(o);
5413        // A tag may point at a TREE, whose subtrees and blobs are then just as
5414        // necessary as a commit's would be. Inserting the tree alone would be
5415        // the same bug one level down.
5416        match abs.resolved(&hex::decode(&target).unwrap_or_default()) {
5417            Ok(Some((GitObjectKind::Tree, _))) => {
5418                crate::reach::accumulate_tree(target.as_str(), facts, union);
5419                return;
5420            }
5421            Ok(Some((GitObjectKind::Tag, _))) => cursor = target,
5422            // A blob, or unreadable: it is in, and it has no closure.
5423            _ => return,
5424        }
5425    }
5426}
5427
5428struct TreesFromStore<'a, S: ObjectIndex> {
5429    abs: &'a Absorber<S>,
5430}
5431
5432impl<S: ObjectIndex> crate::reach::TreeSource for TreesFromStore<'_, S> {
5433    fn tree(&self, oid_hex: &str) -> Option<std::borrow::Cow<'_, [u8]>> {
5434        let raw = hex::decode(oid_hex).ok()?;
5435        // A read that fails is `None`, which is what an object this archive
5436        // does not hold has always answered here — the walk stops at it. It is
5437        // NOT silently dropping an error that matters: an unreadable tree makes
5438        // the closure short, and a short closure is refused downstream by name
5439        // (`missing necessary objects`) rather than served as if it were
5440        // complete.
5441        match self.abs.resolved(&raw) {
5442            Ok(Some((GitObjectKind::Tree, payload))) => Some(std::borrow::Cow::Owned(payload)),
5443            // A blob, a tag, a gitlink, or absent: all "not a tree here".
5444            Ok(_) => None,
5445            Err(e) => {
5446                tracing::debug!(
5447                    target: "znippy.git",
5448                    store = %self.abs.label,
5449                    oid = oid_hex,
5450                    error = %e,
5451                    "a reachability walk could not read a tree; the walk stops there"
5452                );
5453                None
5454            }
5455        }
5456    }
5457}
5458
5459/// The verbatim archive as [`crate::resolve::ArchiveBytes`]: **the mapping
5460/// where it reaches, a `pread` past its end.**
5461///
5462/// One snapshot is taken per read and sliced as many times as the chain is
5463/// deep, which is [`crate::archive_map::ArchiveMap::snapshot`]'s contract. The
5464/// `pread` arm is the one a push that landed after the snapshot takes; both
5465/// arms return the same bytes and differ only in what they cost.
5466struct ArchiveWindow<'a, S: ObjectIndex> {
5467    abs: &'a Absorber<S>,
5468    snap: &'a crate::archive_map::Mapped,
5469}
5470
5471impl<S: ObjectIndex> crate::resolve::ArchiveBytes for ArchiveWindow<'_, S> {
5472    fn at(&self, at: u64, len: u64) -> Result<std::borrow::Cow<'_, [u8]>> {
5473        clamped_window(self.abs, self.snap, at, len)
5474    }
5475}
5476
5477/// At most `len` bytes of the archive at `at`: the mapping where it reaches, a
5478/// `pread` past its end, and **fewer bytes at the end of the file** instead of
5479/// a refusal. A probe near the end of the archive legitimately gets fewer bytes
5480/// than it asked for, so the window is clamped rather than refused — the
5481/// parsers read a grammar, not a fixed width.
5482fn clamped_window<'m, S: ObjectIndex>(
5483    abs: &Absorber<S>,
5484    snap: &'m crate::archive_map::Mapped,
5485    at: u64,
5486    len: u64,
5487) -> Result<std::borrow::Cow<'m, [u8]>> {
5488    let mapped = snap.len();
5489    if at < mapped {
5490        let have = (mapped - at).min(len);
5491        if let Some(b) = snap.get(at, have) {
5492            return Ok(std::borrow::Cow::Borrowed(b));
5493        }
5494    }
5495    let end = abs.reader.metadata().map(|m| m.len()).unwrap_or(mapped);
5496    if at >= end {
5497        return Ok(std::borrow::Cow::Owned(Vec::new()));
5498    }
5499    let have = (end - at).min(len);
5500    Ok(std::borrow::Cow::Owned(abs.read_extent(at, have)?))
5501}
5502
5503/// A thin pack's external delta base, answered from §14's exploded table.
5504///
5505/// Before that table existed this had to read and re-resolve a whole pack per
5506/// base, and it deliberately cached nothing — because a cache here *would* have
5507/// been the undecided table. It is decided now (eager, 2026-08-08), so the cache
5508/// is the table and this is one point lookup.
5509///
5510/// **`None` means absent and `Err` means unreadable, and they are not the same
5511/// answer.** A base this store does not hold still comes back `Ok(None)` and
5512/// [`crate::resolve::ResolveError::missing_base`] refuses the pack by name.
5513///
5514/// # 🔴 This was `.ok().flatten()`
5515///
5516/// A store that HELD the base and could not reconstruct it answered exactly
5517/// like one that never had it, so the resolver blamed the pushed pack: *"this
5518/// repository genuinely does not have that object"*, about an object one index
5519/// lookup away. That sentence went to the client on the `ng` line and to the
5520/// operator in the log, and it named the wrong party — lane AI, 2026-08-30, 21
5521/// of 140 ordinary pushes. The error travels now.
5522impl<S: ObjectIndex> BaseSource for Absorber<S> {
5523    fn content(&self, oid: &[u8]) -> Result<Option<(GitObjectKind, Vec<u8>)>> {
5524        self.resolved(oid).with_context(|| {
5525            format!(
5526                "reading the delta base {} out of this store — it is not absent, it could not \
5527                 be reconstructed",
5528                hex::encode(oid)
5529            )
5530        })
5531    }
5532}
5533
5534#[cfg(test)]
5535mod tests {
5536    use super::*;
5537    use crate::archive_write::{read_journal, SafeWriter};
5538    use crate::object::GitObjectKind;
5539    use crate::store::tests::{one_blob_pack, corpus_pack, tmpdir};
5540    use std::collections::HashSet;
5541    use std::sync::atomic::{AtomicBool, AtomicU64, Ordering as AtomicOrdering};
5542    use std::time::{Duration, Instant};
5543    use znippy_zoomies::background::Job;
5544    use znippy_zoomies::gatling_forkjoin::gatling_for_each;
5545
5546    fn loadavg() -> String {
5547        std::fs::read_to_string("/proc/loadavg")
5548            .unwrap_or_default()
5549            .split_whitespace()
5550            .take(3)
5551            .collect::<Vec<_>>()
5552            .join(" ")
5553    }
5554
5555    /// **The one representation answers exactly what the two it replaced did.**
5556    ///
5557    /// Before S-082 the store carried the ordinal space three times: `oids_raw`
5558    /// (flat bytes), `oids` (hex `Vec<String>`) and `ordinal`
5559    /// (`HashMap<String, u32>`). The last two are gone; oid ⇄ ordinal is now a
5560    /// slice and a binary search over `oids_raw`, through
5561    /// [`crate::reach::OrdinalSpace`]. This rebuilds those two OLD tables inline
5562    /// from the SAME `oids_raw` the store folded, and asserts the new lookups
5563    /// return the identical answer **for every oid in a real corpus** — the
5564    /// property that lets the field be removed without a server ever naming the
5565    /// wrong object in a pack, which no exit code could see.
5566    ///
5567    /// Seen RED by an off-by-one binary search (`Equal => Some(mid as u32 + 1)`
5568    /// in [`crate::reach::OrdinalSpace::ordinal_of_raw`]): every
5569    /// `ordinal_of_raw` / `ordinal_of_hex` below disagreed with the old
5570    /// `HashMap`, and the closure comparison at the end disagreed as well.
5571    #[test]
5572    fn the_one_ordinal_space_answers_what_the_two_it_replaced_did() {
5573        let dir = tmpdir("ordinal-space-one-representation");
5574        let (pack, rows) = corpus_pack();
5575        let store = GitStore::open(&dir, "alice").unwrap();
5576        store.put_pack(&pack).unwrap();
5577        store.wait_indexed();
5578
5579        let oid_len = store.hash_kind().oid_len();
5580        let raw = {
5581            let d = store.absorber.derived.read().unwrap();
5582            d.oids_raw.clone()
5583        };
5584        // The two tables `refold` used to build, rebuilt here from the same
5585        // bytes — this IS the "old" answer the new lookups must equal.
5586        let old_oids: Vec<String> = raw.chunks_exact(oid_len).map(hex::encode).collect();
5587        let old_ordinal: std::collections::HashMap<String, u32> = old_oids
5588            .iter()
5589            .enumerate()
5590            .map(|(i, o)| (o.clone(), i as u32))
5591            .collect();
5592
5593        assert!(
5594            !old_oids.is_empty(),
5595            "the fixture pack ({} entries) folded to an EMPTY ordinal space, so every \
5596             comparison below would hold vacuously",
5597            rows.len()
5598        );
5599        assert_eq!(
5600            raw.len(),
5601            old_oids.len() * oid_len,
5602            "the raw ordinal space is {} bytes for {} oids of {oid_len} bytes",
5603            raw.len(),
5604            old_oids.len()
5605        );
5606
5607        let space = crate::reach::OrdinalSpace::new(&raw, oid_len);
5608        assert_eq!(space.len(), old_oids.len(), "the space holds a different count");
5609        let mut checked = 0usize;
5610        for (o, h) in old_oids.iter().enumerate() {
5611            let want_ord = old_ordinal[h];
5612            // oid → ordinal, hex and raw, both == the old HashMap answer.
5613            assert_eq!(
5614                space.ordinal_of_hex(h),
5615                Some(want_ord),
5616                "ordinal_of_hex({h}) != the old HashMap answer {want_ord}"
5617            );
5618            let raw_oid = hex::decode(h).unwrap();
5619            assert_eq!(
5620                space.ordinal_of_raw(&raw_oid),
5621                Some(want_ord),
5622                "ordinal_of_raw disagrees with the old HashMap at {h}"
5623            );
5624            // ordinal → oid == the old hex Vec's slice, in both spellings.
5625            assert_eq!(
5626                space.oid(o as u32),
5627                Some(raw_oid.as_slice()),
5628                "oid({o}) != the old oids_raw slice"
5629            );
5630            assert_eq!(
5631                space.oid(o as u32).map(hex::encode).as_deref(),
5632                Some(h.as_str()),
5633                "oid({o}) hex != the old hex Vec entry {h}"
5634            );
5635            checked += 1;
5636        }
5637        assert_eq!(checked, old_oids.len());
5638
5639        // A non-member answers None both ways, exactly as `HashMap::get` did for
5640        // an absent key — the binary search must not land on a neighbour.
5641        let absent = vec![0xffu8; oid_len];
5642        assert_eq!(space.ordinal_of_raw(&absent), None, "an absent oid resolved to an ordinal");
5643        assert_eq!(
5644            space.ordinal_of_hex(&hex::encode(&absent)),
5645            None,
5646            "an absent hex oid resolved to an ordinal"
5647        );
5648        // A wrong-width key is absent, not a panic.
5649        assert_eq!(space.ordinal_of_raw(&absent[..oid_len - 1]), None);
5650        assert_eq!(space.oid(old_oids.len() as u32), None, "an out-of-range ordinal named an oid");
5651
5652        // And the answer built out of it: the raw closure IS the hex closure,
5653        // for a real tip rather than for an empty request.
5654        let tip = store
5655            .graph_snapshot()
5656            .into_iter()
5657            .max_by_key(|c| c.generation)
5658            .expect("a graph");
5659        let tip_raw = hex::decode(&tip.oid).unwrap();
5660        let flat = store.reachable_raw(&[&tip_raw], &[]).unwrap();
5661        let hexed = store.reachable_oids(&[&tip_raw], &[]).unwrap();
5662        assert!(
5663            flat.len() > 1,
5664            "the tip's closure is {} object(s) — too small to tell an ordering bug from a \
5665             coincidence",
5666            flat.len()
5667        );
5668        assert_eq!(
5669            flat.iter().map(hex::encode).collect::<Vec<_>>(),
5670            hexed,
5671            "the serving answer and the maintenance answer name different objects"
5672        );
5673        assert!(flat.contains(&tip_raw), "a closure without its own tip");
5674        eprintln!(
5675            "load {}; {} ordinals agree in both spellings, tip closure {} objects",
5676            loadavg(),
5677            checked,
5678            flat.len()
5679        );
5680    }
5681
5682    /// Distinct commits in a resolved pack — what the graph must hold exactly
5683    /// once.
5684    fn commits_in(rows: &[crate::resolve::Resolved]) -> usize {
5685        rows.iter()
5686            .filter(|r| r.kind == GitObjectKind::Commit)
5687            .map(|r| r.oid.clone())
5688            .collect::<HashSet<Vec<u8>>>()
5689            .len()
5690    }
5691
5692    /// **A push ends in object rows, and no read asked for them.**
5693    ///
5694    /// This is the seam this file wires: before it, `put` produced a *pack* row
5695    /// on the account indexer's channel and the object rows appeared only when
5696    /// somebody read (or called `absorb_pending` by hand). Here nothing is read
5697    /// between `put_pack` and `wait_indexed` — the only thing that could have
5698    /// built the rows is the drain.
5699    ///
5700    /// Asserted on applied output: one row per pack entry, every oid resolving
5701    /// out of the index, and the pack's indexed bit up. `absorb_failures` is
5702    /// asserted too, because a drain that absorbed nothing and recorded the
5703    /// failure would otherwise look like a drain that was never called.
5704    ///
5705    /// Seen RED by making the drain skip the absorber
5706    /// (`match absorber.as_deref()` → `match None::<&dyn ObjectAbsorb>` in
5707    /// `indexer::index_worker`): "the drain built no object rows: 0 of 2687".
5708    /// Restored.
5709    ///
5710    /// MEASURED on oden, 2026-08-08, release, three runs at **1-minute loadavg
5711    /// 5.77** (another tenant's work — oden is shared, so this is not an idle
5712    /// box): 2687 objects out of a 5 653 302-byte real pack absorbed in
5713    /// 161–164 ms, **16 421–16 673 object rows/s**, behind an ack of 29.1–29.4 ms.
5714    /// An earlier set at loadavg 16.94 gave 15 528 rows/s, so the figure moves
5715    /// about 7% with the box.
5716    ///
5717    /// Read it as a *floor* rather than as the engine's speed: the drain here is
5718    /// one pack, so it never uses more than one absorb, and the 162 ms covers a
5719    /// full inflate-and-delta-apply of every entry, a SHA-1 per object, the redb
5720    /// append and a whole-tail re-fold. Nothing about it has been optimised and
5721    /// no arm of it has been isolated. The rate is printed with the loadavg of
5722    /// its own run so a later number is never compared against a different
5723    /// machine state by accident.
5724    #[test]
5725    fn a_push_ends_in_object_rows_and_no_read_asked_for_them() {
5726        let dir = tmpdir("drain-rows");
5727        let store = GitStore::open(&dir, "alice").unwrap();
5728        let (pack, rows) = corpus_pack();
5729
5730        let t = Instant::now();
5731        let tx = store.put_pack(&pack).unwrap();
5732        let ack = t.elapsed();
5733        let pack_id = tx.pack_id.expect("a pack push assigns an id");
5734
5735        // Nothing is read here. The drain is the only thing that can move.
5736        store.wait_indexed();
5737        let drained = t.elapsed();
5738
5739        assert_eq!(
5740            store.object_count(),
5741            rows.len(),
5742            "the drain built no object rows: {} of {}",
5743            store.object_count(),
5744            rows.len()
5745        );
5746        assert_eq!(store.unindexed_packs(), 0, "the bit is still clear");
5747        assert!(
5748            store.indexer().is_indexed(pack_id),
5749            "the pack's indexed bit is not up after the drain"
5750        );
5751        assert_eq!(
5752            store.indexer().absorb_failures(),
5753            0,
5754            "the drain recorded an absorb failure: {:?}",
5755            store.indexer().last_absorb_error()
5756        );
5757
5758        // Every oid answers **out of the rows**, not out of a fallback: the
5759        // index is asked directly, so nothing can absorb behind this loop.
5760        for (i, r) in rows.iter().enumerate() {
5761            let row = store
5762                .index()
5763                .lookup(&r.oid)
5764                .unwrap_or_else(|| panic!("object {i} {} has no row", hex::encode(&r.oid)));
5765            assert_eq!(
5766                (row.offset, row.len),
5767                (tx.extent.unwrap().0 + r.offset, r.len)
5768            );
5769            assert_eq!(row.uncompressed_size, r.uncompressed_size);
5770        }
5771        assert_eq!(
5772            store.absorb_pending().unwrap(),
5773            0,
5774            "the drain left work for a read to do"
5775        );
5776        assert_eq!(
5777            store.commit_count(),
5778            commits_in(&rows),
5779            "the commit graph does not match the pack"
5780        );
5781
5782        let secs = (drained - ack).as_secs_f64();
5783        eprintln!(
5784            "load {}; {} objects in {} bytes: ack {:.0} µs, drain {:.1} ms, {:.0} object rows/s",
5785            loadavg(),
5786            rows.len(),
5787            pack.len(),
5788            ack.as_secs_f64() * 1e6,
5789            (drained - ack).as_secs_f64() * 1e3,
5790            rows.len() as f64 / secs.max(1e-9),
5791        );
5792    }
5793
5794    // ── the bit across a restart ─────────────────────────────────────────────
5795
5796    /// Where the child of the kill guard finds its fixture and leaves its marker.
5797    const KILL_DIR: &str = "GUNNAR_KILL_MID_ABSORB_DIR";
5798
5799    /// **The other half of [`a_store_reopened_after_a_kill_mid_absorb_requeues_the_pack`]:
5800    /// the process that dies.**
5801    ///
5802    /// It runs as a child of that test and it **aborts on purpose**, which is why
5803    /// it is `#[ignore]`d: `SIGABRT` from inside a test binary is a failing test
5804    /// run, and the point is for the *parent* to see the signal.
5805    ///
5806    /// The kill lands where it has to land — after `put_pack` has returned, so
5807    /// the bytes and the journal row are on the platter, and before any row is in
5808    /// the index, which the held absorb gate makes a **state** rather than a
5809    /// hope. Nothing is simulated: no injected error, no fault flag, no early
5810    /// return. The process is gone.
5811    #[test]
5812    #[ignore = "spawned by a_store_reopened_after_a_kill_mid_absorb_requeues_the_pack; it aborts on purpose"]
5813    fn the_child_that_dies_between_durable_bytes_and_indexed_rows() {
5814        let Ok(dir) = std::env::var(KILL_DIR) else {
5815            // Run by hand with `--ignored` and no fixture: there is nothing to
5816            // kill over, so do nothing rather than abort somebody's test run.
5817            return;
5818        };
5819        let dir = PathBuf::from(dir);
5820        let pack = std::fs::read(dir.join("fixture.pack")).expect("the parent's fixture");
5821
5822        let store = GitStore::open(&dir, "alice").expect("open");
5823        // The drain is parked in front of its absorb for as long as this lives,
5824        // so no row can land before the abort below.
5825        let _gate = store.hold_absorb_gate();
5826        let tx = store.put_pack(&pack).expect("the ack path");
5827        assert_eq!(tx.pack_id, Some(0), "a fresh archive starts at ordinal 0");
5828        assert_eq!(store.unindexed_packs(), 1, "the pack is not queued");
5829        assert_eq!(store.index().len(), 0, "a row landed before the kill");
5830        std::fs::write(dir.join("ready"), b"durable, not indexed").expect("marker");
5831
5832        // Not `panic!`, not `exit`: the process stops here with a live gate, a
5833        // live worker and a live redb handle, which is what a machine that dies
5834        // mid-absorb leaves behind.
5835        std::process::abort();
5836    }
5837
5838    /// **A store reopened over a pack that was still being absorbed re-queues
5839    /// it, answers correctly the whole way through, and gets its derived tables
5840    /// back.**
5841    ///
5842    /// The interruption is a **real** one: a child process is spawned, pushes the
5843    /// pack, and is killed by `SIGABRT` between "bytes durable" and "rows
5844    /// indexed". The parent then asserts what the kill left on disk — one journal
5845    /// row, the pack verbatim, **zero** index rows — before opening a store over
5846    /// it, so the recovery is being tested against the state it claims to
5847    /// recover from and not against a lucky one.
5848    ///
5849    /// Three things are then asserted, all on applied output:
5850    ///
5851    /// 1. **the pack is re-queued** — the account indexer of the *new* process
5852    ///    publishes ordinal 0, which it can only do for a job somebody submitted;
5853    /// 2. **a read is correct throughout** — `has` is asked the instant the store
5854    ///    is open, before any drain has been waited for, and must answer `true`;
5855    /// 3. **`commit_count()` and `reachable()` come back** — the derived tables
5856    ///    are rebuilt by the same absorb, so the graph holds exactly the pack's
5857    ///    commits and a tip's closure is non-empty.
5858    ///
5859    /// Seen RED by making `open_with` re-queue nothing (`absorber
5860    /// .adopt_journal(&acked)?` → an empty `Vec`, which is what this store did
5861    /// before this change): "a durable object read `absent` from a store reopened
5862    /// over the pack that holds it — the crash-recovery bit did not re-queue
5863    /// anything". Restored.
5864    ///
5865    /// Seen RED a second time by inverting the diff in
5866    /// [`Absorber::adopt_journal`] (`if absorbed` → `if !absorbed`), so a pack
5867    /// with **no** rows has its bit set and a pack with rows is re-queued: the
5868    /// same first assertion fires, because a set bit over an empty index is
5869    /// exactly the wrong answer a bloom filter's false positive would have
5870    /// produced — the read stops falling back and says `absent` about an object
5871    /// whose bytes are durable. Restored.
5872    ///
5873    /// **Why the assertions are not immediately after the reopen.** The trap the
5874    /// drain guards already record applies here too, mirrored: right after
5875    /// `GitStore::open` the drain may legitimately have finished the re-queued
5876    /// pack already, so `unindexed_packs() == 1` is not assertable and is not
5877    /// asserted. What *is* assertable at that instant is the answer to a read,
5878    /// which must be `true` whichever side of the drain it lands on — and that is
5879    /// exactly the property §13.12 promises.
5880    ///
5881    /// **And it still holds now that a `gc` can retire a pack's journal row.**
5882    /// The tombstone tells "indexed then garbage-collected" from "never
5883    /// indexed", and this pack is the second: nothing retired it, so it must
5884    /// still be re-queued. Seen RED by making `adopt_journal` treat *every* pack
5885    /// as retired (`retired.contains(&extent.0)` → `true`): "a durable object
5886    /// read `absent` from a store reopened over the pack that holds it — the
5887    /// crash-recovery bit did not re-queue anything". Restored.
5888    #[test]
5889    fn a_store_reopened_after_a_kill_mid_absorb_requeues_the_pack() {
5890        use std::os::unix::process::ExitStatusExt;
5891
5892        let dir = tmpdir("kill-mid-absorb");
5893        let (pack, rows) = corpus_pack();
5894        std::fs::write(dir.join("fixture.pack"), &pack).unwrap();
5895
5896        let status = std::process::Command::new(std::env::current_exe().unwrap())
5897            .args([
5898                "--exact",
5899                "--ignored",
5900                "--nocapture",
5901                "git_ops::tests::the_child_that_dies_between_durable_bytes_and_indexed_rows",
5902            ])
5903            .env(KILL_DIR, &dir)
5904            .status()
5905            .expect("spawn the child that dies");
5906        assert_eq!(
5907            status.signal(),
5908            Some(6),
5909            "the child did not die by SIGABRT — it {status:?}, so nothing was interrupted"
5910        );
5911        assert!(
5912            dir.join("ready").exists(),
5913            "the child never reached the kill point"
5914        );
5915
5916        // ── what the kill left on disk ───────────────────────────────────────
5917        let blobs = dir.join("objects.pack");
5918        let acked = read_journal(&SafeWriter::journal_path(&blobs)).unwrap();
5919        assert_eq!(
5920            acked.len(),
5921            1,
5922            "the pack's extent is not durable: {acked:?}"
5923        );
5924        assert_eq!(acked[0].1, pack.len() as u64);
5925        let on_disk = std::fs::read(&blobs).unwrap();
5926        assert_eq!(
5927            &on_disk[acked[0].0 as usize..(acked[0].0 + acked[0].1) as usize],
5928            &pack[..],
5929            "the verbatim bytes did not survive the kill"
5930        );
5931        {
5932            // Opened and dropped before the store takes redb's file lock.
5933            let tail = ObjectReadStack::<OneTableFourColumns>::open(
5934                &dir.join("objects.tail"),
5935                RebuildTriggers::default(),
5936                crate::arms::DEFAULT_REDB_CACHE_BYTES,
5937            )
5938            .unwrap();
5939            assert_eq!(
5940                tail.len(),
5941                0,
5942                "the kill landed after the rows were indexed, not between the bytes and the rows \
5943                 — this guard would then be testing nothing"
5944            );
5945        }
5946
5947        // ── the reopen ───────────────────────────────────────────────────────
5948        let store = GitStore::open(&dir, "alice").unwrap();
5949        assert!(
5950            store.has(&rows[0].oid).unwrap(),
5951            "a durable object read `absent` from a store reopened over the pack that holds it — \
5952             the crash-recovery bit did not re-queue anything"
5953        );
5954
5955        store.wait_indexed();
5956        assert_eq!(
5957            store.object_count(),
5958            rows.len(),
5959            "the re-queued pack's objects never reached the index — left: {}, right: {}",
5960            store.object_count(),
5961            rows.len()
5962        );
5963        assert!(
5964            store.indexer().is_indexed(0),
5965            "the re-queued pack's indexed bit never went up in the new process"
5966        );
5967        assert_eq!(store.unindexed_packs(), 0);
5968        assert_eq!(store.absorb_pending().unwrap(), 0);
5969        for r in rows.iter().take(512) {
5970            let row = store
5971                .index()
5972                .lookup(&r.oid)
5973                .unwrap_or_else(|| panic!("{} has no row after recovery", hex::encode(&r.oid)));
5974            assert_eq!((row.offset, row.len), (acked[0].0 + r.offset, r.len));
5975        }
5976
5977        // ── the derived tables, which were the second symptom ────────────────
5978        assert_eq!(
5979            store.commit_count(),
5980            commits_in(&rows),
5981            "the commit graph did not come back: {} of {}",
5982            store.commit_count(),
5983            commits_in(&rows)
5984        );
5985        let tip = store
5986            .graph_snapshot()
5987            .into_iter()
5988            .max_by_key(|c| c.generation)
5989            .expect("a graph");
5990        let tip_raw = hex::decode(&tip.oid).unwrap();
5991        let closure = store.reachable(&[&tip_raw], &[]).unwrap();
5992        assert!(
5993            !closure.is_empty(),
5994            "reachable() came back empty after the reopen"
5995        );
5996        assert!(closure.contains(&tip_raw));
5997        eprintln!(
5998            "load {}; killed mid-absorb: {} journal extent(s) recovered {} object rows, {} \
5999             commits, closure of the tip {} objects",
6000            loadavg(),
6001            acked.len(),
6002            store.object_count(),
6003            store.commit_count(),
6004            closure.len(),
6005        );
6006    }
6007
6008    /// **A clean reopen re-queues nothing, and the next push gets a fresh
6009    /// ordinal.**
6010    ///
6011    /// The other side of the diff. A store whose pack was fully absorbed before
6012    /// it closed must come back with the bit **set**: re-absorbing would be a
6013    /// whole pack re-resolved for nothing, and — because the commit graph is a
6014    /// `Vec` — it is the same doubling the drain's own guard exists to prevent.
6015    ///
6016    /// "Re-queued nothing" is asserted deterministically rather than by a timing
6017    /// window: the account indexer in this process publishes a pack only for a
6018    /// job somebody submitted, so `is_indexed(0)` being **false** after
6019    /// `wait_indexed` is proof that open submitted none.
6020    ///
6021    /// The second half is the ordinal. Pack ids are the journal's row numbers, so
6022    /// a reopened store must resume after them. Asserted on applied output rather
6023    /// than on the id alone: the newly pushed pack's object has to be findable,
6024    /// and it is exactly what goes missing when a fresh pack is handed an
6025    /// absorbed pack's ordinal — `queue` skips it, the drain skips it, and its
6026    /// durable bytes are never indexed by anybody.
6027    ///
6028    /// Seen RED by making `Absorber::adopt_journal` treat every journal extent as
6029    /// unabsorbed (`let has_rows = vec![false; journal.len()]`): "a fully
6030    /// absorbed pack was re-queued on open — the diff is not exact".
6031    ///
6032    /// Seen RED a second time by `AtomicU64::new(packs_already_acked(...)?)` →
6033    /// `AtomicU64::new(0)` in `PushPath::with_absorber`: "a reopened store handed
6034    /// a fresh pack the ordinal of an absorbed one — left: Some(0), right:
6035    /// Some(1)". With that same edit **and the id assertion deleted**, the
6036    /// applied-output one goes too — "the second push's object never reached the
6037    /// index — its ordinal collided with an absorbed pack's, so every writer
6038    /// skipped it" — which is what says this guard is about the objects and not
6039    /// about a counter. Restored.
6040    #[test]
6041    fn a_clean_reopen_requeues_nothing_and_the_next_push_gets_a_fresh_ordinal() {
6042        let dir = tmpdir("clean-reopen");
6043        let (pack, rows) = corpus_pack();
6044        let (second, blob_oid) = one_blob_pack(b"a push that arrives after the restart");
6045
6046        {
6047            let store = GitStore::open(&dir, "alice").unwrap();
6048            store.put_pack(&pack).unwrap();
6049            store.wait_indexed();
6050            assert_eq!(
6051                store.object_count(),
6052                rows.len(),
6053                "the first run did not index"
6054            );
6055        }
6056
6057        let store = GitStore::open(&dir, "alice").unwrap();
6058        store.wait_indexed();
6059        assert!(
6060            !store.indexer().is_indexed(0),
6061            "a fully absorbed pack was re-queued on open — the diff is not exact"
6062        );
6063        assert_eq!(store.unindexed_packs(), 0);
6064        assert_eq!(
6065            store.absorb_pending().unwrap(),
6066            0,
6067            "the reopen left index work"
6068        );
6069        assert_eq!(
6070            store.object_count(),
6071            rows.len(),
6072            "the reopened store lost rows"
6073        );
6074        assert!(store.has(&rows[0].oid).unwrap());
6075
6076        let tx = store.put_pack(&second).unwrap();
6077        assert_eq!(
6078            tx.pack_id,
6079            Some(1),
6080            "a reopened store handed a fresh pack the ordinal of an absorbed one"
6081        );
6082        store.wait_indexed();
6083        assert!(
6084            store.has(&blob_oid).unwrap(),
6085            "the second push's object never reached the index — its ordinal collided with an \
6086             absorbed pack's, so every writer skipped it"
6087        );
6088        assert_eq!(store.object_count(), rows.len() + 1);
6089        assert!(store.indexer().is_indexed(1));
6090        // And the journal is a log: both packs' extents are in it, in order.
6091        let acked = read_journal(&SafeWriter::journal_path(store.blobs_path())).unwrap();
6092        assert_eq!(
6093            acked.len(),
6094            2,
6095            "the reopen truncated the journal: {acked:?}"
6096        );
6097        assert_eq!(acked[1], tx.extent.unwrap());
6098    }
6099
6100    /// **What the derive-on-open diff costs at a realistic pack count.**
6101    ///
6102    /// Not a guard — a measurement, with two assertions on it that keep it from
6103    /// measuring the wrong thing: every absorbed pack must come back `true`, and
6104    /// the one pack that has no rows must come back `false`.
6105    ///
6106    /// Both cases are timed because they are different algorithms in practice.
6107    /// The **clean reopen** — every pack absorbed — terminates as soon as each
6108    /// extent has been hit once, which happens after a few rows per pack because
6109    /// the tail is in oid order and an oid says nothing about which pack its
6110    /// object came from. The **crash** case has one pack with no rows at all, and
6111    /// proving an absence means reading the tail to the end.
6112    ///
6113    /// MEASURED on oden 2026-08-08, release, 1000 real pushed packs / 3686 object
6114    /// rows, 1-minute loadavg 2.77: **0.38 ms** clean and 0.39 ms with one pack
6115    /// un-absorbed (0.40 / 0.39 ms at loadavg 13.06, so the figure barely moves
6116    /// with the box). At this row count the two cases are the same, because a
6117    /// 3686-row tail is scanned to the end faster than the early exit saves
6118    /// anything; they diverge with the object count, and the table on
6119    /// [`ObjectReadStack::extents_with_rows`] carries that at a million rows
6120    /// (0.74 ms against 109 ms).
6121    ///
6122    /// The printed line carries the loadavg of its own run, the pack count and
6123    /// the row count, because none of the three is comparable across machines or
6124    /// across a busy box.
6125    #[test]
6126    fn the_derive_on_open_diff_costs_milliseconds_at_a_realistic_pack_count() {
6127        const PACKS: usize = 1000;
6128        let dir = tmpdir("diff-cost");
6129        let (real, rows) = corpus_pack();
6130
6131        {
6132            let store = GitStore::open(&dir, "alice").unwrap();
6133            store.put_pack(&real).unwrap();
6134            for i in 0..PACKS - 1 {
6135                let (p, _) = one_blob_pack(format!("pack number {i}").as_bytes());
6136                store.put_pack(&p).unwrap();
6137            }
6138            store.wait_indexed();
6139            assert_eq!(
6140                store.object_count(),
6141                rows.len() + PACKS - 1,
6142                "the fixture did not absorb"
6143            );
6144        }
6145
6146        let acked = read_journal(&SafeWriter::journal_path(&dir.join("objects.pack"))).unwrap();
6147        assert_eq!(acked.len(), PACKS, "one journal row per pack");
6148        // The tail on its own, so what is timed is the diff rather than redb's
6149        // open and the projection build that a store's open also pays for.
6150        let tail = ObjectReadStack::<OneTableFourColumns>::open(
6151            &dir.join("objects.tail"),
6152            RebuildTriggers::default(),
6153            crate::arms::DEFAULT_REDB_CACHE_BYTES,
6154        )
6155        .unwrap();
6156
6157        let t = Instant::now();
6158        let hit = tail.extents_with_rows(&acked).unwrap();
6159        let clean = t.elapsed();
6160        assert!(
6161            hit.iter().all(|&h| h),
6162            "an absorbed pack was reported unabsorbed"
6163        );
6164
6165        // One pack in flight when the machine died: durable bytes, no rows.
6166        let mut in_flight = acked.clone();
6167        let end = acked[PACKS - 1].0 + acked[PACKS - 1].1;
6168        in_flight.push((end, 4096));
6169        let t = Instant::now();
6170        let hit = tail.extents_with_rows(&in_flight).unwrap();
6171        let crashed = t.elapsed();
6172        assert_eq!(
6173            hit.iter().filter(|h| !**h).count(),
6174            1,
6175            "the pack with no rows is not the only one reported unabsorbed"
6176        );
6177
6178        eprintln!(
6179            "load {}; {PACKS} packs / {} object rows: derive-on-open diff {:.2} ms clean \
6180             (early exit), {:.2} ms with one pack un-absorbed (full tail scan)",
6181            loadavg(),
6182            tail.len(),
6183            clean.as_secs_f64() * 1e3,
6184            crashed.as_secs_f64() * 1e3,
6185        );
6186    }
6187
6188    /// **The bit is a bitset over dense ordinals, and one bit is one pack.**
6189    ///
6190    /// Ordinals that straddle word boundaries (63, 64, 65) are the whole point:
6191    /// a shift that ignores the word split, or a word index that does not, sets
6192    /// a neighbour's bit — and a neighbour's bit set means a durable pack is
6193    /// declared absorbed with no rows behind it.
6194    ///
6195    /// Seen RED by `>> (i % 64) & 1` → `>> (i % 63) & 1` in
6196    /// `PackState::is_absorbed`: "the bit for ordinal 64 did not go up". Restored.
6197    ///
6198    /// Seen RED a second time by `if self.extent[i].is_none()` → `if
6199    /// self.extent[i].is_none() || true` in `PackState::note`, i.e. letting a
6200    /// re-queue of a pack that is already recorded count as new work — which is
6201    /// the race the ack path really produces, since the drain can absorb before
6202    /// `queue` runs: "an absorbed pack was queued again — left: 6, right: 5".
6203    /// Restored.
6204    #[test]
6205    fn the_indexed_bit_is_a_bitset_over_dense_pack_ordinals() {
6206        let ids = [0u64, 1, 63, 64, 65, 4095];
6207        let mut s = PackState::default();
6208        for id in ids {
6209            s.note(id, (id * 4096 + 12, 4096));
6210        }
6211        assert_eq!(s.unabsorbed, ids.len());
6212        assert!(
6213            ids.iter().all(|&i| !s.is_absorbed(i)),
6214            "a bit is up already"
6215        );
6216
6217        s.mark_absorbed(64, (64 * 4096 + 12, 4096));
6218        assert!(s.is_absorbed(64), "the bit for ordinal 64 did not go up");
6219        for other in [0u64, 1, 63, 65, 4095] {
6220            assert!(
6221                !s.is_absorbed(other),
6222                "setting ordinal 64 also set ordinal {other} — one bit spilled onto a neighbour"
6223            );
6224        }
6225        assert_eq!(s.unabsorbed, ids.len() - 1);
6226        assert_eq!(
6227            s.pending().iter().map(|p| p.pack_id).collect::<Vec<_>>(),
6228            vec![0, 1, 63, 65, 4095],
6229            "the pending list is not the clear bits"
6230        );
6231        assert_eq!(
6232            s.pending()[0],
6233            PendingPack {
6234                pack_id: 0,
6235                offset: 12,
6236                len: 4096,
6237                resumed: true,
6238            },
6239            "a pending pack lost the extent it must be absorbed from"
6240        );
6241
6242        // Both writers are idempotent, because both of them race.
6243        s.mark_absorbed(64, (64 * 4096 + 12, 4096));
6244        s.note(64, (64 * 4096 + 12, 4096));
6245        assert!(s.is_absorbed(64));
6246        assert_eq!(
6247            s.unabsorbed,
6248            ids.len() - 1,
6249            "an absorbed pack was queued again"
6250        );
6251
6252        // One bit per pack, and that is the whole cost: 4096 ordinals is 64
6253        // words. A hash set of the same ordinals is an order of magnitude more,
6254        // and a bloom would be smaller and wrong.
6255        assert_eq!(s.words.len(), 64);
6256        assert_eq!(s.words.len() * 8, 512, "4096 packs is 512 bytes of bits");
6257    }
6258
6259    /// **A read that arrives before the drain is served from the pack, on its
6260    /// own thread, and never waits for the worker.**
6261    ///
6262    /// The owner's rule, 2026-08-30: *nobody waits on the indexer.* Until then
6263    /// this test asserted the opposite — that a read parked behind the absorb
6264    /// gate stayed unanswered until the drain released it, which on a kernel
6265    /// pack is hours, and under the memory watchdog is *as long as the box is
6266    /// short*. Now the gate is the absorbers' and only the absorbers': the read
6267    /// finds no row, sees a pending pack, and resolves that pack read-only
6268    /// from the mmapped bytes for the one oid it wants
6269    /// (`Absorber::scan_pending`). The drain is still parked, the index is
6270    /// still empty, and the answer is **present** — from the verbatim truth.
6271    ///
6272    /// Seen RED by making `lookup_never_wait` return `Ok(None)` when the index
6273    /// misses: "a read with the drain parked answered absent for a durable
6274    /// object". And by putting `self.gate.lock()` back in front of it: the
6275    /// 2-second bound below trips with the reader still parked.
6276    #[test]
6277    fn a_read_before_the_drain_is_served_from_the_pack_and_never_waits() {
6278        let dir = tmpdir("drain-before");
6279        let store = Arc::new(GitStore::open(&dir, "alice").unwrap());
6280        let (pack, rows) = corpus_pack();
6281        let (second, blob_oid) = one_blob_pack(b"a second push while the drain is parked");
6282
6283        // The drain is parked here, before it can absorb anything.
6284        let gate = store.hold_absorb_gate();
6285        let tx = store.put_pack(&pack).unwrap();
6286        let pack_id = tx.pack_id.unwrap();
6287        assert_eq!(store.unindexed_packs(), 1, "the pack is not queued");
6288        assert_eq!(store.index().len(), 0, "the index already holds rows");
6289        assert!(!store.indexer().is_indexed(pack_id), "the bit is already up");
6290
6291        // (1) a second push does not wait either: the ack path never meets the gate.
6292        let t = Instant::now();
6293        let tx2 = store.put_pack(&second).unwrap();
6294        assert_eq!(tx2.pack_id, Some(pack_id + 1));
6295        assert!(t.elapsed() < Duration::from_secs(2), "the push waited on the parked drain");
6296
6297        // (2) reads into exactly that state: present, from the pack, bounded.
6298        let oid = rows[0].oid.clone();
6299        let last = rows[rows.len() - 1].oid.clone();
6300        let t = Instant::now();
6301        assert!(store.has(&oid).unwrap(), "a read with the drain parked answered absent for a durable object");
6302        assert!(store.has(&last).unwrap(), "the last object of the pack answered absent");
6303        assert!(store.has(&blob_oid).unwrap(), "the second (still pending) pack's object answered absent");
6304        let (kind, body) = store.content(&blob_oid).unwrap().expect("content from the pending pack");
6305        assert_eq!(kind, GitObjectKind::Blob);
6306        assert_eq!(body, b"a second push while the drain is parked");
6307        let stored = store.get(&oid).unwrap().expect("get from the pending pack");
6308        assert_eq!(stored.extent.0, rows[0].offset + tx.extent.unwrap().0);
6309        assert_eq!(
6310            store.size(&last).unwrap(),
6311            Some(rows[rows.len() - 1].uncompressed_size),
6312            "size from the pending pack"
6313        );
6314        let absent = vec![0xEEu8; 20];
6315        assert!(!store.has(&absent).unwrap(), "an oid in no pack must still be absent");
6316        let elapsed = t.elapsed();
6317        assert!(elapsed < Duration::from_secs(20), "reads took {elapsed:?} with the drain parked");
6318        // The drain is STILL parked and the index STILL empty: nothing above
6319        // absorbed anything, and nothing waited.
6320        assert_eq!(store.index().len(), 0, "a read absorbed the pack behind the parked gate");
6321        assert!(!store.indexer().is_indexed(pack_id));
6322        assert_eq!(store.unindexed_packs(), 2);
6323        eprintln!("{} reads served from two pending packs with the drain parked, in {elapsed:?}", 6);
6324
6325        // (3) a whole-graph request is refused by name, not waited on.
6326        let err = store.reachable(&[oid.as_slice()], &[]).unwrap_err();
6327        assert!(err.to_string().contains("index in progress"), "{err:#}");
6328
6329        drop(gate);
6330        // And afterwards it is served from the rows.
6331        store.wait_indexed();
6332        assert_eq!(store.unindexed_packs(), 0);
6333        assert!(store.indexer().is_indexed(pack_id));
6334        assert_eq!(store.index().len(), rows.len() + 1);
6335        assert_eq!(store.absorb_pending().unwrap(), 0);
6336        for r in rows.iter().take(256) {
6337            assert!(
6338                store.index().lookup(&r.oid).is_some(),
6339                "{} is not in the rows after the drain",
6340                hex::encode(&r.oid)
6341            );
6342        }
6343        assert!(store.reachable(&[oid.as_slice()], &[]).is_ok(), "reachable after the drain");
6344    }
6345
6346    /// **The memory watchdog pauses the indexer worker and nobody else.**
6347    ///
6348    /// A fake `/proc/meminfo` under the floor is put under the process-wide
6349    /// watchdog; the worker parks in front of its batch (`index_worker`'s
6350    /// pre-batch check) and stays parked. Meanwhile a push is acked, every
6351    /// kind of read answers from the pack, and the progress snapshot returns —
6352    /// all bounded, none of them behind the pause. Then the fake is raised,
6353    /// the worker resumes, and the pack is indexed normally.
6354    ///
6355    /// Seen RED with the pre-batch check taking the absorb gate before pausing
6356    /// (the reads' scan does not need the gate, but `absorb_pending`-based
6357    /// reads did): every read parked for the length of the pause.
6358    #[test]
6359    fn a_paused_indexer_blocks_no_push_and_no_read() {
6360        let dir = tmpdir("paused-indexer");
6361        let meminfo = dir.join("meminfo");
6362        let write = |avail_kib: u64| {
6363            std::fs::write(&meminfo, format!("MemTotal:       16000000 kB\nMemAvailable:   {avail_kib} kB\n"))
6364                .unwrap()
6365        };
6366        write(1_000_000);
6367        let dog = Arc::new(crate::memguard::MemWatchdog::with(
6368            &meminfo,
6369            3_200_000 * 1024,
6370            Duration::from_millis(10),
6371        ));
6372        let store = Arc::new(GitStore::open(&dir, "alice").unwrap());
6373        // This store's worker only: every other store in the process keeps
6374        // the real watchdog.
6375        store.set_mem_watchdog(dog.clone());
6376        let (pack, rows) = corpus_pack();
6377        let (second, blob_oid) = one_blob_pack(b"pushed while the indexer is paused");
6378
6379        let t_push = Instant::now();
6380        let tx = store.put_pack(&pack).unwrap();
6381        let pack_id = tx.pack_id.unwrap();
6382        // Give the worker time to reach its pre-batch check and park.
6383        let mut parked = false;
6384        for _ in 0..200 {
6385            if dog.is_paused() {
6386                parked = true;
6387                break;
6388            }
6389            std::thread::sleep(Duration::from_millis(10));
6390        }
6391        assert!(parked, "the worker never paused under a meminfo 1 GB < 3.2 GB");
6392        assert!(!store.indexer().is_indexed(pack_id), "indexed while paused");
6393        assert_eq!(store.index().len(), 0);
6394
6395        // The ack path: a push while paused.
6396        let tx2 = store.put_pack(&second).unwrap();
6397        assert_eq!(tx2.pack_id, Some(pack_id + 1));
6398        assert!(t_push.elapsed() < Duration::from_secs(5), "a push waited on the paused worker");
6399
6400        // Reads while paused: present, from the pack.
6401        let t = Instant::now();
6402        assert!(store.has(&rows[0].oid).unwrap());
6403        assert!(store.has(&blob_oid).unwrap());
6404        let (kind, body) = store.content(&blob_oid).unwrap().unwrap();
6405        assert_eq!((kind, body.as_slice()), (GitObjectKind::Blob, &b"pushed while the indexer is paused"[..]));
6406        assert_eq!(
6407            store.extents(&[rows[0].oid.as_slice(), rows[1].oid.as_slice(), blob_oid.as_slice()])
6408                .unwrap()
6409                .iter()
6410                .filter(|e| e.is_some())
6411                .count(),
6412            3,
6413            "the batch path answered absent while paused"
6414        );
6415        assert!(t.elapsed() < Duration::from_secs(20), "reads waited on the paused worker");
6416        // The progress row: information, never a gate.
6417        let snap = store.index_progress();
6418        assert_eq!(snap.root, store.root().display().to_string());
6419        assert!(dog.is_paused(), "the worker resumed on its own — the fake still says 1 GB");
6420        assert!(!store.indexer().is_indexed(pack_id), "indexed while still paused");
6421
6422        // A whole-graph request while the worker is paused: the gate is free
6423        // (a paused worker holds nothing), so the READER absorbs the pending
6424        // packs on its own thread and answers — the worker is still parked.
6425        let reach = store.reachable(&[rows[0].oid.as_slice()], &[]).unwrap();
6426        assert!(!reach.is_empty());
6427        assert_eq!(store.unindexed_packs(), 0, "the reader did not absorb for itself");
6428        assert!(dog.is_paused(), "the worker is still parked; the reader did the work");
6429        assert_eq!(store.index().len(), rows.len() + 1);
6430
6431        // Memory comes back: the worker resumes, finds nothing to do, and the
6432        // bits go up through the ordinary drain.
6433        write(9_000_000);
6434        store.wait_indexed();
6435        assert!(store.indexer().is_indexed(pack_id));
6436        assert!(store.indexer().is_indexed(pack_id + 1));
6437        assert_eq!(dog.pauses(), 1, "one pause, one resume");
6438        assert!(!dog.is_paused());
6439    }
6440
6441    /// **The bit is cleared after the rows go in, never before.**
6442    ///
6443    /// The ordering inside `absorb_gated` is the whole reason a read cannot be
6444    /// told "absent" while the drain is running, and the only way to see it is to
6445    /// read *during* one. Four gatling workers (LAW 3 — never rayon, and never a
6446    /// raw thread pool) hammer `has` over the pack's oids from the moment
6447    /// `put_pack` returns until the pack's bit is up.
6448    ///
6449    /// Asserted on applied output: not one of those reads may answer absent for
6450    /// an object whose bytes are already durable.
6451    ///
6452    /// Seen RED by clearing the bit *before* absorbing (a
6453    /// `s.pending.retain(|p| p.pack_id != job.pack_id)` added in `absorb_gated`
6454    /// above the `self.absorb_one(job)` call): "4 reader(s) were told a durable
6455    /// object was absent — **32 865 593** absent answers over 2687 objects".
6456    /// Restored. The count is that large because the readers spin until the bit
6457    /// goes up and, with the bit cleared first, every pass over the sample missed
6458    /// for the whole length of the absorb.
6459    ///
6460    /// The honest limit: this is a race, so a green run proves the window was not
6461    /// hit, not that no window exists. That is why the printed line carries how
6462    /// many of the reads actually landed while the pack was still un-indexed —
6463    /// **a run where that number is 0 proved nothing**, and it is reported rather
6464    /// than asserted because forcing it would mean pausing the drain, which is
6465    /// the one thing this test must not do. MEASURED 2026-08-08 on oden at
6466    /// loadavg 16.94: 336 of 336 reads landed early.
6467    #[test]
6468    fn no_read_is_told_absent_while_the_drain_is_running() {
6469        let dir = tmpdir("drain-during");
6470        let store = GitStore::open(&dir, "alice").unwrap();
6471        let (pack, rows) = corpus_pack();
6472        let oids: Vec<&[u8]> = rows.iter().map(|r| r.oid.as_slice()).collect();
6473
6474        let tx = store.put_pack(&pack).unwrap();
6475        let pack_id = tx.pack_id.unwrap();
6476
6477        let early = AtomicU64::new(0);
6478        let absent = AtomicU64::new(0);
6479        let reads = AtomicU64::new(0);
6480        let workers = 4usize;
6481        gatling_for_each(workers, workers, |w| loop {
6482            let still_pending = store.unindexed_packs() > 0;
6483            for oid in oids.iter().skip(w).step_by(workers * 8) {
6484                if still_pending {
6485                    early.fetch_add(1, AtomicOrdering::Relaxed);
6486                }
6487                reads.fetch_add(1, AtomicOrdering::Relaxed);
6488                if !store.has(oid).unwrap() {
6489                    absent.fetch_add(1, AtomicOrdering::Relaxed);
6490                }
6491            }
6492            if store.indexer().is_indexed(pack_id) {
6493                break;
6494            }
6495        });
6496
6497        let absent = absent.load(AtomicOrdering::Acquire);
6498        assert_eq!(
6499            absent,
6500            0,
6501            "{workers} reader(s) were told a durable object was absent — {absent} absent answers \
6502             over {} objects",
6503            rows.len()
6504        );
6505        assert_eq!(store.object_count(), rows.len());
6506        eprintln!(
6507            "load {}; {} reads across {workers} gatling workers, {} of them while the pack was \
6508             still un-indexed",
6509            loadavg(),
6510            reads.load(AtomicOrdering::Acquire),
6511            early.load(AtomicOrdering::Acquire),
6512        );
6513    }
6514
6515    /// **One pack is absorbed once, whichever of the two callers gets there
6516    /// first.**
6517    ///
6518    /// Both callers are made to attempt it, deterministically: with the gate
6519    /// held, the drain is parked inside `ObjectAbsorb::absorb` and a reader is
6520    /// parked inside `absorb_pending`, both past their own early exits. Releasing
6521    /// the gate lets exactly one do the work; the other must take the skip.
6522    ///
6523    /// The observable is the **commit graph**, not the object table:
6524    /// `ObjectReadStack::append` is keyed by oid and would swallow a second
6525    /// identical row, but the graph is a `Vec` and a second absorb pushes every
6526    /// commit into it again — which silently doubles the fold every reachability
6527    /// bitmap is built from.
6528    ///
6529    /// Seen RED by disabling the `s.absorbed.contains(&job.pack_id)` skip in
6530    /// `absorb_gated`: "the commit graph carries **1102** commits, the pack has
6531    /// **551**" — exactly double, both callers having done the whole fold.
6532    /// Restored.
6533    #[test]
6534    fn a_pack_is_absorbed_once_even_when_both_callers_race_for_it() {
6535        let dir = tmpdir("drain-once");
6536        let store = Arc::new(GitStore::open(&dir, "alice").unwrap());
6537        let (pack, rows) = corpus_pack();
6538
6539        let gate = store.hold_absorb_gate();
6540        store.put_pack(&pack).unwrap();
6541
6542        let s = store.clone();
6543        let reader = Job::spawn(move || s.absorb_pending().unwrap());
6544        // Both are now blocked on the gate: the drain inside `absorb`, the reader
6545        // inside `absorb_pending`, each having already decided there is work.
6546        std::thread::sleep(Duration::from_millis(100));
6547        drop(gate);
6548
6549        let by_read = reader.join().unwrap();
6550        store.wait_indexed();
6551
6552        assert_eq!(
6553            store.commit_count(),
6554            commits_in(&rows),
6555            "the commit graph carries {} commits, the pack has {}",
6556            store.commit_count(),
6557            commits_in(&rows)
6558        );
6559        assert_eq!(store.object_count(), rows.len());
6560        assert_eq!(store.unindexed_packs(), 0);
6561        eprintln!(
6562            "load {}; the read absorbed {by_read} pack(s), the drain absorbed the rest",
6563            loadavg()
6564        );
6565    }
6566
6567    // ── §14's exploded table ─────────────────────────────────────────────────
6568
6569    /// **A push produces an exploded row for every object in the pack, and each
6570    /// one hashes back to its own oid.**
6571    ///
6572    /// Eager, decided 2026-08-08: no threshold, no size cap, no type skipped —
6573    /// so the row count is the *object* count and not some subset of it. Nothing
6574    /// is read between `put_pack` and `wait_indexed`, so the only thing that
6575    /// could have built the rows is the background drain, which is the half of
6576    /// §14 that says "built by the same background indexer".
6577    ///
6578    /// The assertion is applied output in its strongest available form: every
6579    /// object's content is read back out of the table, re-serialised canonically
6580    /// and **re-hashed**, and the hash has to be the oid it was filed under. A
6581    /// row count alone could be satisfied by 2687 rows of the wrong bytes; a
6582    /// re-hash cannot.
6583    ///
6584    /// Seen RED by deleting the `sink.explode(&oid, kind, &payload)?` call in
6585    /// `resolve::resolve_walked`: "the drain built no exploded rows: 0 of 2687".
6586    /// Restored.
6587    ///
6588    /// Seen RED a second time by making the sink skip blobs in
6589    /// `ExplodedTable::explode` (`if kind == GitObjectKind::Blob { return Ok(()) }`
6590    /// at the top — which is what a *lazy* or size-capped table would look like
6591    /// from here): "the drain built no exploded rows: 1420 of 2687". Restored,
6592    /// because §14's open question was closed EAGER and disk is explicitly not a
6593    /// consideration — and because the 1267 rows that edit removes are exactly
6594    /// the blobs a content read wants most.
6595    #[test]
6596    fn a_push_produces_an_exploded_row_for_every_object_in_the_pack() {
6597        let dir = tmpdir("exploded-every-object");
6598        let store = GitStore::open(&dir, "alice").unwrap();
6599        let (pack, rows) = corpus_pack();
6600
6601        let t = Instant::now();
6602        store.put_pack(&pack).unwrap();
6603        let ack = t.elapsed();
6604        store.wait_indexed();
6605        // **Drain, printed beside ack**, because the two answer different
6606        // questions and only one of them was ever in doubt. Ack is unchanged by
6607        // construction — nothing on it touches this table. Drain is where the
6608        // eager resolve and the sink both live, and it is the column redb's
6609        // 2026-08-08 measurement recorded as 316.2–329.8 ms against 166.7–168.6
6610        // ms with the sink stubbed out. Without it here, "did the medium buy
6611        // anything but disk" has no answer in this repository.
6612        let drain = t.elapsed();
6613
6614        let distinct: HashSet<Vec<u8>> = rows.iter().map(|r| r.oid.clone()).collect();
6615        let stats = store.exploded_stats();
6616        // **The push explodes the GRAPH, not the content.** Under the shipped
6617        // policy the drain writes commits and trees and leaves blobs in the
6618        // pack; the assertion is therefore two-sided, because "some rows" would
6619        // pass on a table that wrote one row and on one that wrote all of them,
6620        // and it is exactly the second that costs 17× the pack.
6621        let graph_rows = store.exploded_of_kind(GitObjectKind::Commit).unwrap().len();
6622        assert_eq!(
6623            store.exploded_eager_rows().unwrap(),
6624            graph_rows as u64,
6625            "the drain wrote {} eager rows and there are {} commits — the eager pool \
6626             holds something other than the graph",
6627            store.exploded_eager_rows().unwrap(),
6628            graph_rows
6629        );
6630        assert!(
6631            stats.rows > 0 && stats.rows < distinct.len() as u64,
6632            "the eager pool holds {} of {} objects; lazy explode means neither 0 \
6633             (the graph would be blind) nor all of them (that is the 22.4x tier)",
6634            stats.rows,
6635            distinct.len()
6636        );
6637
6638        // Every one of them, re-hashed. Bytes that came back wrong cannot pass.
6639        let hash = store.hash_kind();
6640        let mut checked = 0usize;
6641        for oid in &distinct {
6642            let (kind, payload) = store
6643                .content(oid)
6644                .unwrap()
6645                .unwrap_or_else(|| panic!("{} has no exploded row", hex::encode(oid)));
6646            let rehashed = hash.oid_of(&crate::object::canonical(kind, &payload));
6647            assert_eq!(
6648                &rehashed,
6649                oid,
6650                "the exploded row filed under {} contains an object that hashes to {}",
6651                hex::encode(oid),
6652                hex::encode(&rehashed)
6653            );
6654            checked += 1;
6655        }
6656        assert_eq!(checked, distinct.len());
6657        // **Every object came back byte-correct**, and the ones the eager pool
6658        // does not hold came back by inflating their own delta chain out of the
6659        // mapped pack ([`crate::resolve::materialize_at`]). So the re-derivation
6660        // count is the blob count and not zero — asserting zero here is what a
6661        // test written for the eager tier does, and it would now be asserting
6662        // that lazy explode is off.
6663        let after_reads = store.exploded_stats();
6664        assert!(
6665            after_reads.rederived > 0,
6666            "no read re-derived anything, so either every object is eager or the \
6667             lazy path never ran"
6668        );
6669        assert_eq!(
6670            after_reads.rederived + after_reads.served,
6671            checked as u64,
6672            "reads {} = served {} + rederived {} must account for every object",
6673            checked,
6674            after_reads.served,
6675            after_reads.rederived
6676        );
6677        // **What it costs on disk, measured rather than assumed.** §14 says the
6678        // price is "both copies are held" and the decision was taken with "we
6679        // don't care if disk is tripled" — so the real multiple belongs in the
6680        // output where anybody can read it, not in a sentence. It is not 3x on a
6681        // real pack: the verbatim bytes are deflated *and* delta-encoded, the
6682        // table holds raw inflated content, and redb pages it.
6683        let inflated: u64 = rows.iter().map(|r| r.uncompressed_size).sum();
6684        let table = std::fs::metadata(store.exploded_path())
6685            .map(|m| m.len())
6686            .unwrap_or(0);
6687        eprintln!(
6688            "load {}; {} pack entries → {} exploded rows, all {} re-hashed to their own oid; \
6689             ack {:.1} ms, drain {:.1} ms, {:.0} rows/s; disk: pack {:.1} MiB verbatim, objects \
6690             inflate to {:.1} MiB, table file {:.1} MiB ({:.0}x the pack, {:.3}x the payload)",
6691            loadavg(),
6692            rows.len(),
6693            stats.rows,
6694            checked,
6695            ack.as_secs_f64() * 1e3,
6696            drain.as_secs_f64() * 1e3,
6697            stats.rows as f64 / drain.as_secs_f64(),
6698            pack.len() as f64 / (1 << 20) as f64,
6699            inflated as f64 / (1 << 20) as f64,
6700            table as f64 / (1 << 20) as f64,
6701            table as f64 / pack.len() as f64,
6702            table as f64 / inflated.max(1) as f64,
6703        );
6704    }
6705
6706    /// **The amplification measurement, at whatever scale you point it at.**
6707    ///
6708    /// [`a_push_produces_an_exploded_row_for_every_object_in_the_pack`] prints
6709    /// the same ratio on a 5.4 MiB fixture and is a *guard*, so it has to stay
6710    /// fast. This is the same arithmetic with no assertion about size and no
6711    /// fixture of its own: give it a pack and it reports what the table cost.
6712    ///
6713    /// ```text
6714    /// ZNIPPY_EXPLODE_BENCH_PACK=/path/to/pack-….pack \
6715    ///   cargo test -p znippy-plugin-git --lib the_table_costs_what_it_holds \
6716    ///   -- --ignored --nocapture
6717    /// ```
6718    ///
6719    /// It exists because the 12× figure that killed redb — 16.8 GB of resolved
6720    /// `linux.git` content in a 204 GB file — was taken by hand off a bench run
6721    /// that no longer exists, and a number nobody can re-take is a number that
6722    /// rots. `#[ignore]`d and pack-less by default: with no pack named it
6723    /// returns, so it never fails a suite for the absence of a corpus nobody
6724    /// promised to keep.
6725    #[test]
6726    #[ignore = "needs a pack: ZNIPPY_EXPLODE_BENCH_PACK=<path> cargo test … -- --ignored --nocapture"]
6727    fn the_table_costs_what_it_holds() {
6728        let Ok(path) = std::env::var("ZNIPPY_EXPLODE_BENCH_PACK") else {
6729            eprintln!("no ZNIPPY_EXPLODE_BENCH_PACK named; nothing measured");
6730            return;
6731        };
6732        let pack = std::fs::read(&path).unwrap_or_else(|e| panic!("reading {path}: {e}"));
6733        let dir = tmpdir("exploded-cost");
6734        let store = GitStore::open(&dir, "alice").unwrap();
6735
6736        let t = Instant::now();
6737        store.put_pack(&pack).unwrap();
6738        let ack = t.elapsed();
6739        store.wait_indexed();
6740        let drain = t.elapsed();
6741
6742        let stats = store.exploded_stats();
6743        let table = std::fs::metadata(store.exploded_path())
6744            .map(|m| m.len())
6745            .unwrap_or(0);
6746        // The payload the table actually holds, asked of the table rather than
6747        // of the pack — a pack entry's `uncompressed_size` is what git recorded,
6748        // and this is what we stored. Only the second one can be divided into
6749        // the file size and mean anything.
6750        let mut held = 0u64;
6751        for kind in [
6752            GitObjectKind::Commit,
6753            GitObjectKind::Tree,
6754            GitObjectKind::Blob,
6755            GitObjectKind::Tag,
6756        ] {
6757            held += store
6758                .exploded_of_kind(kind)
6759                .unwrap()
6760                .iter()
6761                .map(|(_, p)| p.len() as u64)
6762                .sum::<u64>();
6763        }
6764        let mib = |n: u64| n as f64 / (1 << 20) as f64;
6765        eprintln!(
6766            "load {}; pack {} — {:.1} MiB verbatim → {} rows holding {:.1} MiB, table file \
6767             {:.1} MiB = {:.3}x the payload and {:.1}x the pack; ack {:.0} ms, drain {:.0} ms",
6768            loadavg(),
6769            path,
6770            mib(pack.len() as u64),
6771            stats.rows,
6772            mib(held),
6773            mib(table),
6774            table as f64 / held.max(1) as f64,
6775            table as f64 / pack.len() as f64,
6776            ack.as_secs_f64() * 1e3,
6777            drain.as_secs_f64() * 1e3,
6778        );
6779    }
6780
6781    /// Where the child of the clean-shutdown guard finds its fixture and leaves
6782    /// its markers.
6783    const CLEAN_DIR: &str = "GUNNAR_CLEAN_SHUTDOWN_DIR";
6784
6785    /// **The other half of
6786    /// [`a_clean_shutdown_and_reopen_keeps_the_graph_and_reachable`]: the process
6787    /// that exits normally.**
6788    ///
6789    /// A real process, a real push, a real drain, and then a real `drop` — every
6790    /// worker joined, every redb handle closed, no signal, exit status 0. That is
6791    /// what makes the parent's reopen a *clean* reopen rather than a recovery,
6792    /// and it is the case the crash guard cannot reach: after a clean shutdown
6793    /// every pack's bit is legitimately set and **nothing is re-queued**, so the
6794    /// derived tables have to have been on disk or they are gone.
6795    ///
6796    /// `#[ignore]`d because it is a fixture, not a guard — run on its own it has
6797    /// no directory to work in and returns.
6798    #[test]
6799    #[ignore = "spawned by a_clean_shutdown_and_reopen_keeps_the_graph_and_reachable"]
6800    fn the_child_that_pushes_and_shuts_down_cleanly() {
6801        let Ok(dir) = std::env::var(CLEAN_DIR) else {
6802            return;
6803        };
6804        let dir = PathBuf::from(dir);
6805        let pack = std::fs::read(dir.join("fixture.pack")).expect("the parent's fixture");
6806
6807        let store = GitStore::open(&dir, "alice").expect("open");
6808        store.put_pack(&pack).expect("the ack path");
6809        store.wait_indexed();
6810        assert_eq!(store.unindexed_packs(), 0, "the child shut down mid-index");
6811
6812        let tip = store
6813            .graph_snapshot()
6814            .into_iter()
6815            .max_by_key(|c| c.generation)
6816            .expect("the child built no graph at all");
6817        let tip_raw = hex::decode(&tip.oid).unwrap();
6818        let closure = store.reachable(&[&tip_raw], &[]).unwrap();
6819        assert!(!closure.is_empty(), "the child's own reachable() was empty");
6820
6821        std::fs::write(dir.join("tip"), &tip.oid).expect("marker");
6822        std::fs::write(dir.join("closure"), closure.len().to_string()).expect("marker");
6823        std::fs::write(dir.join("commits"), store.commit_count().to_string()).expect("marker");
6824        std::fs::write(dir.join("objects"), store.object_count().to_string()).expect("marker");
6825
6826        // **The clean shutdown.** Not a signal and not an abort: the store drops,
6827        // which closes the channel, joins the account indexer's worker and closes
6828        // redb. The process then returns normally with status 0.
6829        drop(store);
6830        std::fs::write(dir.join("clean"), b"closed").expect("marker");
6831    }
6832
6833    /// **`commit_count()` and `reachable()` are correct after a CLEAN shutdown
6834    /// and reopen.** This is the guard the exploded table exists to make pass.
6835    ///
6836    /// It was the live bug: the commit graph was folded from commit and tree
6837    /// *payloads* which were stored nowhere, accumulated in RAM as packs were
6838    /// absorbed. A store killed mid-absorb got them back only because the
6839    /// interrupted pack was re-queued and re-resolved
6840    /// ([`a_store_reopened_after_a_kill_mid_absorb_requeues_the_pack`]); a store
6841    /// that shut down **cleanly** re-queued nothing, and MEASURED on oden
6842    /// 2026-08-08 came back with 2687 rows and **0 of 551 commits** —
6843    /// `reachable()` returning a commit alone instead of its closure, and `gc`
6844    /// seeing an empty live set. Both are quiet wrong answers.
6845    ///
6846    /// The shape is the crash guard's: a **real** child process, which here exits
6847    /// normally, and the parent asserts against what is on disk. Two assertions
6848    /// carry it and they have to be in this order:
6849    ///
6850    /// 1. **`commit_count()` immediately after `open`**, before anything is
6851    ///    waited on — so the graph can only have come from the fold over the
6852    ///    exploded table that `open` performs, not from a re-absorb;
6853    /// 2. **`is_indexed(0)` is false after `wait_indexed()`** — the account
6854    ///    indexer in *this* process publishes a pack only for a job somebody
6855    ///    submitted, so a clear bit proves open re-queued nothing and the graph
6856    ///    above is not a recovery artefact.
6857    ///
6858    /// `reachable()` is then asserted against the closure the child computed on
6859    /// the same tip, so it is the same answer and not merely a non-empty one.
6860    ///
6861    /// Seen RED by deleting `store.refold()?` from the end of
6862    /// `GitStore::open_with_arms`, so nothing folds the table on open: "the
6863    /// commit graph did not survive a clean shutdown: 0 of 551 commits — the
6864    /// exploded table is a warm cache, not a durable table", left 0 right 551.
6865    /// That is the recorded bug's own number, reproduced. Restored.
6866    ///
6867    /// Seen RED a second time by deleting `self.exploded.flush()?` from
6868    /// `Absorber::absorb_one`, which is the **durability** edit: the rows then
6869    /// live only in the un-flushed buffer, which answers perfectly inside the
6870    /// child and is gone when the process ends. It fires assertion (2) rather
6871    /// than (1) — "the reopen re-queued the pack, so the graph above is a
6872    /// recovery artefact rather than the table's" — and that is worth writing
6873    /// down, because assertion (1) *passed*: the 64 MiB flush threshold had
6874    /// tripped once mid-pack, leaving 1798 of 2687 rows on disk which happened
6875    /// to include all 551 commits. A guard with only assertion (1) would have
6876    /// called that edit green. Restored.
6877    /// **An annotated tag reaches the commit it points at.**
6878    ///
6879    /// # The bug this is the guard for
6880    ///
6881    /// Reachability bitmaps are built over the commit graph, and no tree points
6882    /// at a tag, so a tag is in nobody's closure. `reachable`'s `want` loop had
6883    /// one arm for "not a commit": insert the object itself, and stop. So a
6884    /// `want` that was an annotated tag answered with **one object** — the tag —
6885    /// and the commit it names, plus that commit's whole history, was left out.
6886    ///
6887    /// It stayed invisible because a tag is nearly always pushed alongside the
6888    /// branch that carries the same commit, and then the branch's closure
6889    /// covers it. It surfaces the moment a tag is the ONLY thing naming a
6890    /// commit — which is what gunnar's replication does, one ref at a time. Six
6891    /// repositories on the live fleet could not replicate on 2026-08-31, one of
6892    /// them with 25 tags hanging off a single commit no branch carried, all
6893    /// refused by the far end as `missing necessary objects … (peeling a tag)`.
6894    ///
6895    /// So the fixture asserts the premise it needs: the tag is the only `want`.
6896    ///
6897    /// # 🔴 Seen red
6898    ///
6899    /// Against the arm as it was (`union.insert(o)` and nothing more):
6900    /// `the tag's closure is 1 object, so it carries the tag and nothing it
6901    /// points at`. With the peel: the commit, its trees and its blobs.
6902    #[test]
6903    fn an_annotated_tag_reaches_the_commit_it_points_at() {
6904        let dir = tmpdir("tag-peel");
6905        let store = GitStore::open(&dir, "alice").unwrap();
6906        let (pack, rows) = corpus_pack();
6907        store.put(&pack, &[]).unwrap();
6908        store.wait_indexed();
6909
6910        // Any commit will do: the tag is the only `want`, so nothing else can
6911        // pull this commit's closure in behind our backs.
6912        let commit = rows
6913            .iter()
6914            .find(|r| r.kind == GitObjectKind::Commit)
6915            .expect("the corpus has commits");
6916        let commit_hex = hex::encode(&commit.oid);
6917        let commit_closure = store.reachable(&[&commit.oid], &[]).unwrap();
6918        assert!(
6919            commit_closure.len() > 1,
6920            "the fixture's commit closes over nothing, so this test could not tell a peel from a              no-op"
6921        );
6922
6923        let (tag_pack, tag_oid) = crate::store::tests::one_tag_pack(&commit_hex, "v1.0");
6924        store.put(&tag_pack, &[]).unwrap();
6925        store.wait_indexed();
6926
6927        let closure = store.reachable(&[&tag_oid], &[]).unwrap();
6928        assert!(
6929            closure.contains(&tag_oid),
6930            "the tag itself is not in its own closure"
6931        );
6932        assert!(
6933            closure.contains(&commit.oid),
6934            "the tag's closure is {} object(s), so it carries the tag and nothing it points at —              the commit {} it names is missing and a mirror built from this answer cannot be              read",
6935            closure.len(),
6936            &commit_hex[..12]
6937        );
6938        // Not merely the commit: everything the commit reaches, or a clone from
6939        // this answer is short by a tree.
6940        for oid in &commit_closure {
6941            assert!(
6942                closure.contains(oid),
6943                "the tag's closure is missing {} which the commit it points at reaches",
6944                hex::encode(oid)
6945            );
6946        }
6947    }
6948
6949    #[test]
6950    fn a_clean_shutdown_and_reopen_keeps_the_graph_and_reachable() {
6951        let dir = tmpdir("clean-shutdown-graph");
6952        let (pack, rows) = corpus_pack();
6953        std::fs::write(dir.join("fixture.pack"), &pack).unwrap();
6954
6955        let status = std::process::Command::new(std::env::current_exe().unwrap())
6956            .args([
6957                "--exact",
6958                "--ignored",
6959                "--nocapture",
6960                "git_ops::tests::the_child_that_pushes_and_shuts_down_cleanly",
6961            ])
6962            .env(CLEAN_DIR, &dir)
6963            .status()
6964            .expect("spawn the child that shuts down cleanly");
6965        assert!(
6966            status.success(),
6967            "the child did not exit cleanly — it {status:?}, so this is a crash guard and not a \
6968             clean-shutdown one"
6969        );
6970        assert!(
6971            dir.join("clean").exists(),
6972            "the child never reached its clean shutdown"
6973        );
6974
6975        let tip_hex = std::fs::read_to_string(dir.join("tip")).unwrap();
6976        let closure_before: usize = std::fs::read_to_string(dir.join("closure"))
6977            .unwrap()
6978            .parse()
6979            .unwrap();
6980        let commits_before: usize = std::fs::read_to_string(dir.join("commits"))
6981            .unwrap()
6982            .parse()
6983            .unwrap();
6984        assert_eq!(
6985            commits_before,
6986            commits_in(&rows),
6987            "the child's own graph was already wrong, so the reopen proves nothing"
6988        );
6989
6990        // ── the reopen, and (1): the graph before anything is waited on ──────
6991        let store = GitStore::open(&dir, "alice").unwrap();
6992        assert_eq!(
6993            store.commit_count(),
6994            commits_in(&rows),
6995            "the commit graph did not survive a clean shutdown: {} of {} commits — the exploded \
6996             table is a warm cache, not a durable table",
6997            store.commit_count(),
6998            commits_in(&rows)
6999        );
7000        let tip_raw = hex::decode(&tip_hex).unwrap();
7001        let closure = store.reachable(&[&tip_raw], &[]).unwrap();
7002        assert_eq!(
7003            closure.len(),
7004            closure_before,
7005            "reachable() answers differently after a clean reopen: {} objects against the {} the \
7006             same tip closed over before the shutdown",
7007            closure.len(),
7008            closure_before
7009        );
7010        assert!(closure.contains(&tip_raw));
7011
7012        // ── (2): and none of it came from a re-absorb ────────────────────────
7013        store.wait_indexed();
7014        assert!(
7015            !store.indexer().is_indexed(0),
7016            "the reopen re-queued the pack, so the graph above is a recovery artefact rather \
7017             than the table's"
7018        );
7019        assert_eq!(store.unindexed_packs(), 0);
7020        assert_eq!(
7021            store.absorb_pending().unwrap(),
7022            0,
7023            "the reopen left index work"
7024        );
7025        assert_eq!(store.object_count(), rows.len());
7026        assert_eq!(
7027            store.exploded_stats().written,
7028            0,
7029            "this process wrote exploded rows, so the table was rebuilt rather than read"
7030        );
7031
7032        eprintln!(
7033            "load {}; clean shutdown → reopen: {} rows, {} commits, tip closure {} objects \
7034             (rebuilt nothing)",
7035            loadavg(),
7036            store.object_count(),
7037            store.commit_count(),
7038            closure.len(),
7039        );
7040    }
7041
7042    /// **A reopen READS `objects.graph`; it does not recompute it — and the
7043    /// read graph equals the recomputed one.** This is S-065's fix, and the only
7044    /// thing that can say which path a fold took is a counter, so the assertion
7045    /// is on [`GitStore::graph_source_stats`], not on the (identical) graph
7046    /// bytes — the identity-value trap, since both paths yield the same graph.
7047    ///
7048    /// One test exercises **both** arms so the counter cannot be blindly stuck:
7049    ///
7050    /// 1. A first store builds the graph, persists it, and shuts down cleanly.
7051    /// 2. `objects.graph` is deleted, so the reopen has no cache: it **recomputes**
7052    ///    (`(read, recomputed) == (0, 1)`) and re-persists. Its graph and its
7053    ///    `reachable()` are captured — this is the recomputed truth.
7054    /// 3. A second reopen finds the cache the step above wrote: it **reads**
7055    ///    (`(1, 0)`), and its graph and `reachable()` must equal step 2's exactly
7056    ///    — read == recomputed.
7057    ///
7058    /// # 🔴 Seen red
7059    ///
7060    /// With the persistence removed (open always folds), step 3 is `(0, 1)`:
7061    /// "the reopen recomputed the graph instead of reading objects.graph", left
7062    /// `(0, 1)` right `(1, 0)`. With the token check inverted so a stale cache is
7063    /// accepted, step 3's `reachable()` diverges from step 2's and the read==
7064    /// recomputed assertion fires. Restored.
7065    #[test]
7066    fn a_reopen_reads_the_persisted_graph_it_does_not_recompute() {
7067        let dir = tmpdir("persisted-graph");
7068        let (pack, rows) = corpus_pack();
7069        let tip_hex;
7070        {
7071            let store = GitStore::open(&dir, "alice").unwrap();
7072            store.put_pack(&pack).unwrap();
7073            store.wait_indexed();
7074            tip_hex = store
7075                .graph_snapshot()
7076                .into_iter()
7077                .max_by_key(|c| c.generation)
7078                .expect("a graph")
7079                .oid;
7080            assert!(
7081                dir.join("objects.graph").exists(),
7082                "an absorb never persisted the graph, so a reopen has nothing to read"
7083            );
7084        }
7085        let tip_raw = hex::decode(&tip_hex).unwrap();
7086
7087        // ── (2) no cache → recompute, and capture the recomputed truth ───────
7088        std::fs::remove_file(dir.join("objects.graph")).unwrap();
7089        let (recomputed_graph, recomputed_closure) = {
7090            let store = GitStore::open(&dir, "alice").unwrap();
7091            assert_eq!(
7092                store.graph_source_stats(),
7093                (0, 1),
7094                "with no objects.graph the reopen should have recomputed the graph"
7095            );
7096            assert_eq!(store.commit_count(), commits_in(&rows));
7097            let g = store.graph_snapshot();
7098            let c = store.reachable(&[&tip_raw], &[]).unwrap();
7099            assert!(!c.is_empty());
7100            (g, c)
7101        };
7102
7103        // ── (3) cache present → read, and it must equal the recomputed truth ──
7104        let store = GitStore::open(&dir, "alice").unwrap();
7105        assert_eq!(
7106            store.graph_source_stats(),
7107            (1, 0),
7108            "the reopen recomputed the graph instead of reading objects.graph"
7109        );
7110        assert_eq!(
7111            store.graph_snapshot(),
7112            recomputed_graph,
7113            "the graph READ from objects.graph differs from the one refold RECOMPUTES"
7114        );
7115        assert_eq!(
7116            store.reachable(&[&tip_raw], &[]).unwrap(),
7117            recomputed_closure,
7118            "reachable() over the persisted graph differs from reachable() over the recomputed one"
7119        );
7120        assert_eq!(store.commit_count(), commits_in(&rows));
7121    }
7122
7123    /// **The graph cache switched off leaves no `objects.graph`** — and the store
7124    /// still answers the same graph and closure, folded from the exploded table.
7125    /// The switch is flipped on the absorber directly: `ZNIPPY_GIT_GRAPH_CACHE`
7126    /// is read at open, and setting it process-wide would race the test above.
7127    ///
7128    /// Seen red with the persist guard removed: "the graph cache is off and
7129    /// objects.graph was written anyway".
7130    #[test]
7131    fn with_the_graph_cache_off_no_objects_graph_is_written_and_the_graph_is_the_same() {
7132        let on_dir = tmpdir("graph-cache-on");
7133        let off_dir = tmpdir("graph-cache-off");
7134        let (pack, rows) = corpus_pack();
7135        let on = GitStore::open(&on_dir, "alice").unwrap();
7136        on.put_pack(&pack).unwrap();
7137        on.wait_indexed();
7138        let off = GitStore::open(&off_dir, "alice").unwrap();
7139        off.absorber.graph_cache.store(false, std::sync::atomic::Ordering::Relaxed);
7140        std::fs::remove_file(off_dir.join("objects.graph")).ok();
7141        off.put_pack(&pack).unwrap();
7142        off.wait_indexed();
7143        assert!(on_dir.join("objects.graph").exists(), "the default still persists the graph");
7144        assert!(
7145            !off_dir.join("objects.graph").exists(),
7146            "the graph cache is off and objects.graph was written anyway"
7147        );
7148        assert_eq!(off.graph_source_stats().0, 0, "a store with the cache off never reads one");
7149        assert_eq!(on.graph_snapshot(), off.graph_snapshot());
7150        assert_eq!(off.commit_count(), commits_in(&rows));
7151    }
7152
7153    /// **The token gates the read: a cache whose token does not match the
7154    /// object set is rejected and the graph is recomputed.** This is what makes
7155    /// `objects.graph` a memo rather than a second truth — and a push, a gc, or
7156    /// a torn write all reach the store through exactly this door.
7157    ///
7158    /// The object set is left unchanged and the token in the cache is corrupted
7159    /// directly, so the ONE thing under test is the token comparison and not
7160    /// some incidental object-count difference.
7161    ///
7162    /// # 🔴 Seen red
7163    ///
7164    /// With the token comparison dropped (`Some((_, g)) => g`), the corrupted
7165    /// cache is read and the reopen is `(1, 0)`: "a cache with the wrong token
7166    /// was trusted". Restored.
7167    #[test]
7168    fn a_graph_whose_token_does_not_match_is_rejected() {
7169        let dir = tmpdir("wrong-token-graph");
7170        let (pack, _rows) = corpus_pack();
7171        {
7172            let store = GitStore::open(&dir, "alice").unwrap();
7173            store.put_pack(&pack).unwrap();
7174            store.wait_indexed();
7175        }
7176        // Flip a byte inside the 32-byte token (offset 5..37) without touching
7177        // the graph body, so only the token is wrong.
7178        let path = dir.join("objects.graph");
7179        let mut bytes = std::fs::read(&path).unwrap();
7180        assert!(bytes.len() > 37 && &bytes[..4] == b"GRPH");
7181        bytes[6] ^= 0xff;
7182        std::fs::write(&path, &bytes).unwrap();
7183
7184        let store = GitStore::open(&dir, "alice").unwrap();
7185        assert_eq!(
7186            store.graph_source_stats(),
7187            (0, 1),
7188            "a cache with the wrong token was trusted instead of being recomputed"
7189        );
7190    }
7191
7192    /// **The ordinal space comes off the persisted index, not off a scan — and
7193    /// no `objects.ordinal` is ever written.** This replaces S-082's memo test:
7194    /// the memo spared a cold open the redb scan that rebuilt the sorted oid
7195    /// list, and since 2026-09-04 that list IS the persisted index's oid column,
7196    /// so there is nothing to spare and no file to validate. What is asserted:
7197    ///
7198    /// 1. A first store absorbs the corpus; its ordinal space equals the tail in
7199    ///    oid order and every oid is accounted to one of the two sources. Then
7200    ///    it folds explicitly, so redb is empty and the index holds everything.
7201    /// 2. The reopen is warm: its space comes **entirely off the index** —
7202    ///    `(folded, tail) == (n, 0)` — and equals step 1's space, closure and
7203    ///    counts exactly. `objects.ordinal` does not exist after either session.
7204    ///
7205    /// # 🔴 Seen red
7206    ///
7207    /// With `ObjectReadStack::oids_raw` returning only the redb tail on a
7208    /// persisting stack, step 2's space is empty (redb holds nothing after the
7209    /// fold) and `reachable()` over it is empty: "the ordinal space after the
7210    /// warm reopen is not the tail in oid order". Restored.
7211    #[test]
7212    fn the_ordinal_space_comes_off_the_index_not_a_scan() {
7213        let dir = tmpdir("ordinal-off-the-index");
7214        let (pack, rows) = corpus_pack();
7215        let tip_hex;
7216        let (first_oids, first_closure, first_objects, first_commits);
7217        {
7218            let store = GitStore::open(&dir, "alice").unwrap();
7219            store.put_pack(&pack).unwrap();
7220            store.wait_indexed();
7221            tip_hex = store
7222                .graph_snapshot()
7223                .into_iter()
7224                .max_by_key(|c| c.generation)
7225                .expect("a graph")
7226                .oid;
7227            let tip_raw = hex::decode(&tip_hex).unwrap();
7228            let n = store.object_count() as u64;
7229            let (folded, tail) = store.ordinal_source_stats();
7230            assert_eq!(folded + tail, n, "every oid is accounted to the index or the tail");
7231            let oids = store.ordinal_oids();
7232            let scanned: Vec<String> =
7233                store.index().oids_in_order().unwrap().iter().map(hex::encode).collect();
7234            assert_eq!(oids, scanned, "the ordinal space is not the repository in oid order");
7235            assert!(
7236                !dir.join("objects.ordinal").exists(),
7237                "no memo may be written: the index's oid column is the ordinal space"
7238            );
7239            // Fold, so the reopen has everything on the index and nothing in redb.
7240            store.index().rebuild().unwrap();
7241            first_closure = store.reachable(&[&tip_raw], &[]).unwrap();
7242            assert!(!first_closure.is_empty());
7243            first_oids = oids;
7244            first_objects = store.object_count();
7245            first_commits = store.commit_count();
7246        }
7247        let tip_raw = hex::decode(&tip_hex).unwrap();
7248
7249        let store = GitStore::open(&dir, "alice").unwrap();
7250        let n = store.object_count() as u64;
7251        assert_eq!(
7252            store.ordinal_source_stats(),
7253            (n, 0),
7254            "after a fold the warm reopen's ordinal space must come entirely off the index"
7255        );
7256        assert_eq!(
7257            store.ordinal_oids(),
7258            first_oids,
7259            "the ordinal space after the warm reopen is not the tail in oid order"
7260        );
7261        assert_eq!(
7262            store.reachable(&[&tip_raw], &[]).unwrap(),
7263            first_closure,
7264            "reachable() over the index's ordinal space differs from the first session's"
7265        );
7266        assert_eq!(store.object_count(), first_objects);
7267        assert_eq!(store.commit_count(), first_commits);
7268        assert_eq!(store.commit_count(), commits_in(&rows));
7269        assert!(!dir.join("objects.ordinal").exists(), "still no memo after the reopen");
7270    }
7271
7272    /// **A stale `objects.ordinal` left by an older binary is removed at open.**
7273    /// The file is dead weight now (279 MB on the kernel mirror) and its
7274    /// contents are never read, so a store that carries one loses it the first
7275    /// time this build opens it — and keeps working.
7276    ///
7277    /// # 🔴 Seen red
7278    ///
7279    /// With the `remove_file` in `open_with_arms` dropped, the file survives the
7280    /// open: "a stale objects.ordinal must be removed at open". Restored.
7281    #[test]
7282    fn a_stale_objects_ordinal_from_an_older_binary_is_removed_at_open() {
7283        let dir = tmpdir("stale-ordinal");
7284        let (pack, _rows) = corpus_pack();
7285        {
7286            let store = GitStore::open(&dir, "alice").unwrap();
7287            store.put_pack(&pack).unwrap();
7288            store.wait_indexed();
7289        }
7290        // The old memo's shape: magic, version, oid_len, epoch, count, oids.
7291        let path = dir.join("objects.ordinal");
7292        let mut bytes = b"ORDL".to_vec();
7293        bytes.push(1);
7294        bytes.push(20);
7295        bytes.extend_from_slice(&7u64.to_le_bytes());
7296        bytes.extend_from_slice(&1u64.to_le_bytes());
7297        bytes.extend_from_slice(&[0xabu8; 20]);
7298        std::fs::write(&path, &bytes).unwrap();
7299        assert!(path.exists(), "precondition");
7300
7301        let store = GitStore::open(&dir, "alice").unwrap();
7302        assert!(!path.exists(), "a stale objects.ordinal must be removed at open");
7303        assert!(store.object_count() > 0, "and the store still answers");
7304    }
7305
7306    /// **A content read is served from the table, not re-derived** — and the only
7307    /// thing that can say so is a counter.
7308    ///
7309    /// Both paths return the **identical** bytes: the table hands back what the
7310    /// resolver produced, and the fallback re-resolves the same pack and produces
7311    /// it again. A byte comparison therefore proves nothing at all about which
7312    /// one ran — this is the identity-value trap in its exact form, and it fired
7313    /// on this codebase once already. So the assertion is on
7314    /// [`ExplodedStats::served`] against [`ExplodedStats::rederived`], both of
7315    /// which are bumped where the work happens.
7316    ///
7317    /// The fallback is not asserted away: it is exercised in the same test, on a
7318    /// store whose table has been dropped, and the counters move the other way.
7319    /// A guard that only saw the fast path could not tell a working counter from
7320    /// one that is never incremented.
7321    ///
7322    /// Seen RED by deleting the `if let Some(hit) = self.exploded.content(oid)?`
7323    /// early return in `Absorber::resolved`, so every read re-derives: "512 content
7324    /// reads re-resolved a whole pack instead of hitting the table — served 0,
7325    /// rederived 512". The bytes were still correct on every one of them, which
7326    /// is the point. Restored.
7327    ///
7328    /// Seen RED a second time by bumping `served` unconditionally at the top of
7329    /// `ExplodedTable::content`, before the `pending`/redb probe — the counter
7330    /// then reports a hit whether or not the table had the object. The first half
7331    /// fires on the double count, "512 content reads re-resolved a whole pack
7332    /// instead of hitting the table — served 1024, rederived 0", and
7333    /// `exploded::tests::a_row_round_trips_and_the_kind_index_finds_it` fires with
7334    /// it ("a content read was not counted", left 5 right 2). Restored — a
7335    /// counter that cannot be wrong about a miss is the only kind worth
7336    /// asserting on.
7337    #[test]
7338    fn a_content_read_is_served_from_the_table_and_not_re_derived() {
7339        const READS: usize = 512;
7340        let dir = tmpdir("exploded-served");
7341        let (pack, rows) = corpus_pack();
7342        let oids: Vec<Vec<u8>> = rows
7343            .iter()
7344            .map(|r| r.oid.clone())
7345            .collect::<HashSet<Vec<u8>>>()
7346            .into_iter()
7347            .take(READS)
7348            .collect();
7349
7350        {
7351            let store = GitStore::open(&dir, "alice").unwrap();
7352            store.put_pack(&pack).unwrap();
7353            store.wait_indexed();
7354
7355            // ── FIRST read: the lazy path, timed ────────────────────────
7356            let before = store.exploded_stats();
7357            let t = Instant::now();
7358            let mut first: Vec<(GitObjectKind, Vec<u8>)> = Vec::with_capacity(oids.len());
7359            for oid in &oids {
7360                first.push(
7361                    store
7362                        .content(oid)
7363                        .unwrap()
7364                        .expect("the store lost an object"),
7365                );
7366            }
7367            let cold = t.elapsed();
7368            let after = store.exploded_stats();
7369            let rederived = after.rederived - before.rederived;
7370            assert_eq!(
7371                rederived + (after.served - before.served),
7372                oids.len() as u64,
7373                "{} reads = served {} + rederived {} must account for every one",
7374                oids.len(),
7375                after.served - before.served,
7376                rederived
7377            );
7378            assert!(
7379                rederived > 0,
7380                "not one read re-derived: either every object is eager or the lazy \
7381                 path never ran, and this test then proves nothing about either"
7382            );
7383
7384            // The write-back is off the read path, so it has to be waited for
7385            // HERE and only here — a reader never does.
7386            store.explode_cache_maintain().unwrap();
7387            // **How many of those reads the cache DECLINED.** A shallow, small
7388            // object is cheaper to inflate again than to keep, so it is read
7389            // directly and nothing is written
7390            // (`exploded_cache::worth_caching`). Those objects are re-derived
7391            // on every read for ever, on purpose, and the second pass below
7392            // has to expect exactly that many — not zero, which is what this
7393            // test asserted before the admission policy existed and would now
7394            // be asserting that the policy is off.
7395            let declined = store.explode_cache_use().direct_reads;
7396            assert!(
7397                declined > 0,
7398                "not one of {} reads was declined as cheap; either every object in \
7399                 this corpus is expensive or the admission policy is not running",
7400                oids.len()
7401            );
7402
7403            // ── SECOND read: the design note's "= today" column ──────────
7404            let mid = store.exploded_stats();
7405            let t = Instant::now();
7406            for (i, oid) in oids.iter().enumerate() {
7407                let again = store.content(oid).unwrap().expect("the store lost an object");
7408                assert_eq!(again, first[i], "the second read of an object disagreed with the first");
7409            }
7410            let warm = t.elapsed();
7411            let end = store.exploded_stats();
7412            assert_eq!(
7413                end.rederived - mid.rederived,
7414                declined,
7415                "the second pass re-derived {} objects and the cache declined {declined} — \
7416                 every EXPENSIVE object must come out of the table the second time, and \
7417                 every cheap one must be read directly again",
7418                end.rederived - mid.rederived
7419            );
7420            assert_eq!(
7421                end.served - mid.served,
7422                oids.len() as u64 - declined,
7423                "the expensive objects did not come out of the table on the second pass"
7424            );
7425            eprintln!(
7426                "load {}; {} content reads: FIRST {:.1} ms ({:.1} µs/read, {} re-derived from \
7427                 the pack, {declined} of them too cheap to cache) → SECOND {:.1} ms \
7428                 ({:.1} µs/read) = {:.1}x",
7429                loadavg(),
7430                oids.len(),
7431                cold.as_secs_f64() * 1e3,
7432                cold.as_secs_f64() * 1e6 / oids.len() as f64,
7433                rederived,
7434                warm.as_secs_f64() * 1e3,
7435                warm.as_secs_f64() * 1e6 / oids.len() as f64,
7436                cold.as_secs_f64() / warm.as_secs_f64().max(f64::MIN_POSITIVE),
7437            );
7438        }
7439
7440        // ── and the fallback, so the counter is not a constant ───────────────
7441        //
7442        // The table is dropped and the *drain* is what would rebuild it, so the
7443        // reads have to happen before it gets there. `absorb_pending` is called
7444        // by hand first so the fallback under test is the content path's and not
7445        // the index's.
7446        std::fs::remove_file(dir.join("objects.exploded")).unwrap();
7447        // **And the lazy pool with it.** Dropping only the eager table leaves
7448        // the write-backs of the pass above sitting in `objects.exploded.cN`,
7449        // and those would answer — the fallback would then be measured on a
7450        // store that still has a cache, which is the exact shape of a test that
7451        // cannot fail. One `drop_explode_cache` is the sanctioned door, and it
7452        // is what `repo gc` calls.
7453        for e in std::fs::read_dir(&dir).unwrap().flatten() {
7454            if e.file_name().to_string_lossy().starts_with("objects.exploded.c") {
7455                std::fs::remove_file(e.path()).unwrap();
7456            }
7457        }
7458        let store = GitStore::open(&dir, "alice").unwrap();
7459        let gate = store.hold_absorb_gate();
7460        let before = store.exploded_stats();
7461        let mut rederived = 0u64;
7462        for oid in oids.iter().take(8) {
7463            // Straight at the absorber: the store's `content` would absorb the
7464            // re-queued pack first and rebuild the very table this is testing
7465            // the absence of.
7466            assert!(
7467                store.absorber.resolved(oid).unwrap().is_some(),
7468                "the verbatim truth could not re-derive {}",
7469                hex::encode(oid)
7470            );
7471            rederived += 1;
7472        }
7473        let after = store.exploded_stats();
7474        // The drain starts the instant the store opens; on a loaded box it can
7475        // rebuild the small corpus's table before the gate above is taken, and
7476        // then the reads are legitimately served from it. Seen 2026-08-30 under
7477        // a parallel suite. That run proves nothing about the fallback and says
7478        // so, rather than failing on a race the test does not control.
7479        let drain_won = store.unindexed_packs() == 0;
7480        drop(gate);
7481        if drain_won {
7482            eprintln!("the drain rebuilt the table before the gate was held; the fallback was not observed this run");
7483            return;
7484        }
7485        assert_eq!(
7486            after.served - before.served,
7487            0,
7488            "a dropped table still reported {} reads served",
7489            after.served - before.served
7490        );
7491        assert_eq!(
7492            after.rederived - before.rederived,
7493            rederived,
7494            "the fallback ran but was not counted"
7495        );
7496    }
7497
7498    /// ★ **THE READ-AFTER-ACK GAP (lane U → lane Q): every read path answers
7499    /// with the indexer PAUSED, and none of them waits for it.**
7500    ///
7501    /// MEASURED on the rig 2026-08-30 (lane U, both ISOs, timing-dependent):
7502    /// right after an ACKED push a `clone` came back
7503    /// `remote error: object … is not in this repository (peeling a tag)` and a
7504    /// `push --mirror` on top got `[remote rejected] … missing necessary
7505    /// objects`. Both of those are gunnar's `Graph::load` — `GitServe::read` —
7506    /// answering `None` for an object whose bytes were fsynced before the client
7507    /// was told the push had landed. §13.12 says an index miss falls back to the
7508    /// journal; the tag-peel and connectivity-check paths were not observed to.
7509    ///
7510    /// So this holds the absorb gate — the indexer paused, exactly the state
7511    /// lane P's watchdog puts it in — and requires **`has`, `header` and `read`
7512    /// to answer for every object of a just-acked pack**, byte-correct, and to
7513    /// do it inside a deadline. The deadline is not decoration: "nobody waits"
7514    /// (the design note's rule) is violated by a read that blocks on the gate
7515    /// just as badly as by one that answers absent, and a blocking read looks
7516    /// exactly like a correct one to an assertion that has no clock.
7517    ///
7518    /// Seen RED by returning `Ok(None)` from `Absorber::resolved`'s pending arm:
7519    /// 1352 of 1352 objects report "the store said absent for an object it had
7520    /// already acked".
7521    #[test]
7522    fn every_read_path_answers_a_just_acked_push_with_the_indexer_paused() {
7523        use crate::serve::GitServe as _;
7524        let dir = tmpdir("read-after-ack");
7525        let store = Arc::new(GitStore::open(&dir, "alice").unwrap());
7526        let (pack, rows) = corpus_pack();
7527
7528        // The ack: `put_pack` returning is what the client is told.
7529        store.put_pack(&pack).unwrap();
7530
7531        // The indexer, paused. A watchdog pause (lane P) pauses the indexer and
7532        // nothing else, so this is the shape the appliance is really in.
7533        let gate = store.hold_absorb_gate();
7534        assert!(
7535            store.unindexed_packs() > 0,
7536            "the drain absorbed the pack before the gate was taken, so this run \
7537             read an INDEXED store and proves nothing about the window"
7538        );
7539
7540        let distinct: Vec<Vec<u8>> = rows
7541            .iter()
7542            .map(|r| r.oid.clone())
7543            .collect::<HashSet<Vec<u8>>>()
7544            .into_iter()
7545            .collect();
7546        let hash = store.hash_kind();
7547        let n = distinct.len();
7548
7549        // On another thread, so the deadline measures the reads and not this
7550        // test's own patience.
7551        let (tx, rx) = std::sync::mpsc::channel::<Result<(usize, usize, usize)>>();
7552        let reader = {
7553            let store = store.clone();
7554            let distinct = distinct.clone();
7555            std::thread::spawn(move || {
7556                let mut had = 0usize;
7557                let mut headed = 0usize;
7558                let mut readback = 0usize;
7559                let r = (|| -> Result<()> {
7560                    for oid in &distinct {
7561                        if store.has(oid)? {
7562                            had += 1;
7563                        }
7564                        if crate::serve::GitServe::header(&*store, oid)?.is_some() {
7565                            headed += 1;
7566                        }
7567                        // The tag-peel / connectivity-check path, byte for byte.
7568                        let Some((kind, payload)) = crate::serve::GitServe::read(&*store, oid)? else {
7569                            continue;
7570                        };
7571                        let k = match kind {
7572                            crate::index_layout::ObjType::Commit => GitObjectKind::Commit,
7573                            crate::index_layout::ObjType::Tree => GitObjectKind::Tree,
7574                            crate::index_layout::ObjType::Blob => GitObjectKind::Blob,
7575                            crate::index_layout::ObjType::Tag => GitObjectKind::Tag,
7576                            // A read resolves the chain, so a delta type coming
7577                            // back out of it is the store contradicting itself.
7578                            other => anyhow::bail!("`read` answered with a {other:?}"),
7579                        };
7580                        let rehashed = hash.oid_of(&crate::object::canonical(k, &payload));
7581                        if &rehashed == oid {
7582                            readback += 1;
7583                        }
7584                    }
7585                    Ok(())
7586                })();
7587                let _ = tx.send(r.map(|_| (had, headed, readback)));
7588            })
7589        };
7590
7591        // The deadline. Generous — a cold pending scan of the corpus is
7592        // milliseconds — and it is against a WALL clock while the gate is held,
7593        // so a read that parks on the absorber trips it.
7594        let out = rx
7595            .recv_timeout(Duration::from_secs(20))
7596            .expect(
7597                "a read path blocked on the paused indexer for 20 s: 'nobody waits' is the \
7598                 design note's rule and a read that waits for the drain is a bug by definition",
7599            );
7600        let (had, headed, readback) = out.expect("a read path errored inside the index window");
7601        drop(gate);
7602        let _ = reader.join();
7603
7604        assert_eq!(
7605            (had, headed, readback),
7606            (n, n, n),
7607            "the store answered for {had}/{n} `has`, {headed}/{n} `header` and {readback}/{n} \
7608             byte-correct `read`s of a pack it had already ACKED — an object the client was \
7609             told had landed must never read as absent"
7610        );
7611    }
7612
7613    /// ★ **An account at its explode budget serves EVERY object, correctly —
7614    /// only slower.**
7615    ///
7616    /// The quota is a cache size, not a storage limit: over budget the server
7617    /// EVICTS, it does not refuse. So a repository squeezed to a budget far
7618    /// under what it holds must still answer every read byte-for-byte, and the
7619    /// only thing that may change is where the bytes came from — the table, or
7620    /// the pack via [`crate::resolve::materialize_at`].
7621    ///
7622    /// This is requirement (c) of the quota: *"an account at its budget must
7623    /// still serve every read correctly, only slower"*. The `rederived`
7624    /// counter is what proves the eviction really happened, because identical
7625    /// bytes come back either way and a byte comparison cannot tell a working
7626    /// cache from a dead one — the identity-value trap in its exact form.
7627    ///
7628    /// Seen RED by having `write_back` return the payload's bytes from a
7629    /// half-written segment (`out.truncate(1)`): "the store returned the wrong
7630    /// bytes for an object under a squeezed budget".
7631    #[test]
7632    fn an_account_at_its_budget_serves_every_object_only_slower() {
7633        let dir = tmpdir("explode-budget");
7634        let store = GitStore::open(&dir, "alice").unwrap();
7635        let (pack, rows) = corpus_pack();
7636        store.put_pack(&pack).unwrap();
7637        store.wait_indexed();
7638
7639        let oids: Vec<Vec<u8>> = rows
7640            .iter()
7641            .map(|r| r.oid.clone())
7642            .collect::<HashSet<Vec<u8>>>()
7643            .into_iter()
7644            .collect();
7645
7646        // Every object once, unsqueezed: the truth to compare against.
7647        let mut truth: Vec<(GitObjectKind, Vec<u8>)> = Vec::with_capacity(oids.len());
7648        for oid in &oids {
7649            truth.push(store.content(oid).unwrap().expect("the store lost an object"));
7650        }
7651        store.explode_cache_maintain().unwrap();
7652
7653        // Now the plan changes: a budget far under what this repository holds.
7654        store.set_explode_budget(64 << 10);
7655        let before = store.exploded_stats();
7656        for _ in 0..3 {
7657            for (i, oid) in oids.iter().enumerate() {
7658                let got = store
7659                    .content(oid)
7660                    .unwrap()
7661                    .expect("an object went absent when the budget was lowered");
7662                assert_eq!(
7663                    got, truth[i],
7664                    "the store returned the wrong bytes for an object under a squeezed budget"
7665                );
7666            }
7667            store.explode_cache_maintain().unwrap();
7668        }
7669        let after = store.exploded_stats();
7670        let band = store.explode_cache_use();
7671
7672        assert!(
7673            band.evicted_bytes > 0 || band.lazy_bytes <= 64 << 10,
7674            "the budget was lowered to 64 KiB and the lazy pool holds {} B with nothing \\
7675             evicted — the quota is not enforced, so this run proves nothing about reading \\
7676             under one: {band:?}",
7677            band.lazy_bytes
7678        );
7679        assert!(
7680            after.rederived > before.rederived,
7681            "not one read re-derived from the pack under a 64 KiB budget, so the cache was \\
7682             never squeezed and 'only slower' was never tested"
7683        );
7684        eprintln!(
7685            "load {}; {} objects read 3x under a 64 KiB budget: {} re-derived, {} served, \
7686{} evicted bytes over {} segments; every byte identical",
7687            loadavg(),
7688            oids.len(),
7689            after.rederived - before.rederived,
7690            after.served - before.served,
7691            band.evicted_bytes,
7692            band.evicted_segments,
7693        );
7694    }
7695
7696    /// ★ **A THIN push on top of a just-acked one is accepted with the indexer
7697    /// PAUSED** — lane U's second rig symptom.
7698    ///
7699    /// MEASURED on the rig 2026-08-30 (lane U): right after an acked push,
7700    /// `git push --mirror` of the next pack came back
7701    /// `[remote rejected] … missing necessary objects`, on a varying set of
7702    /// repositories run to run — a race, not a repository. A thin pack names
7703    /// its delta bases by oid and expects the server to have them; the server
7704    /// **did** have them, in a pack it had acked and not yet indexed, and
7705    /// `scan_pending`'s re-entrancy guard answered `None` for every base looked
7706    /// up from inside a scan rather than only for the ones that would have
7707    /// started a second scan.
7708    ///
7709    /// Seen RED by restoring that guard (`if nested { return Ok((out, None)) }`):
7710    /// *"reading the thin pack's object errored inside the index window:
7711    /// resolving a pending pack read-only for lookups — this pack is thin: it
7712    /// deltas against 42088154… which is not in it, and no base source could
7713    /// produce that object's content"*. The **push** is accepted either way —
7714    /// `external_bases_exist` finds the base with a top-level `lookup_one`
7715    /// before it ever resolves — so the assertion that discriminates is the
7716    /// READ with the gate still held, and an earlier draft of this test that
7717    /// dropped the gate first passed under both arms.
7718    #[test]
7719    fn a_thin_push_on_top_of_an_unindexed_one_is_accepted() {
7720        let dir = tmpdir("thin-on-unindexed");
7721        let store = GitStore::open(&dir, "alice").unwrap();
7722        let (pack, rows) = corpus_pack();
7723        store.put_pack(&pack).unwrap();
7724
7725        // The indexer, paused — so the base can only be found in the pack that
7726        // was acked a moment ago.
7727        let gate = store.hold_absorb_gate();
7728        assert!(
7729            store.unindexed_packs() > 0,
7730            "the drain absorbed the first pack before the gate was taken; this run \
7731             pushed onto an INDEXED store and proves nothing"
7732        );
7733
7734        // A base with a payload, and its real size: a `REF_DELTA` declares the
7735        // source size and the resolver checks it, so a made-up number would fail
7736        // for a reason that has nothing to do with the window under test.
7737        let base = rows
7738            .iter()
7739            .find(|r| r.kind == GitObjectKind::Blob && r.uncompressed_size > 0)
7740            .expect("the corpus has a blob");
7741        let thin = crate::pack_fixture::thin_ref_delta_pack(
7742            &base.oid,
7743            base.uncompressed_size,
7744            b"lane Q was here\n",
7745        );
7746
7747        let tx = store.put_pack(&thin).unwrap_or_else(|e| {
7748            panic!("the thin push was refused: {e:#}");
7749        });
7750        assert!(tx.pack_id.is_some(), "the thin pack was not stored");
7751
7752        // **And its object reads back with the gate STILL HELD.** This is the
7753        // nested case and the one the guard was too wide for: the read scans
7754        // pack B, B's entry is a ref-delta, its base is in pack A, and
7755        // resolving it is a second `scan_pending` from inside the first. A
7756        // lookup into A's already-built scan terminates; only a *build* would
7757        // not.
7758        let hash = store.hash_kind();
7759        let oid = hash.oid_of(&crate::object::canonical(
7760            GitObjectKind::Blob,
7761            b"lane Q was here\n",
7762        ));
7763        let got = store.content(&oid);
7764        drop(gate);
7765        let (kind, payload) = got
7766            .expect("reading the thin pack's object errored inside the index window")
7767            .expect("the thin pack's own object is absent after its push was acked");
7768        assert_eq!(kind, GitObjectKind::Blob);
7769        assert_eq!(payload, b"lane Q was here\n");
7770    }
7771
7772    /// **Dropping the table and reopening still answers correctly — fall back,
7773    /// then rebuild.**
7774    ///
7775    /// §14 calls the resolved table *droppable*: derived, verifiable against the
7776    /// verbatim truth, and deletable at any time without consulting a client.
7777    /// This is that sentence as an operation — `rm objects.exploded` between two
7778    /// opens — and §13.12's rule applied to it unchanged: **absent means fall
7779    /// back, never means wrong.**
7780    ///
7781    /// Three things are asserted, in this order:
7782    ///
7783    /// 1. the file really is gone and the reopened store really does start with
7784    ///    an empty table — otherwise the rest is testing a warm one;
7785    /// 2. **`reachable()` is already correct**, asked before any drain has been
7786    ///    waited on: the fall-back absorb runs inline and rebuilds what the
7787    ///    answer needs;
7788    /// 3. the table is **rebuilt** — one row per object again, the graph back,
7789    ///    and the rows written by *this* process, which is what says a rebuild
7790    ///    happened rather than a survival.
7791    ///
7792    /// Seen RED by `let exploded_is_whole = self.exploded.rows()? >= self.objects.len()
7793    /// as u64;` → `= true;` in `Absorber::adopt_journal`, so a dropped table is
7794    /// never noticed and nothing is re-queued. Assertion (2) fires first:
7795    /// **"reachable() after dropping the table: 1 objects, against 2683
7796    /// before"** — one object, the tip alone, which is the recorded bug's exact
7797    /// symptom ("`reachable` on a commit returns that commit alone rather than
7798    /// its closure") arrived at from the other direction. Restored.
7799    ///
7800    /// Seen RED a second time by `>=` → `>` in the same expression, which makes a
7801    /// *whole* table look short. This guard stays green — the rebuild is
7802    /// correct — and two others fire instead:
7803    /// [`a_clean_shutdown_and_reopen_keeps_the_graph_and_reachable`]'s "the
7804    /// reopen re-queued the pack, so the graph above is a recovery artefact
7805    /// rather than the table's", and the pre-existing
7806    /// [`a_clean_reopen_requeues_nothing_and_the_next_push_gets_a_fresh_ordinal`]'s
7807    /// "a fully absorbed pack was re-queued on open — the diff is not exact".
7808    /// That pair is the point: one guard catches the table not being rebuilt when
7809    /// it should be, the others catch it being rebuilt when it should not.
7810    /// Restored.
7811    #[test]
7812    fn dropping_the_exploded_table_and_reopening_still_answers() {
7813        let dir = tmpdir("exploded-dropped");
7814        let (pack, rows) = corpus_pack();
7815        let tip_hex;
7816        let closure_before;
7817        {
7818            let store = GitStore::open(&dir, "alice").unwrap();
7819            store.put_pack(&pack).unwrap();
7820            store.wait_indexed();
7821            let tip = store
7822                .graph_snapshot()
7823                .into_iter()
7824                .max_by_key(|c| c.generation)
7825                .expect("a graph");
7826            let tip_raw = hex::decode(&tip.oid).unwrap();
7827            closure_before = store.reachable(&[&tip_raw], &[]).unwrap().len();
7828            tip_hex = tip.oid;
7829        }
7830
7831        // (1) — the drop.
7832        let table = dir.join("objects.exploded");
7833        assert!(table.exists(), "the store never built an exploded table");
7834        std::fs::remove_file(&table).unwrap();
7835        {
7836            let fresh = crate::exploded_arrow::ExplodedArchive::open(&table).unwrap();
7837            assert_eq!(fresh.rows().unwrap(), 0, "the drop did not drop anything");
7838        }
7839        // Opening an absent table does not create one — so this only has
7840        // anything to remove if the assertion above was inspecting a live file.
7841        let _ = std::fs::remove_file(&table);
7842
7843        // (2) — a correct answer with the table gone.
7844        let store = GitStore::open(&dir, "alice").unwrap();
7845        let tip_raw = hex::decode(&tip_hex).unwrap();
7846        let closure = store.reachable(&[&tip_raw], &[]).unwrap();
7847        assert_eq!(
7848            closure.len(),
7849            closure_before,
7850            "reachable() after dropping the table: {} objects, against {} before",
7851            closure.len(),
7852            closure_before
7853        );
7854        assert!(store.has(&rows[0].oid).unwrap());
7855
7856        // (3) — and it is back. The GRAPH is what comes back at push time; a
7857        // blob comes back when somebody reads it, which is the whole of lazy
7858        // explode, so the count to compare against is the graph's and not the
7859        // pack's.
7860        store.wait_indexed();
7861        let stats = store.exploded_stats();
7862        let distinct: HashSet<Vec<u8>> = rows.iter().map(|r| r.oid.clone()).collect();
7863        // Commits, and NOT `of_kind(Tree)`: under the shipped policy a tree is
7864        // lazy, so the trees this test's own reads cached are in the lazy pool
7865        // and `of_kind` unions both.
7866        let graph_rows = store.exploded_of_kind(GitObjectKind::Commit).unwrap().len();
7867        // The EAGER pool, not `stats.rows`: step (2) above read objects through
7868        // the store, and every one of those reads wrote its object back into the
7869        // lazy pool, which `stats.rows` also counts. Comparing the sum against
7870        // the graph would be comparing "what the push wrote" against "what the
7871        // push wrote plus what this test happened to read".
7872        let eager = store.exploded_eager_rows().unwrap();
7873        assert_eq!(
7874            eager, graph_rows as u64,
7875            "the dropped table came back holding {eager} rows against a {}-object graph",
7876            graph_rows
7877        );
7878        assert!(
7879            eager > 0 && eager < distinct.len() as u64,
7880            "the rebuilt pool holds {eager} of {} objects — neither 0 nor all of them is \
7881             the lane Q shape",
7882            distinct.len()
7883        );
7884        assert!(
7885            stats.written > 0,
7886            "the table came back without this process writing a row — it was never dropped"
7887        );
7888        assert_eq!(
7889            store.commit_count(),
7890            commits_in(&rows),
7891            "the graph did not come back with the table: {} of {}",
7892            store.commit_count(),
7893            commits_in(&rows)
7894        );
7895        assert_eq!(
7896            store.object_count(),
7897            rows.len(),
7898            "the rebuild lost index rows"
7899        );
7900        eprintln!(
7901            "load {}; dropped and rebuilt: {} rows written, {} commits, tip closure {} objects",
7902            loadavg(),
7903            stats.written,
7904            store.commit_count(),
7905            closure.len(),
7906        );
7907    }
7908
7909    // ─────────────────────────────────────────────────────────────────────
7910    // lane ENGINE-OPT scaffold: a graded linear-history generator, the O(C)
7911    // negotiation timing test, and the cross-push memo-invalidation guard.
7912    // ─────────────────────────────────────────────────────────────────────
7913
7914    /// A linear commit history of `n` commits as a clone pack, optionally
7915    /// chained onto `start_parent` (an oid already in the store). Each commit
7916    /// carries its own one-entry tree and one unique blob, so the object count
7917    /// is `3n` and the graph is a single parent chain `n` deep — the shape that
7918    /// makes a negotiation's cost O(commits), which is what this lane memoizes.
7919    fn linear_history_pack_from(
7920        n: usize,
7921        start_parent: Option<&[u8]>,
7922    ) -> (Vec<u8>, Vec<Vec<u8>>) {
7923        use crate::object::canonical;
7924        use crate::pack_fixture::{build_pack, Item};
7925        let hash = GitHashKind::Sha1;
7926        let mut items: Vec<Item> = Vec::with_capacity(n * 3);
7927        let mut commits: Vec<Vec<u8>> = Vec::with_capacity(n);
7928        let mut prev: Option<Vec<u8>> = start_parent.map(|p| p.to_vec());
7929        for c in 0..n {
7930            let body = format!("blob for commit {c} of {n}, parent {prev:?}\n").into_bytes();
7931            let blob_oid = hash.oid_of(&canonical(GitObjectKind::Blob, &body));
7932            items.push(Item::Whole(GitObjectKind::Blob, body));
7933
7934            let mut tpayload = Vec::new();
7935            tpayload.extend_from_slice(b"100644 f.bin\0");
7936            tpayload.extend_from_slice(&blob_oid);
7937            let tree_oid = hash.oid_of(&canonical(GitObjectKind::Tree, &tpayload));
7938            items.push(Item::Whole(GitObjectKind::Tree, tpayload));
7939
7940            let mut cbody = format!("tree {}\n", hex::encode(&tree_oid));
7941            if let Some(parent) = &prev {
7942                cbody.push_str(&format!("parent {}\n", hex::encode(parent)));
7943            }
7944            cbody.push_str(
7945                "author A U Thor <a@example.com> 1700000000 +0000\n\
7946                 committer A U Thor <a@example.com> 1700000000 +0000\n\n\
7947                 commit\n",
7948            );
7949            let cbody = cbody.into_bytes();
7950            let commit_oid = hash.oid_of(&canonical(GitObjectKind::Commit, &cbody));
7951            items.push(Item::Whole(GitObjectKind::Commit, cbody));
7952            prev = Some(commit_oid.clone());
7953            commits.push(commit_oid);
7954        }
7955        (build_pack(&items), commits)
7956    }
7957
7958    /// The O(commits) negotiation cost, graded. `reachable` rebuilt the
7959    /// `by_commit` and graph maps on every call — O(C) regardless of how few
7960    /// objects the request touched — so the `root` query (closure ~1) isolates
7961    /// that map-build cost from the walk, and the `tip` query (full closure)
7962    /// pays both. Median of nine, min–max, with the loadavg of its own run.
7963    #[test]
7964    #[ignore = "perf measurement; run with --release --ignored --nocapture"]
7965    fn bench_reachable_negotiation() {
7966        for &n in &[48usize, 2_000, 20_000] {
7967            let dir = tmpdir(&format!("bench-reach-{n}"));
7968            let (pack, commits) = linear_history_pack_from(n, None);
7969            let store = GitStore::open(&dir, "alice").unwrap();
7970            store.put_pack(&pack).unwrap();
7971            store.wait_indexed();
7972            let root = commits.first().unwrap().clone();
7973            let tip = commits.last().unwrap().clone();
7974            // warm the reach bitmaps once so the loop times the negotiation, not
7975            // the one-time bitmap build.
7976            let _ = store.reachable(&[&tip], &[]).unwrap();
7977
7978            let reps = 9usize;
7979            let (mut root_t, mut tip_t) = (Vec::new(), Vec::new());
7980            for _ in 0..reps {
7981                let t = Instant::now();
7982                let _ = store.reachable(&[&root], &[]).unwrap();
7983                root_t.push(t.elapsed());
7984                let t = Instant::now();
7985                let _ = store.reachable(&[&tip], &[]).unwrap();
7986                tip_t.push(t.elapsed());
7987            }
7988            root_t.sort();
7989            tip_t.sort();
7990            let tip_closure = store.reachable(&[&tip], &[]).unwrap().len();
7991            eprintln!(
7992                "load {}; n={n}: reachable(root, closure~1) med {:?} [{:?}..{:?}]  |  \
7993                 reachable(tip, closure {}) med {:?} [{:?}..{:?}]",
7994                loadavg(),
7995                root_t[reps / 2],
7996                root_t[0],
7997                root_t[reps - 1],
7998                tip_closure,
7999                tip_t[reps / 2],
8000                tip_t[0],
8001                tip_t[reps - 1],
8002            );
8003        }
8004    }
8005
8006    /// **A negotiation answered from the memoized reach view is the answer a
8007    /// cold fold gives — including across a push that changes the fold.**
8008    ///
8009    /// Lane ENGINE-OPT memoizes the `by_commit` and graph lookup maps that
8010    /// `reachable` used to rebuild on every call, in `Derived`, folded by the
8011    /// same call that folds `commit_raw`. The bug this discipline exists to
8012    /// catch is a memo that SURVIVES a fold it should not: after a push adds
8013    /// commits, the warm store must answer with the new closure, byte-identical
8014    /// (order included — both spaces sort the same oids) to a fresh store
8015    /// opened cold on the same bytes.
8016    ///
8017    /// Seen RED by skipping the memo rebuild in `refold` (leaving `graph_index`
8018    /// from before the push): the post-push tip is then not a known commit,
8019    /// `reachable` contributes it alone, and the warm answer is a handful of
8020    /// objects where the cold fold gives the whole chain — the assertion names
8021    /// both lengths.
8022    #[test]
8023    fn reachable_memo_matches_a_cold_fold_across_a_push() {
8024        let dir = tmpdir("reach-memo-across-push");
8025        let (pack_a, commits_a) = linear_history_pack_from(40, None);
8026        let store = GitStore::open(&dir, "alice").unwrap();
8027        store.put_pack(&pack_a).unwrap();
8028        store.wait_indexed();
8029        let tip_a = commits_a.last().unwrap().clone();
8030        let warm_a = store.reachable(&[&tip_a], &[]).unwrap();
8031        assert!(
8032            warm_a.len() > 10,
8033            "A's closure is too small to tell a stale memo from a coincidence: {}",
8034            warm_a.len()
8035        );
8036
8037        // Push B: a second linear history chained ONTO A's tip, so B's tip
8038        // closes over A too. This is a fold that genuinely grows the graph.
8039        let (pack_b, commits_b) = linear_history_pack_from(40, Some(&tip_a));
8040        store.put_pack(&pack_b).unwrap();
8041        store.wait_indexed();
8042        let tip_b = commits_b.last().unwrap().clone();
8043        let warm_b_full = store.reachable(&[&tip_b], &[]).unwrap();
8044        let warm_b_minus_a = store.reachable(&[&tip_b], &[&tip_a]).unwrap();
8045        assert!(
8046            warm_b_full.len() > warm_a.len(),
8047            "the push did not grow the closure ({} !> {}), so the memo could be \
8048             stale and this test would not know",
8049            warm_b_full.len(),
8050            warm_a.len()
8051        );
8052
8053        // The cold oracle: a fresh handle on the same bytes, folded from disk.
8054        drop(store);
8055        let cold = GitStore::open(&dir, "alice").unwrap();
8056        cold.wait_indexed();
8057        let cold_b_full = cold.reachable(&[&tip_b], &[]).unwrap();
8058        let cold_b_minus_a = cold.reachable(&[&tip_b], &[&tip_a]).unwrap();
8059
8060        assert_eq!(
8061            warm_b_full, cold_b_full,
8062            "warm memo != cold fold for want=tip_b (a memo that survived the push)"
8063        );
8064        assert_eq!(
8065            warm_b_minus_a, cold_b_minus_a,
8066            "warm memo != cold fold for want=tip_b have=tip_a"
8067        );
8068        // tip_a is still answered identically after the push (its closure did
8069        // not change; only the store grew around it).
8070        let cold_a = cold.reachable(&[&tip_a], &[]).unwrap();
8071        assert_eq!(
8072            warm_a, cold_a,
8073            "tip_a's closure changed across the push"
8074        );
8075        eprintln!(
8076            "load {}; A closure {}, B closure {}, B−A {}",
8077            loadavg(),
8078            warm_a.len(),
8079            warm_b_full.len(),
8080            warm_b_minus_a.len(),
8081        );
8082    }
8083
8084    /// **The clone-after-push lever, witnessed.** A push folds; the fold used
8085    /// to drop the reachability table and the next reader rebuilt it by
8086    /// walking every commit's trees — 0.2 ms per object, 10 s on a 51k-object
8087    /// repository, measured 2026-09-05. Now the fold CARRIES the table
8088    /// (renumbered) when the graph grew by no more than the carry rule allows.
8089    /// RED before the carry: `reach_rebuilt` went 1 → 2 across the push.
8090    #[test]
8091    fn a_push_carries_the_reach_table_across_the_fold() {
8092        let dir = tmpdir("reach-carry-across-push");
8093        let (pack_a, commits_a) = linear_history_pack_from(40, None);
8094        let store = GitStore::open(&dir, "alice").unwrap();
8095        store.put_pack(&pack_a).unwrap();
8096        store.wait_indexed();
8097        let tip_a = commits_a.last().unwrap().clone();
8098        let warm_a = store.reachable(&[&tip_a], &[]).unwrap();
8099        assert_eq!(store.reach_source_stats(), (0, 1), "the first read builds the table once");
8100
8101        // A push of 30 commits onto A's tip: within the carry rule (64 or a
8102        // tenth of 40, whichever is larger), so the fold carries.
8103        let (pack_b, commits_b) = linear_history_pack_from(30, Some(&tip_a));
8104        store.put_pack(&pack_b).unwrap();
8105        store.wait_indexed();
8106        let tip_b = commits_b.last().unwrap().clone();
8107        let warm_b = store.reachable(&[&tip_b], &[]).unwrap();
8108        let warm_b_minus_a = store.reachable(&[&tip_b], &[&tip_a]).unwrap();
8109        assert_eq!(
8110            store.reach_source_stats(),
8111            (1, 1),
8112            "the fold carried the table; no read rebuilt it"
8113        );
8114        assert!(warm_b.len() > warm_a.len(), "the push grew the closure");
8115        assert_eq!(warm_b_minus_a.len(), warm_b.len() - warm_a.len(), "tip_b − tip_a is exactly the pushed objects");
8116        assert_eq!(store.reachable(&[&tip_a], &[]).unwrap(), warm_a, "tip_a's closure survived the renumbering");
8117
8118        // The cold oracle: the same bytes folded from disk with a fresh table.
8119        drop(store);
8120        let cold = GitStore::open(&dir, "alice").unwrap();
8121        cold.wait_indexed();
8122        assert_eq!(cold.reachable(&[&tip_b], &[]).unwrap(), warm_b, "carried != rebuilt for want=tip_b");
8123        assert_eq!(
8124            cold.reachable(&[&tip_b], &[&tip_a]).unwrap(),
8125            warm_b_minus_a,
8126            "carried != rebuilt for want=tip_b have=tip_a"
8127        );
8128        assert_eq!(cold.reach_source_stats(), (0, 1), "the cold handle built once and carried nothing");
8129    }
8130
8131    /// The carry rule's other arm: a push larger than the allowance drops the
8132    /// table and the next read rebuilds it whole — bounded staleness, never a
8133    /// walk that grows without limit.
8134    #[test]
8135    fn a_push_past_the_carry_allowance_rebuilds_the_reach_table() {
8136        let dir = tmpdir("reach-carry-rebuild");
8137        let (pack_a, commits_a) = linear_history_pack_from(20, None);
8138        let store = GitStore::open(&dir, "alice").unwrap();
8139        store.put_pack(&pack_a).unwrap();
8140        store.wait_indexed();
8141        let tip_a = commits_a.last().unwrap().clone();
8142        let _ = store.reachable(&[&tip_a], &[]).unwrap();
8143        assert_eq!(store.reach_source_stats(), (0, 1));
8144        // 100 commits > max(64, 20/10): over the allowance.
8145        let (pack_b, commits_b) = linear_history_pack_from(100, Some(&tip_a));
8146        store.put_pack(&pack_b).unwrap();
8147        store.wait_indexed();
8148        let tip_b = commits_b.last().unwrap().clone();
8149        let warm_b = store.reachable(&[&tip_b], &[]).unwrap();
8150        assert_eq!(store.reach_source_stats(), (0, 2), "over the allowance: not carried, rebuilt once more");
8151        drop(store);
8152        let cold = GitStore::open(&dir, "alice").unwrap();
8153        cold.wait_indexed();
8154        assert_eq!(cold.reachable(&[&tip_b], &[]).unwrap(), warm_b, "rebuilt warm != cold fold");
8155    }
8156
8157    /// ★ **A push alone makes the cache give space back at the disk line.**
8158    /// Owner 2026-09-28: the indexing thread should also de-cache.
8159    ///
8160    /// The lazy pool is filled into several segments under a 10 % disk, then
8161    /// the disk reads 85 % and a pack is pushed. No reader touches the store
8162    /// after that, so only the indexer can have started the cleanup.
8163    ///
8164    /// Seen RED by removing `self.exploded.kick()` from `absorb_one_inner`:
8165    /// the pool kept every segment after the push.
8166    #[test]
8167    fn a_push_past_the_disk_line_evicts_the_oldest_cache_without_a_read() {
8168        let dir = tmpdir("push-disk-line");
8169        let store = GitStore::open(&dir, "rickard").unwrap();
8170        let c = store.absorber.exploded.clone();
8171        c.set_global_line(0);
8172        c.set_segment_bytes_for_test(256 << 10);
8173        c.set_disk_reading_for_test(10);
8174        // Incompressible, deep enough to be admitted: the cache must hold it.
8175        let mut x: u32 = 0x9E37_79B9;
8176        let big: Vec<u8> = (0..64 << 10)
8177            .map(|_| {
8178                x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
8179                (x >> 24) as u8
8180            })
8181            .collect();
8182        let cost = crate::resolve::ReadCost {
8183            depth: crate::exploded_cache::CHEAP_DEPTH + 8,
8184            inflated: big.len() as u64,
8185            external_base: false,
8186        };
8187        for i in 0..24u32 {
8188            let mut oid = vec![0u8; 20];
8189            oid[..4].copy_from_slice(&i.to_be_bytes());
8190            c.write_back(&oid, GitObjectKind::Blob, &big, cost).unwrap();
8191            c.maintain().unwrap();
8192        }
8193        let before = c.use_now();
8194        assert!(before.segments >= 4, "precondition: rotated into segments, has {}", before.segments);
8195        assert_eq!(before.evicted_segments, 0, "precondition: nothing evicted under a 10 % disk");
8196
8197        c.set_disk_reading_for_test(85);
8198        let (pack, _) = one_blob_pack(b"pushed after the disk crossed its line\n");
8199        store.put(&pack, &[]).unwrap();
8200        store.wait_indexed();
8201        let deadline = Instant::now() + Duration::from_secs(10);
8202        while c.use_now().evicted_segments == 0 && Instant::now() < deadline {
8203            std::thread::sleep(Duration::from_millis(20));
8204        }
8205        let after = c.use_now();
8206        assert!(after.evicted_segments > 0, "the push did not make the cache give anything back");
8207        assert_eq!(after.segments, 1, "past the line the pool keeps only its live segment");
8208    }
8209}