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 /// How many folds CARRIED the reachability table across (renumbered, no
636 /// tree opened) vs how many reads had to build it from scratch. The only
637 /// witness that the carry ran — the bytes a carried and a rebuilt table
638 /// answer with are identical by construction (LAW 2). See
639 /// [`Absorber::reach_source_stats`].
640 reach_carried: std::sync::atomic::AtomicU64,
641 reach_rebuilt: std::sync::atomic::AtomicU64,
642 /// Where the last fold's **ordinal space** came from: how many oids straight
643 /// off the persisted index's column, how many off a scan of the redb tail.
644 /// The same shape as [`Self::graph_read`]/[`Self::graph_recomputed`], and for
645 /// the same reason: which source produced the space is not visible in the
646 /// (identical) bytes, only in a counter (LAW 2). See
647 /// [`Absorber::ordinal_source_stats`].
648 ordinal_folded: std::sync::atomic::AtomicU64,
649 ordinal_tail: std::sync::atomic::AtomicU64,
650 packs: Mutex<PackState>,
651 /// Serialises **absorption**, which is long. Held across a whole pack.
652 ///
653 /// Two absorbs of one pack are duplicated *work* and nothing worse: every
654 /// writer under it is keyed by oid — [`ObjectReadStack::append`] swallows an
655 /// identical second row, the exploded table overwrites its own key, and the
656 /// commit graph is folded from that table rather than pushed at, so it cannot
657 /// double. (It could before §14's table existed, when the graph was a `Vec`
658 /// accumulated per absorb; that is why this paragraph used to say the gate
659 /// was load-bearing for correctness and now does not.) The gate is still what
660 /// makes the ordering below hold: **the rows go in, and only then is the bit
661 /// cleared**, so a read either finds the bit still clear (and waits here) or
662 /// finds the rows. Neither order can produce "absent".
663 gate: Mutex<()>,
664 /// **The progress counter** ([`crate::progress`]) a dashboard or a stats
665 /// call reads. One per store, registered process-wide.
666 progress: Arc<crate::progress::IndexProgress>,
667 /// Set by [`GitStore::cancel_index`]: the repository is being deleted. The
668 /// resolver checks it every few thousand objects and aborts the pack; the
669 /// drain skips every pack still queued.
670 cancel: std::sync::atomic::AtomicBool,
671 /// Names the store in log lines: its root directory.
672 label: String,
673 /// The memory watchdog this store's worker pauses on. The process-wide one
674 /// unless a test hands in a fake ([`GitStore::set_mem_watchdog`]).
675 watchdog: RwLock<Arc<crate::memguard::MemWatchdog>>,
676 /// **Pending packs a reader has already scanned**, keyed by pack offset:
677 /// the walk and an oid-sorted row table, built by ONE read-only pass and
678 /// kept until the pack is absorbed. Every later lookup into that pack is
679 /// a binary search; a content read inflates one chain. Without this,
680 /// every `has` during the index window re-resolved the whole pack —
681 /// MEASURED on the rig 2026-08-30: a `push --mirror` of linux stalled
682 /// pre-ack, ~800 ref updates × one 8 GiB resolve each.
683 scanned: Mutex<HashMap<u64, Arc<ScannedPack>>>,
684 /// One scan build at a time: a second reader waits for the first reader's
685 /// pass rather than starting its own. That is waiting on a *reader*, not
686 /// on the indexer.
687 scan_build: Mutex<()>,
688 /// **Packs this store has stopped trying to index**, by ordinal, with how
689 /// many attempts it took to stop and the reason the last one gave.
690 ///
691 /// 🔴 The state this replaces was *no state*. A pack whose absorb failed
692 /// stayed pending, every absorb loop hit it FIRST (they run in ordinal
693 /// order) and aborted on it with `?`, so the packs behind it were never
694 /// reached — and the next push, and the one after that, re-did the same
695 /// doomed resolve and was refused for it. MEASURED on oden 2026-08-30
696 /// (lane AI): one pack that could not be absorbed refused **every** push to
697 /// that repository from then on, 21 of 140, and lane AD's standby re-asked
698 /// for it every 300 s for 21 minutes. Work that cannot succeed has to stop
699 /// and say why; see [`MAX_ABSORB_ATTEMPTS`] and [`Absorber::absorb_each`].
700 unindexable: Mutex<HashMap<u64, Unindexable>>,
701}
702
703/// A pack this store will not try to index again, and why.
704#[derive(Debug, Clone)]
705pub struct Unindexable {
706 /// The pack's ordinal.
707 pub pack_id: u64,
708 /// Its extent in the verbatim archive. The bytes are still there.
709 pub extent: Extent,
710 /// How many absorbs were spent before it stopped.
711 pub attempts: u32,
712 /// The last attempt's root cause — the backend's own sentence, which is
713 /// what a client and an operator both get told.
714 pub reason: String,
715}
716
717/// How many times one pack's absorb is attempted before the store stops.
718///
719/// Three, and the reason it is not one: an absorb can fail for a reason outside
720/// the pack — the memory watchdog gave up, the repository was being deleted, a
721/// transient read error — and a single bad moment must not condemn durable
722/// bytes. It is not thirty either, because the resolve is deterministic in the
723/// store's own state: a pack that failed three times with nothing else changing
724/// fails the fourth time for the same reason, and every one of those attempts
725/// is paid for by a client waiting on a push.
726pub const MAX_ABSORB_ATTEMPTS: u32 = 3;
727
728// **The pack this thread is inside an absorb of**, if any.
729//
730// `absorb_gated` holds `Absorber::gate` for the whole pack, and the resolver it
731// runs asks this very store for delta bases through `BaseSource::content`. That
732// re-entry must not take the gate again — a `std::sync::Mutex` is not
733// reentrant, so it would not be a wait, it would be a deadlock — and it must
734// not treat the pack it is halfway through as something it can absorb. One
735// `Cell` answers both questions, and it is the same guard shape
736// `scan_pending`'s `SCANNING` uses for the same reason.
737thread_local! {
738 static IN_ABSORB: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
739}
740
741/// Sets [`IN_ABSORB`] for the duration of one absorb and restores whatever was
742/// there before — absorbs nest (a pack whose base is in another pending pack).
743struct InAbsorb(Option<u64>);
744
745impl InAbsorb {
746 fn enter(pack_id: u64) -> Self {
747 InAbsorb(IN_ABSORB.with(|c| c.replace(Some(pack_id))))
748 }
749}
750
751impl Drop for InAbsorb {
752 fn drop(&mut self) {
753 IN_ABSORB.with(|c| c.set(self.0));
754 }
755}
756
757/// One pending pack, scanned read-only: what a lookup needs and nothing that
758/// the index will hold. O(objects) memory — for linux ~56 B of walk + 32 B of
759/// row per object, ~1.2 GB — and gone when the pack's bit goes up.
760///
761/// **Nothing on the production read path builds one of these any more.** A
762/// reader that needs an un-indexed pack indexes it instead
763/// ([`Absorber::index_pending_now`]); this remains for the one caller that
764/// cannot — a resolver already inside `absorb_gated`, which may consult a scan
765/// somebody else already built but must never start one.
766struct ScannedPack {
767 walk: crate::pack_walk::PackWalk,
768 /// `(oid, entry index, resolved size)`, sorted by oid.
769 rows: Vec<(Vec<u8>, u32, u64)>,
770 archive_offset: u64,
771}
772
773impl ScannedPack {
774 fn find(&self, oid: &[u8]) -> Option<(usize, u64)> {
775 self.rows
776 .binary_search_by(|(o, _, _)| o.as_slice().cmp(oid))
777 .ok()
778 .map(|i| (self.rows[i].1 as usize, self.rows[i].2))
779 }
780
781 fn row(&self, oid: &[u8]) -> Option<crate::index_layout::IndexRow> {
782 let (entry, size) = self.find(oid)?;
783 let e = &self.walk.entries[entry];
784 Some(crate::index_layout::IndexRow {
785 // Not ranked: the row is not in the index yet.
786 ordinal: u32::MAX,
787 offset: e.offset + self.archive_offset,
788 len: e.len,
789 obj_type: e.obj_type,
790 uncompressed_size: size,
791 delta_base: match &e.delta_base {
792 crate::pack_walk::DeltaBase::Offset(o) => o + self.archive_offset,
793 _ => 0,
794 },
795 })
796 }
797}
798
799/// One repository's git store.
800///
801/// | on disk | what it is | who owns the format |
802/// |---|---|---|
803/// | `<root>/objects.pack` | the verbatim pack bytes, appended | [`SafeWriter`] |
804/// | `<root>/objects.pack.journal` | the durable extent rows | [`SafeWriter`] |
805/// | `<root>/objects.tail` | the un-sealed index tail | [`ObjectReadStack`] (redb) |
806/// | `<root>/objects.exploded` | §14's resolved content, **droppable** | [`ExplodedArchive`] (one Arrow IPC table) |
807/// | `<root>/refs.log` | the ref transaction log | [`RefLog`] (Arrow IPC frames) |
808/// | `<root>/<name>.znippy` | the sealed archive GC compacts | base znippy |
809///
810/// Nothing in that table is new. The store is the composition, not a fifth
811/// format.
812/// # Which implementation of each trait, and how a caller picks
813///
814/// Three of the four boxes in the diagram at the top of this file have more than
815/// one implementation. [`StoreConfig`] is the choice, [`crate::arms`] documents
816/// what each arm promises, and the dispatch is split deliberately:
817///
818/// | trait | selected by | dispatch | cost |
819/// |---|---|---|---|
820/// | [`ObjectIndex`] | the type parameter `S` | **static** | none — the hot path is monomorphised |
821/// | [`ArchiveWrite`](crate::archive_write::ArchiveWrite) | [`StoreConfig::writer`] | `dyn`, inside [`PushPath`] as it already was | one virtual call **per pack append**, not per object |
822/// | [`Gc`] | [`StoreConfig::gc`] | `dyn` | one virtual call per `gc()` run |
823///
824/// `S` defaults to [`OneTableFourColumns`], so `GitStore` still names exactly
825/// what it named before and every existing caller compiles unchanged.
826pub struct GitStore<S: ObjectIndex = OneTableFourColumns> {
827 root: PathBuf,
828 /// The verbatim pack bytes. The **truth** (§14).
829 blobs: PathBuf,
830 /// The sealed znippy archive `gc` compacts. Only GC and `seal` touch it.
831 archive: PathBuf,
832 /// The selected [`ArchiveWrite`](crate::archive_write::ArchiveWrite) arm
833 /// plus the per-account index channel. The ack path.
834 push: PushPath,
835 account: String,
836 hash: GitHashKind,
837 /// The object-level index and the absorb behind it — **shared with the
838 /// account indexer's worker**, which is how a push ends in object rows
839 /// without a read having to ask for them.
840 absorber: Arc<Absorber<S>>,
841 refs: RefLog,
842 /// Serialises the ref log's read-compare-append. A CAS that read the
843 /// namespace, then appended, without holding this could be overtaken between
844 /// the two and would overwrite the update it did not see.
845 ref_gate: Mutex<()>,
846 /// The selected [`Gc`] arm. Built once, here, and run by
847 /// [`GitOps::gc`] — a `Box<dyn>` because a GC is a whole-archive
848 /// compaction that happens on a maintenance timer, so one virtual call
849 /// against it is unmeasurable and a third type parameter on every signature
850 /// in the crate is not free to read.
851 gc: Box<dyn Gc + Send + Sync>,
852 /// What this store was built with, for a bench row or a config dump. A
853 /// description of the arms already constructed — **nothing consults it on a
854 /// serving path**.
855 arms: StoreConfig,
856}
857
858impl GitStore<OneTableFourColumns> {
859 /// Open (creating) the store for one repository under `root`.
860 ///
861 /// **The default arms, and it reads no environment to get them.** An
862 /// operator who exported `ZNIPPY_GIT_WRITER=fast` does not change what this
863 /// builds — a caller that never asked for a selector must not have its
864 /// durability contract moved out from under it. Use
865 /// [`open_from_env`] when the selection is wanted.
866 ///
867 /// ⚠ One variable *is* read here, and it is not an arm:
868 /// `ZNIPPY_GIT_REDB_CACHE_BYTES` (see
869 /// [`crate::arms::redb_cache_bytes`]). It selects no implementation,
870 /// changes no durability contract and leaves the bytes on disk identical —
871 /// it caps redb's page cache, whose own default is 1 GiB *per database*, of
872 /// which this store opens two. The sentence above is about arms, and a
873 /// memory ceiling is not one.
874 pub fn open(root: &Path, account: &str) -> Result<Self> {
875 Self::open_with(root, account, GitHashKind::Sha1)
876 }
877
878 /// Same, with an explicit oid width. sha256 repositories exist and the store
879 /// does not get to assume otherwise; every oid it stores is
880 /// [`GitHashKind::oid_len`] bytes wide and the tail refuses a mixed batch.
881 ///
882 /// # What a reopen recovers, and why nothing had to be stored for it
883 ///
884 /// A store opened over an `objects.pack` some earlier process was still
885 /// indexing has to end up in the same state that process would have reached.
886 /// §13.12's `indexed` bit is what says so, and it is **derived here rather
887 /// than persisted**: a pack is unabsorbed *iff* its extent is in the journal
888 /// and its rows are not in the index, and both of those are already durable
889 /// (see [`Absorber::adopt_journal`]). The packs that come back clear are put
890 /// straight onto the account's channel, so the drain finishes the interrupted
891 /// work with no read having to ask — and until it does, every read falls back
892 /// exactly as it would have before the crash. **Slower, never wrong**, across
893 /// a restart as well as within one.
894 ///
895 /// The bits are set **before** the channel is fed, which is the same ordering
896 /// the ack path uses for the same reason: a read that arrives in between finds
897 /// the bit clear and waits, never an index that does not mention the pack.
898 ///
899 /// # What a reopen recovers of the DERIVED tables, and how that was fixed
900 ///
901 /// [`Derived::graph`] and [`Derived::trees`] come back **in full**, for every
902 /// pack, whether or not anything is re-queued. They are folded from §14's
903 /// exploded table ([`Absorber::refold`]), which is on disk, so a clean
904 /// shutdown carries them across.
905 ///
906 /// This was a live correctness hole until 2026-08-08 and it is recorded
907 /// rather than quietly repaired. The graph was *accumulated in RAM* as packs
908 /// were absorbed and stored nowhere; a store killed mid-absorb got it back
909 /// because the interrupted pack was re-queued and re-resolved, but a store
910 /// that shut down **cleanly** re-queued nothing and came back with an empty
911 /// graph. MEASURED on oden 2026-08-08: 2687 rows and **0 of 551 commits**,
912 /// `reachable` on a commit returning that commit alone instead of its
913 /// closure — a *quiet* wrong answer — and `gc` seeing an empty live set. The
914 /// fix is the table, because the payloads the graph is folded from had no
915 /// home; [`tests::a_clean_shutdown_and_reopen_keeps_the_graph_and_reachable`]
916 /// is what proves it, over a real process that exits normally.
917 ///
918 /// The exploded table is still **droppable**: if its row count is short of
919 /// the `objects` table's, [`Absorber::adopt_journal`] declares no pack
920 /// absorbed and every one of them is re-queued and re-exploded. Absent means
921 /// rebuild, never wrong (§13.12, applied unchanged to a derived table).
922 ///
923 /// **Also the default arms, and also no environment read** — see
924 /// [`open`](GitStore::open).
925 pub fn open_with(root: &Path, account: &str, hash: GitHashKind) -> Result<Self> {
926 // The default ARMS, and the environment's CEILING: the one variable
927 // this constructor reads, because an operator capping a server's
928 // footprint must not have to pick a different constructor to do it.
929 // See [`crate::arms::redb_cache_bytes`].
930 Self::open_with_arms(
931 root,
932 account,
933 hash,
934 StoreConfig::DEFAULT.with_redb_cache_bytes(crate::arms::redb_cache_bytes()?),
935 )
936 }
937}
938
939/// Where a stored entry's delta base sits, relative to the request being served.
940///
941/// The `Outside` arm carries the base in **both** namings because the two delta
942/// forms know only one each — an `OFS_DELTA` its archive offset, a `REF_DELTA`
943/// its oid — and [`GitStore::emit_set`]'s thin arm has to ask "does the client
944/// hold this" in whichever one the entry speaks.
945enum BaseOf {
946 /// Not a delta at all. There is nothing to place it after and nothing to
947 /// re-head; the stored entry is copied as it stands.
948 Whole,
949 /// A delta whose base **is** in the request, at this archive offset.
950 ///
951 /// The offset is carried rather than recomputed because a `REF_DELTA` does
952 /// not know it — it names its base by oid, and its own `delta_base` column
953 /// is `0` by construction — and the emitter needs it twice: as the
954 /// `EmitEntry::delta_base` that [`crate::pack_walk::topological_order`]
955 /// orders on, and as the base an in-pack ref-delta is re-headed to point at.
956 Inside { at: u64 },
957 /// A delta whose base the request does not carry.
958 Outside {
959 /// The base's archive offset, `0` when the entry named an oid instead.
960 offset: u64,
961 /// The base's oid, `None` when the entry named an offset instead — or
962 /// when the entry is too short to read one out of.
963 oid: Option<Vec<u8>>,
964 },
965}
966
967/// The bases a **thin** pack may name without carrying them: what the receiver
968/// already held before this transfer, keyed both ways.
969///
970/// Built by [`GitStore::client_bases`], which is where the argument for trusting
971/// it is written down.
972struct ExternalBases {
973 /// The held object's oid, keyed by its archive offset — the coordinate an
974 /// `OFS_DELTA` names its base in, and the only reason this map exists.
975 by_offset: HashMap<u64, OidKey>,
976 /// The same objects keyed the way a `REF_DELTA` names them.
977 held: std::collections::HashSet<OidKey>,
978}
979
980/// **A raw oid as a fixed-size, `Copy` hash key — 20 bytes for SHA-1, 32 for
981/// SHA-256, and no heap allocation either way.**
982///
983/// The two maps in [`ExternalBases`] are repository-sized: the client's `have`
984/// closure on an incremental fetch is very nearly every object in the store.
985/// Keying them by `Vec<u8>` is one heap allocation per entry per request —
986/// 12 112 of them for a 69-object fetch, measured on oden 2026-08-15 — for a
987/// question (*does the client hold this base?*) that never looks at more than
988/// `oid_len` bytes.
989///
990/// Padded to the widest hash rather than parameterised by width: a store holds
991/// exactly one hash kind, so every key in one set has the same real length and
992/// two different oids can never collide through the padding. The length is
993/// carried anyway, because a key that silently compared equal across widths is
994/// the kind of wrong that only shows up in a mixed-hash mirror.
995#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
996struct OidKey {
997 bytes: [u8; 32],
998 len: u8,
999}
1000
1001impl OidKey {
1002 /// The key for `oid`, or a refusal for one wider than any git hash.
1003 fn new(oid: &[u8]) -> Result<Self> {
1004 let len = oid.len();
1005 if len == 0 || len > 32 {
1006 bail!("{len} is not the width of any git object id (1..=32 bytes)");
1007 }
1008 let mut bytes = [0u8; 32];
1009 bytes[..len].copy_from_slice(oid);
1010 Ok(OidKey {
1011 bytes,
1012 len: len as u8,
1013 })
1014 }
1015
1016 /// The oid back, at its real width.
1017 fn as_slice(&self) -> &[u8] {
1018 &self.bytes[..self.len as usize]
1019 }
1020}
1021
1022impl<S: ObjectIndex + 'static> GitStore<S> {
1023 /// **The one constructor**, with every arm named.
1024 ///
1025 /// `S` picks the [`ObjectIndex`] layout at compile time; `arms` picks the
1026 /// [`ArchiveWrite`](crate::archive_write::ArchiveWrite) and [`Gc`]
1027 /// implementations at run time. [`open`](GitStore::open) and
1028 /// [`open_with`](GitStore::open_with) are this function with
1029 /// [`StoreConfig::DEFAULT`], which is why nothing about them moved.
1030 ///
1031 /// `arms.index` is **not** consulted here — `S` is the index arm, and a
1032 /// value that disagreed with the type would be a second source of truth.
1033 /// [`open_from_env`] is the one place the two are tied together, and it does
1034 /// it by choosing `S` from the value.
1035 ///
1036 /// ```no_run
1037 /// use znippy_plugin_git::arms::{GcArm, StoreConfig, WriterArm};
1038 /// use znippy_plugin_git::index_layout::PackedPayload;
1039 /// use znippy_plugin_git::{GitHashKind, GitStore};
1040 ///
1041 /// // A rebuildable import mirror: the ceiling writer, the packed payload
1042 /// // layout, and an in-place compaction.
1043 /// let arms = StoreConfig::DEFAULT
1044 /// .with_writer(WriterArm::Fast)
1045 /// .with_gc(GcArm::CompactInPlace);
1046 /// let store = GitStore::<PackedPayload>::open_with_arms(
1047 /// std::path::Path::new("/srv/repo"),
1048 /// "rickard",
1049 /// GitHashKind::Sha1,
1050 /// arms,
1051 /// )?;
1052 /// # Ok::<(), anyhow::Error>(())
1053 /// ```
1054 pub fn open_with_arms(
1055 root: &Path,
1056 account: &str,
1057 hash: GitHashKind,
1058 arms: StoreConfig,
1059 ) -> Result<Self> {
1060 std::fs::create_dir_all(root)
1061 .with_context(|| format!("creating the store root {}", root.display()))?;
1062 let blobs = root.join("objects.pack");
1063 let writer = arms.writer.create(&blobs)?;
1064 // `None` for an arm that keeps no durable ack log — see
1065 // [`crate::arms::WriterArm::journal`]. Everything below that touches the
1066 // journal is written against that `Option` rather than assuming one.
1067 let journal = arms.writer.journal(&blobs);
1068 // **The redb ceiling**, carried in on the config rather
1069 // than read here: `StoreConfig::from_env` reads it with the arms, and
1070 // `open`/`open_with` put the environment's value on `DEFAULT` before
1071 // getting here, so every constructor still honours the variable and
1072 // `arms.to_string()` prints what was actually applied. Not an arm — see
1073 // [`crate::arms::redb_cache_bytes`] — redb's own default is 1 GiB *per
1074 // database*, and this store opens exactly ONE of them, the
1075 // `ObjectReadStack` below. The `let _ = cache_bytes` two lines down is
1076 // what is left of the second.
1077 let cache_bytes = arms.redb_cache_bytes;
1078 let objects = ObjectReadStack::<S>::open(
1079 &root.join("objects.tail"),
1080 RebuildTriggers::default(),
1081 cache_bytes,
1082 )?;
1083 let _ = cache_bytes; // no page cache to size: the blob is pread, the index is a slice
1084 // A store written before 2026-09-04 carries `objects.ordinal`, the memo
1085 // of the sorted oid list the read stack's persisted index now IS. Its
1086 // bytes are never read again, so it goes on the first open by this
1087 // build — 278 964 902 B on the kernel mirror. Absent is the normal case
1088 // and not an event; anything else that stops the delete is logged, not
1089 // fatal, because the store does not depend on the file being gone.
1090 match std::fs::remove_file(root.join(ORDINAL_CACHE_FILE)) {
1091 Ok(()) => tracing::info!(
1092 target: "znippy.git",
1093 path = %root.join(ORDINAL_CACHE_FILE).display(),
1094 "removed a stale objects.ordinal — the persisted index's oid column is the \
1095 ordinal space now"
1096 ),
1097 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
1098 Err(e) => tracing::warn!(
1099 target: "znippy.git",
1100 path = %root.join(ORDINAL_CACHE_FILE).display(),
1101 error = %e,
1102 "could not remove a stale objects.ordinal; it is dead weight, not a fault"
1103 ),
1104 }
1105 let exploded = Arc::new(ExplodedCache::open(
1106 &root.join("objects.exploded"),
1107 crate::exploded_arrow::ExplodePolicy::from_env()?,
1108 crate::exploded_cache::budget_from_env(root)?,
1109 )?)
1110 .registered();
1111 // **One descriptor on the blob file, not three.** The writer opened it
1112 // read/write and `flock`ed it; the absorber's `pread` fallback and the
1113 // index workers' `read_exact_at` both address it positionally, so they
1114 // take the same open file description rather than two more of their
1115 // own. 5 descriptors per open store → 3, which is the divisor of
1116 // gunnar's `open_repo_budget` (`tests/fd_per_store.rs`).
1117 let absorber = Arc::new(Absorber::<S>::open(
1118 &blobs,
1119 writer.blobs_shared(),
1120 hash,
1121 objects,
1122 exploded,
1123 )?);
1124
1125 // **The `indexed` bit, derived.** Every extent this archive has ever
1126 // acked, diffed against the rows the index holds. `SafeWriter::create`
1127 // above appends to that journal and never truncates it, which is what
1128 // makes the older extents still be here to diff against.
1129 //
1130 // An arm with no journal has nothing to derive the bit from, and that is
1131 // the honest consequence of choosing it rather than a gap: a
1132 // `FastWriter` store has no durable record that a pack was acked, so a
1133 // reopen re-queues nothing and there is nothing for it to re-queue.
1134 let acked = match journal.as_deref() {
1135 Some(p) if p.exists() => crate::archive_write::read_journal(p).with_context(|| {
1136 format!(
1137 "reading {} — it is the durable half of the indexed bit and a store \
1138 cannot be opened without knowing which packs it owes index work for",
1139 p.display()
1140 )
1141 })?,
1142 _ => Vec::new(),
1143 };
1144 let requeue = absorber.adopt_journal(&acked)?;
1145
1146 // **The wiring.** The absorber goes into the push path, which hands it to
1147 // every account indexer it starts, so the drain behind `push_pack` ends
1148 // in object rows instead of one pack row. Nothing about the ack changes:
1149 // `append` still returns after the two fsyncs and this is all downstream
1150 // of that (§13.9).
1151 let push = PushPath::with_absorber(
1152 writer,
1153 &blobs,
1154 journal,
1155 absorber.clone() as Arc<dyn ObjectAbsorb>,
1156 )?;
1157 let refs = RefLog::new(root.join("refs.log"));
1158 let archive = root.join("repository.znippy");
1159
1160 let store = Self {
1161 root: root.to_path_buf(),
1162 blobs,
1163 archive,
1164 push,
1165 account: account.to_string(),
1166 hash,
1167 absorber,
1168 refs,
1169 ref_gate: Mutex::new(()),
1170 gc: arms.gc.create(),
1171 arms,
1172 };
1173 // The interrupted work, back on the channel it fell off. Nothing reads
1174 // here and nothing blocks: this is the same 24-byte handoff a push makes,
1175 // and the drain picks it up on its own thread (§13.9-11).
1176 let indexer = store.push.indexer(&store.account);
1177 for p in &requeue {
1178 indexer.submit(IndexJob {
1179 pack_id: p.pack_id,
1180 offset: p.offset,
1181 len: p.len,
1182 })?;
1183 }
1184 // A store reopened over an existing tail already knows its objects; the
1185 // derived tables are rebuilt from them rather than from a side-car,
1186 // because a side-car is a second truth.
1187 store.refold()?;
1188 Ok(store)
1189 }
1190
1191 /// The arms this store was built with. A description of what was
1192 /// constructed, not a switch — nothing on a serving path reads it.
1193 pub fn arms(&self) -> StoreConfig {
1194 self.arms
1195 }
1196
1197 /// The selected writer's name, as it goes on a bench row.
1198 pub fn writer_name(&self) -> &'static str {
1199 self.push.name()
1200 }
1201
1202 /// What the selected writer's `append` actually promises. Printed next to a
1203 /// throughput, because a throughput without it is not a comparison.
1204 pub fn writer_durability(&self) -> &'static str {
1205 self.push.durability()
1206 }
1207
1208 /// The selected [`Gc`] arm.
1209 pub(crate) fn gc_arm(&self) -> &dyn Gc {
1210 self.gc.as_ref()
1211 }
1212
1213 /// Where the verbatim packs are.
1214 pub fn blobs_path(&self) -> &Path {
1215 &self.blobs
1216 }
1217
1218 /// The sealed archive GC operates on.
1219 pub fn archive_path(&self) -> &Path {
1220 &self.archive
1221 }
1222
1223 pub fn root(&self) -> &Path {
1224 &self.root
1225 }
1226
1227 pub fn hash_kind(&self) -> GitHashKind {
1228 self.hash
1229 }
1230
1231 /// The `objects` table, for a caller that wants the index directly (the
1232 /// bench does).
1233 pub fn index(&self) -> &ObjectReadStack<S> {
1234 &self.absorber.objects
1235 }
1236
1237 /// Objects in the `objects` table.
1238 pub fn object_count(&self) -> usize {
1239 self.absorber.objects.len()
1240 }
1241
1242 /// Commits in the graph.
1243 pub fn commit_count(&self) -> usize {
1244 self.absorber
1245 .derived
1246 .read()
1247 .expect("derived lock")
1248 .graph
1249 .len()
1250 }
1251
1252 /// `(graph_read_from_disk, graph_recomputed)` since this store was opened —
1253 /// how many folds read the persisted `objects.graph` vs re-derived it.
1254 pub fn graph_source_stats(&self) -> (u64, u64) {
1255 self.absorber.graph_source_stats()
1256 }
1257
1258 /// `(reach_carried, reach_rebuilt)` since this store was opened — how many
1259 /// folds carried the reachability table across (renumbered, no tree
1260 /// opened) vs how many reads built it from scratch. The witness for the
1261 /// clone-after-push lever; see `REACH_CARRY_MAX_FRACTION`.
1262 pub fn reach_source_stats(&self) -> (u64, u64) {
1263 self.absorber.reach_source_stats()
1264 }
1265
1266 /// `(folded_rows, tail_rows)` of the ordinal space the last fold built — how
1267 /// many oids came off the persisted index's oid column and how many off a
1268 /// scan of the redb tail. Replaces S-082's `(read, recomputed)`: there is no
1269 /// memo to read any more, the index IS the space.
1270 pub fn ordinal_source_stats(&self) -> (u64, u64) {
1271 self.absorber.ordinal_source_stats()
1272 }
1273
1274 /// The folded ordinal space — every oid in ordinal order, hex. For the tests
1275 /// that assert a memo read reproduces the scanned space exactly (same oids,
1276 /// same order, same ordinals — the ordinal is the index into this vector).
1277 ///
1278 /// Formatted on demand from `oids_raw`: there is no resident hex `Vec` any
1279 /// more (S-082), and a test that wants hex encodes the flat buffer here
1280 /// rather than the store carrying a second copy for it.
1281 #[cfg(test)]
1282 pub(crate) fn ordinal_oids(&self) -> Vec<String> {
1283 let oid_len = self.hash.oid_len();
1284 self.absorber
1285 .derived
1286 .read()
1287 .expect("derived lock")
1288 .oids_raw
1289 .chunks_exact(oid_len)
1290 .map(hex::encode)
1291 .collect()
1292 }
1293
1294 /// **One object's resolved content** — §14's exploded table, in one lookup.
1295 ///
1296 /// Not one of the twelve and deliberately not on [`GitOps`]: the twelve are
1297 /// the wire vocabulary and they hand back **stored** bytes ([`GitOps::get`]),
1298 /// which for a delta entry is a delta. This is the other question — *what
1299 /// does this object contain* — and it is the one §14's table exists to make
1300 /// a point lookup instead of a chain walk.
1301 ///
1302 /// It absorbs any pending pack first, for the same reason
1303 /// [`GitOps::has`] does: a store with durable bytes it has not indexed cannot
1304 /// answer "absent" without lying.
1305 pub fn content(&self, oid: Oid<'_>) -> Result<Option<(GitObjectKind, Vec<u8>)>> {
1306 // No absorb here any more: a pack the worker has not reached is read
1307 // for this one oid by `Absorber::resolved`'s third arm — on this thread,
1308 // from the mmapped pack, without waiting for the worker.
1309 self.absorber.resolved(oid)
1310 }
1311
1312 /// **One object, with the bill for producing it.**
1313 ///
1314 /// [`Self::content`] plus what the read cost: `None` for a cost when the
1315 /// exploded pools answered (nothing was rebuilt), `Some(cost)` when the
1316 /// object was inflated out of the pack. What a measurement needs and a
1317 /// server does not — the cache admission decision is made inside, not by
1318 /// the caller.
1319 pub fn content_costed(
1320 &self,
1321 oid: Oid<'_>,
1322 ) -> Result<Option<(GitObjectKind, Vec<u8>, Option<crate::resolve::ReadCost>)>> {
1323 self.absorber.resolved_costed(oid)
1324 }
1325
1326 /// §14's table, counted: rows, rows written, reads it served, reads that fell
1327 /// through to re-deriving from the verbatim truth.
1328 ///
1329 /// Applied output. Both content paths return identical bytes, so `served`
1330 /// against `rederived` is the only thing that can say which one ran.
1331 pub fn exploded_stats(&self) -> ExplodedStats {
1332 self.absorber.exploded.engine_stats()
1333 }
1334
1335 /// **What the exploded tier is holding and what it is allowed to hold** —
1336 /// the row a dashboard prints (`explode 3.1G/8G`) and `Admin.StorageInfo`
1337 /// carries, per repository.
1338 pub fn explode_cache_use(&self) -> crate::exploded_cache::CacheUse {
1339 self.absorber.exploded.use_now()
1340 }
1341
1342 /// Rows in the EAGER pool only — the graph the push wrote, with nothing a
1343 /// reader has since cached. What `adopt_journal` compares against the index
1344 /// and what a test asking "did the push explode the graph" must ask,
1345 /// because [`Self::exploded_stats`]'s `rows` counts both pools and a read
1346 /// moves it.
1347 pub fn exploded_eager_rows(&self) -> Result<u64> {
1348 self.absorber.exploded.eager_rows()
1349 }
1350
1351 /// **Set this repository's explode budget** — what the control plane calls
1352 /// when an account's quota changes. Takes effect at the next rotation; it
1353 /// never refuses a read, never refuses a write and never deletes on the
1354 /// caller's thread. A bigger budget buys a bigger cache and therefore faster
1355 /// first reads; a smaller one costs re-inflation, never an error.
1356 pub fn set_explode_budget(&self, bytes: u64) {
1357 self.absorber.exploded.set_budget(bytes);
1358 }
1359
1360 /// Flush the lazy pool, rotate it and evict it back under budget — on
1361 /// **this** thread. What a test and a `gc` call; a reader never does, it
1362 /// starts a write-back thread and walks away.
1363 pub fn explode_cache_maintain(&self) -> Result<u64> {
1364 self.absorber.exploded.maintain()
1365 }
1366
1367 /// **`repo gc` reclaims the cache**: every lazy segment unlinked, the graph
1368 /// pool kept. Returns the bytes that left the filesystem.
1369 pub fn drop_explode_cache(&self) -> Result<u64> {
1370 self.absorber.exploded.drop_cache()
1371 }
1372
1373 /// Where §14's table lives. Deleting this file while no store holds it is the
1374 /// sanctioned way to drop it; the next open re-explodes every pack.
1375 pub fn exploded_path(&self) -> &Path {
1376 self.absorber.exploded.path()
1377 }
1378
1379 /// Every live row of one kind, with its payload — what the graph fold reads,
1380 /// exposed so a measurement can ask **the table** how many bytes it holds
1381 /// rather than dividing a file size by what the pack claimed.
1382 pub fn exploded_of_kind(&self, kind: GitObjectKind) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
1383 self.absorber.exploded.of_kind(kind)
1384 }
1385
1386 /// The commit graph as it stands, generations included. A snapshot: the
1387 /// caller holds no lock and the next fold replaces it wholesale.
1388 pub fn graph_snapshot(&self) -> Vec<CommitNode> {
1389 self.absorber
1390 .derived
1391 .read()
1392 .expect("derived lock")
1393 .graph
1394 .clone()
1395 }
1396
1397 /// Packs whose objects are not in the index yet — §13.12's bit, counted.
1398 pub fn unindexed_packs(&self) -> usize {
1399 self.absorber.unindexed_packs()
1400 }
1401
1402 /// This account's indexer: the thread that drains the channel a push queues
1403 /// on. `is_indexed(pack_id)` on it is §13.12's bit at the pack level, and it
1404 /// is set only once that pack's **objects** are in the index.
1405 pub fn indexer(&self) -> std::sync::Arc<crate::indexer::AccountIndexer> {
1406 self.push.indexer(&self.account)
1407 }
1408
1409 /// Block until the background drain has absorbed everything this store has
1410 /// pushed. What a maintenance tick, a seal or a test uses instead of racing
1411 /// it.
1412 pub fn wait_indexed(&self) {
1413 self.push.pool().wait_caught_up();
1414 }
1415
1416 /// **Stop this store's index work — the repository is being deleted.**
1417 ///
1418 /// Sets the cancel flag the resolver polls every few thousand objects and
1419 /// the drain checks before every pack. The pack in flight is **aborted at
1420 /// its next checkpoint** (milliseconds), not finished: its rows are keyed
1421 /// by oid and the exploded tier is droppable, and the whole directory is
1422 /// about to be removed anyway. Every pack still queued is skipped. After
1423 /// this the store can be dropped without the drop joining a worker that is
1424 /// hours into a kernel pack — which is what `Drop for AccountIndexer` would
1425 /// otherwise do.
1426 pub fn cancel_index(&self) {
1427 self.absorber.cancel.store(true, std::sync::atomic::Ordering::SeqCst);
1428 self.push.pool().cancel_all();
1429 }
1430
1431 /// Put a different memory watchdog under this store's worker — a test's
1432 /// fake `/proc/meminfo`. Takes effect from the next pack.
1433 pub fn set_mem_watchdog(&self, dog: Arc<crate::memguard::MemWatchdog>) {
1434 *self.absorber.watchdog.write().unwrap_or_else(|e| e.into_inner()) = dog;
1435 }
1436
1437 /// Has [`cancel_index`](Self::cancel_index) been called?
1438 pub fn index_cancelled(&self) -> bool {
1439 self.absorber.cancel.load(std::sync::atomic::Ordering::SeqCst)
1440 }
1441
1442 /// This store's index progress counter, for a dashboard or a stats call.
1443 pub fn index_progress(&self) -> crate::progress::Snapshot {
1444 self.absorber.progress.snapshot()
1445 }
1446
1447 /// **The indexer's half of the split**, and not one of the twelve.
1448 ///
1449 /// Runs on the account indexer's worker (§13.9), and on a read that arrives
1450 /// before that worker got there — the same function either way, which is why
1451 /// there is no such thing as a background index that behaves differently from
1452 /// the fallback one.
1453 pub fn absorb_pending(&self) -> Result<usize> {
1454 self.absorber.absorb_pending()
1455 }
1456
1457 /// **Make every durable object addressable, on this thread, now** —
1458 /// [`git_storage_trait::GitServe::index_now`]'s implementation and the ack
1459 /// path's one call. See [`Absorber::index_pending_now`] for why the
1460 /// connectivity check is the only caller allowed to wait here.
1461 pub fn index_now(&self) -> Result<usize> {
1462 self.absorber.index_pending_now()
1463 }
1464
1465 /// **The packs this store has stopped trying to index**, with the reason
1466 /// each one stopped for.
1467 ///
1468 /// Empty is the ordinary answer and the only good one. A non-empty list is
1469 /// a repository that is missing objects whose bytes are durable, and it is
1470 /// the fact an operator needs before wondering why a push was refused or a
1471 /// standby is behind — see [`MAX_ABSORB_ATTEMPTS`].
1472 pub fn unindexable_packs(&self) -> Vec<Unindexable> {
1473 self.absorber.unindexable_packs()
1474 }
1475
1476 /// **The object ids a stored pack introduced**, and not one of the twelve.
1477 ///
1478 /// [`GitOps::put_pack`] cannot answer this and never will: the walk it does
1479 /// reads pack *entries*, and a pack entry does not carry its oid — that is
1480 /// why [`crate::pack_walk::Closure`] can only name the `REF_DELTA` bases and
1481 /// says so. The oids exist once the indexer has absorbed the pack, so this
1482 /// absorbs first rather than racing the drain, and is therefore the one
1483 /// caller that deliberately pays §13.9's deferral back.
1484 ///
1485 /// **Only call it when something needs the set.** A push whose policy has no
1486 /// gate over objects must not pay this, which is the whole reason it is a
1487 /// separate call and not a field on `TxId`.
1488 ///
1489 /// The extent *is* the identity: §14 stores a pack verbatim as one
1490 /// contiguous append, so "introduced by this pack" and "indexed at an offset
1491 /// inside this pack's extent" are the same set, with no pack column to keep
1492 /// in step. Bases the push deltas against live in *other* extents and are
1493 /// excluded by construction — which is what a thin push needs, and what
1494 /// reading a written `.idx` back could never do.
1495 ///
1496 /// Cost is one pass over the index, so it is O(objects in the repository),
1497 /// not O(objects pushed). Recorded rather than hidden: a per-pack ordinal
1498 /// range would make it O(pushed), and needs the indexer to keep one.
1499 pub fn oids_in_extent(&self, extent: Extent) -> Result<Vec<Vec<u8>>> {
1500 let (start, len) = extent;
1501 // **Never waits on the indexer.** If the gate is free this thread
1502 // absorbs the pending packs (the same work, done now, by the caller
1503 // that needs it); if the worker holds it, the extent is resolved
1504 // read-only on this thread and its oids are the answer — the same
1505 // resolver, no sink, no rows written.
1506 if self.try_absorb_pending()?.is_none() {
1507 let snap = self.archive_snapshot()?;
1508 let bytes = self.extent(&snap, start, len)?;
1509 let walked = crate::pack_walk::walk(&bytes, self.hash.oid_len())?;
1510 let mut oids: Vec<Vec<u8>> = Vec::with_capacity(walked.entries.len());
1511 let limits = crate::resolve::ResolveLimits {
1512 base_budget: (crate::resolve::base_budget_from_env() / 4).max(1),
1513 ..crate::resolve::ResolveLimits::none()
1514 };
1515 crate::resolve::resolve_streaming(
1516 &bytes,
1517 &walked,
1518 self.hash,
1519 start,
1520 &*self.absorber,
1521 &crate::exploded::NoSink,
1522 limits,
1523 &mut |rows| {
1524 oids.extend(rows.drain(..).map(|r| r.oid));
1525 Ok(())
1526 },
1527 )
1528 .context("resolving the pushed pack read-only to enumerate its objects")?;
1529 return Ok(oids);
1530 }
1531 let end = start.saturating_add(len);
1532 let all = self.index().oids_in_order()?;
1533 let refs: Vec<&[u8]> = all.iter().map(Vec::as_slice).collect();
1534 let extents = self.index().extents_batch(&refs);
1535 Ok(all
1536 .into_iter()
1537 .zip(extents)
1538 .filter_map(|(oid, at)| match at {
1539 Some((offset, _)) if offset >= start && offset < end => Some(oid),
1540 _ => None,
1541 })
1542 .collect())
1543 }
1544
1545 /// **Everything needed to emit a pack for EXACTLY `oids`** — nothing added,
1546 /// nothing dropped.
1547 ///
1548 /// Not one of the twelve. It sits beside [`absorb_pending`](Self::absorb_pending)
1549 /// for the same reason those do: it is about how storage is *shaped*, not
1550 /// about what a caller stores or reads.
1551 ///
1552 /// # 🔴 It used to add the delta bases, and that shipped broken clones
1553 ///
1554 /// Until 2026-08-11 this closed the request over its delta bases: an
1555 /// `OFS_DELTA` names its base by position, so a base outside the set cannot
1556 /// be encoded, and *adding the base* looked like the answer that is always
1557 /// safe. It is not, and the failure is silent on the server:
1558 ///
1559 /// ```text
1560 /// git clone --bare --single-branch --branch base <url>
1561 /// fatal: did not receive expected object 8601ec33920b7d701c9887fa04916501136d6e90
1562 /// fatal: fetch-pack: invalid index-pack output
1563 /// event="git.upload_pack.served" objects=33773 ← the server said SUCCESS
1564 /// ```
1565 ///
1566 /// Measured on the `h2h-linear-sha1-2048c-1024f-16k` fixture: `base` reaches
1567 /// **31 805** objects, the delta-base closure added **1 968** more — 33 773,
1568 /// exactly the count the server logged — and 507 of those additions were
1569 /// **trees**. A tree that is not reachable from the wants drags its own
1570 /// children in as a *requirement*, because `git index-pack
1571 /// --check-self-contained-and-connected` (which is what a clone runs, and
1572 /// which sets `strict`) walks every received object's links and then demands
1573 /// each one exist. Those 507 trees named **204** objects the pack did not
1574 /// contain. `8601ec33…` is one of them.
1575 ///
1576 /// So a delta base pulled into a narrowed request is not free: it satisfies
1577 /// the *pack format* and violates *connectivity*. Closing over the added
1578 /// bases' children in turn does terminate — the same fixture reaches a fixed
1579 /// point at 34 534 objects — but it is an over-send by construction, and on
1580 /// a `--filter=blob:none` fetch it adds back exactly the blobs the filter
1581 /// excluded, which is the defect
1582 /// `emit_pack_emits_exactly_the_set_it_is_given_and_never_its_closure`
1583 /// exists to forbid.
1584 ///
1585 /// # What it does instead: the base decides how the entry is encoded
1586 ///
1587 /// The set is never widened. For each requested object:
1588 ///
1589 /// * **base inside the request** → the stored entry is copied byte for byte,
1590 /// which is the whole point of this engine and is what a full clone does
1591 /// for every single object;
1592 /// * **base outside the request** → the entry is **rebuilt** from §14's
1593 /// resolved content and marked
1594 /// [`EmitEntry::recompressed`](crate::pack_walk::EmitEntry::recompressed) —
1595 /// as a computed delta against a base the request *does* carry where one
1596 /// can be found (see [`Self::in_request_chain_ancestor`] and
1597 /// [`crate::delta`]), and whole where it cannot.
1598 ///
1599 /// That is what stock `pack-objects` does — it reuses a stored delta only
1600 /// when the base is also being packed — and it keeps the receipt a fact:
1601 /// `copied` and `recompressed` add up to the objects written, and
1602 /// `recompressed` is **0 for a whole-repository clone** because such a
1603 /// selection contains every base. The cost lands on the boundary a narrowed
1604 /// request cuts through and nowhere else: 1 968 of 33 773 entries, 5.9 %, on
1605 /// the fixture above.
1606 ///
1607 /// `ofs_delta_ok` is the client's `ofs-delta` capability. An old client that
1608 /// cannot parse one is not refused any more: the entry is re-headed as a
1609 /// `REF_DELTA` naming its base by oid, **carrying the same compressed delta
1610 /// payload**, so that arm stays a copy too. Only a base outside the request
1611 /// costs a rebuild.
1612 ///
1613 /// # `thin_haves` — the boundary entry that does NOT have to be rebuilt
1614 ///
1615 /// `Some(tips)` says the client consented to a thin pack *and* the
1616 /// negotiation found common ground; [`crate::serve::GitStore::emit_oids`] is
1617 /// where the two halves are required together, because either alone is a
1618 /// failed fetch rather than a smaller one. Given it, a base outside the
1619 /// request is checked against [`the client's own objects`](GitStore::client_bases),
1620 /// and one the client already holds is named by oid in a `REF_DELTA` that
1621 /// carries the **stored delta stream unchanged**. That entry counts as
1622 /// `copied`, because it is: nothing was inflated and nothing was deflated.
1623 ///
1624 /// `None` — a clone, or a client that did not ask for a thin pack — behaves
1625 /// exactly as before, and a **full clone is byte-identical either way**,
1626 /// because a whole-repository request has no base outside itself for any of
1627 /// this to apply to.
1628 ///
1629 /// It does nothing for the *clone* half of the boundary cost, and cannot: a
1630 /// clone's receiver holds nothing, so there is no external base to name.
1631 /// That half is the arm below it — [`crate::delta`] computes a delta against
1632 /// something the pack **does** carry, which is the only move left when there
1633 /// is no receiver to lean on. The two do not overlap: `thin_haves` reaches a
1634 /// boundary entry first and costs nothing when it fires, so a fetch stays a
1635 /// pure copy and only a clone pays for a computed delta.
1636 ///
1637 /// # An oid this repository does not hold is REFUSED
1638 ///
1639 /// It used to be skipped, which is the same silent-under-send shape as the
1640 /// closure defect above wearing a different hat: N asked, N-1 emitted,
1641 /// `PackStats` reporting success, and the client the first to know. gunnar's
1642 /// in-memory arm has always refused it, so the skip also meant the two
1643 /// engines behind one contract disagreed about what a missing object means.
1644 /// A partial-clone filter does not depend on the skip and cannot: a filter
1645 /// *removes* oids from the selection, it never adds one the store lacks.
1646 ///
1647 /// # Cost
1648 ///
1649 /// O(objects requested). The previous shape read `oids_in_order()` and
1650 /// `lookup_batch` over the **whole repository** on every call, to build an
1651 /// offset→oid map it needed only for the closure; with no closure the
1652 /// selection's own offsets answer "is this base in the request", so that
1653 /// scan is gone and no reverse index has to exist.
1654 pub fn emit_set(
1655 &self,
1656 oids: &[Oid<'_>],
1657 ofs_delta_ok: bool,
1658 thin_haves: Option<&[Oid<'_>]>,
1659 ) -> Result<Vec<crate::pack_walk::EmitEntry>> {
1660 use crate::index_layout::ObjType;
1661 use std::collections::hash_map::Entry;
1662
1663 // If the gate is free, absorb now; if the worker holds it, the rows
1664 // come from the index plus a read-only pass over the pending packs
1665 // (`lookup_batch_never_wait` below). Never a wait.
1666 let _ = self
1667 .try_absorb_pending()
1668 .context("absorbing pending index work before emitting a pack")?;
1669
1670 // Deduplicate, preserving the caller's order. A repeated oid in the
1671 // request must not become a repeated entry in the pack.
1672 //
1673 // **A map and not a set**, keyed oid → position in `asked` (which is the
1674 // position in `rows`). The membership question is the one it always was;
1675 // what the position buys is the base's *archive offset* for a
1676 // `REF_DELTA`, which names its base by oid and can learn where that base
1677 // sits no other way. It is a `usize` per requested object on a vector
1678 // this function already builds — not a second pass and not a second
1679 // index lookup.
1680 let mut asked: Vec<&[u8]> = Vec::with_capacity(oids.len());
1681 let mut seen: HashMap<&[u8], usize> = HashMap::with_capacity(oids.len());
1682 for oid in oids {
1683 if let Entry::Vacant(slot) = seen.entry(*oid) {
1684 slot.insert(asked.len());
1685 asked.push(*oid);
1686 }
1687 }
1688
1689 let rows = self.lookup_batch_never_wait(&asked)?;
1690
1691 // **One mapping for the whole emit set.** Only the header probes below
1692 // read through it — the entries themselves are recorded as extents and
1693 // resolved by [`Self::resolve_emit_payloads`], which takes its own
1694 // snapshot of the same [`ArchiveMap`](crate::archive_map::ArchiveMap) and
1695 // therefore the same `Arc` unless a push landed in between.
1696 let snap = self.archive_snapshot()?;
1697
1698 // The two questions "is this base in the request" are asked in the two
1699 // coordinate systems the two delta forms use, and both are answered off
1700 // the request itself rather than off the index.
1701 //
1702 // **Offset → position in the request**, not a bare set of offsets. The
1703 // membership question is the same one it always was; what the position
1704 // buys is the base's *oid*, which the re-delta arm below needs in order
1705 // to read the base's content. It is the same single pass over `rows`
1706 // that built the set, so it costs a `usize` per requested object and no
1707 // extra work — and it is emphatically not the whole-repository
1708 // offset→oid scan the old delta-base closure used to do.
1709 let in_set_offsets: HashMap<u64, usize> = rows
1710 .iter()
1711 .enumerate()
1712 .filter_map(|(i, r)| r.as_ref().map(|r| (r.offset, i)))
1713 .collect();
1714 let oid_len = self.hash_kind().oid_len();
1715
1716 // The base of the last boundary entry that was re-deltified, kept so a
1717 // run of entries hanging off one ancestor inflates it once. One slot and
1718 // not an LRU: measured on `h2h-linear-sha1-2048c-1024f-16k`, 961
1719 // boundary entries name 855 distinct stored bases, so there is no reuse
1720 // worth a cache that could hold a repository's worth of objects — and
1721 // this crate is under a live RSS budget.
1722 let mut base_content: Option<(u64, Vec<u8>)> = None;
1723
1724 // What the client already holds, built **lazily and at most once**. A
1725 // clone never asks for it (`thin_haves` is `None`), and a fetch every one
1726 // of whose bases is inside the request never reaches the line that builds
1727 // it either — only a request that actually cuts a delta chain pays, which
1728 // is the only request it can help.
1729 let mut client_bases: Option<ExternalBases> = None;
1730
1731 let mut out = Vec::with_capacity(asked.len());
1732 for (oid, row) in asked.iter().zip(rows.iter()) {
1733 // **An oid this repository does not hold is REFUSED, not dropped.**
1734 //
1735 // This used to `continue`, and that is the same silent-under-send
1736 // shape as the closure defect above: the request asks for N objects,
1737 // the pack carries N-1, the receipt says success and the client is
1738 // the first to find out. It also made the two engines disagree about
1739 // what a missing object *means* — gunnar's in-memory arm has always
1740 // said *"emit_pack was asked for X, which this store does not hold;
1741 // a pack emitter that skipped it would send a pack no client can
1742 // close"* — so a benchmark across the two was comparing two
1743 // contracts.
1744 //
1745 // No legitimate caller relies on the skip. Every set that reaches
1746 // here is built out of this store's own index: `select` answers from
1747 // the reachability bitmaps, and gunnar's own walk resolves each id
1748 // against the store before it selects it. A partial-clone filter is
1749 // no exception and is the case worth naming, because it is the one
1750 // that *looks* like it might be: `--filter=blob:none` **removes**
1751 // oids from the selection, it never adds one the store lacks, so a
1752 // filtered request is a smaller set of objects that are all present
1753 // and this refusal never fires on it.
1754 let Some(row) = row.as_ref() else {
1755 bail!(
1756 "this pack was asked for {}, which this repository does not hold; emitting \
1757 the rest would be a short pack reported as a success, so this refuses \
1758 instead",
1759 hex::encode(oid)
1760 );
1761 };
1762
1763 // Which base this entry names, if any, and whether the request holds
1764 // it. A `REF_DELTA`'s `delta_base` column is 0 by construction —
1765 // `DeltaBase::as_offset` reports 0 for a ref base and 0 is also the
1766 // no-base sentinel — so its base is read out of the entry's own
1767 // bytes, directly after the type/size varint.
1768 //
1769 // A base that is *outside* the request is carried in **both**
1770 // coordinate systems rather than as a bare `false`, because that is
1771 // what the thin arm below needs: an `OFS_DELTA` knows only where its
1772 // base sits, a `REF_DELTA` knows only what it is called, and
1773 // "does the client hold it" has to be asked in whichever one the
1774 // entry speaks.
1775 let base = match row.obj_type {
1776 ObjType::OfsDelta => match in_set_offsets.get(&row.delta_base) {
1777 Some(_) => BaseOf::Inside {
1778 at: row.delta_base,
1779 },
1780 None => BaseOf::Outside {
1781 offset: row.delta_base,
1782 oid: None,
1783 },
1784 },
1785 ObjType::RefDelta => {
1786 let head = self.extent(&snap, row.offset, row.len.min(64))?;
1787 let (_, _, n) = crate::pack_walk::type_and_size_of(&head)?;
1788 match head.get(n..n + oid_len) {
1789 // In the request — **and where**. `rows[i]` is `None`
1790 // only for an oid this store does not hold, which the
1791 // refusal above turns into an error the moment the loop
1792 // reaches it; until then the base is treated as outside,
1793 // which is the answer that cannot be wrong.
1794 Some(b) => match seen
1795 .get(b)
1796 .and_then(|i| rows[*i].as_ref())
1797 .map(|r| r.offset)
1798 {
1799 Some(at) => BaseOf::Inside { at },
1800 None => BaseOf::Outside {
1801 offset: 0,
1802 oid: Some(b.to_vec()),
1803 },
1804 },
1805 // A truncated entry names no base this can read. It is
1806 // not "inside", and it is not offered to the client
1807 // either — it falls through to the whole rebuild, which
1808 // is the answer that cannot be wrong.
1809 None => BaseOf::Outside {
1810 offset: 0,
1811 oid: None,
1812 },
1813 }
1814 }
1815 _ => BaseOf::Whole,
1816 };
1817
1818 if let BaseOf::Whole | BaseOf::Inside { .. } = base {
1819 let inside_at = match base {
1820 BaseOf::Inside { at } => Some(at),
1821 _ => None,
1822 };
1823 // **The whole-clone arm, and it reads not one byte here.** The
1824 // entry is recorded as its address in the archive; the bytes are
1825 // resolved against the mapping at emit time and go straight from
1826 // the page cache to the wire. This line used to be a `pread`
1827 // into a fresh `Vec` — 13.8 M of each on a `linux.git` clone, all
1828 // of them held at once, which is the 2314 MB peak RSS
1829 // [`crate::pack_walk::EntryBytes`] documents.
1830 let extent = crate::pack_walk::EntryBytes::Extent {
1831 offset: row.offset,
1832 len: row.len,
1833 };
1834 let (stored, obj_type, delta_base) = match (row.obj_type, inside_at) {
1835 // An `OFS_DELTA` for a client that cannot read one: re-head it
1836 // as a `REF_DELTA` naming the same base by oid. The compressed
1837 // payload is the delta stream either way, so this is still a
1838 // copy in the `recompressed` sense — nothing is inflated — but
1839 // the concatenation of a new header and an old payload exists
1840 // nowhere on disk, so it cannot be an extent and is `Owned`.
1841 (ObjType::OfsDelta, Some(_)) if !ofs_delta_ok => {
1842 let base_oid = self.oid_at_offset(row.delta_base, &asked, &rows)?;
1843 let bytes = self.extent(&snap, row.offset, row.len)?;
1844 (
1845 crate::pack_walk::EntryBytes::Owned(Self::as_ref_delta(
1846 &bytes, &base_oid,
1847 )?),
1848 ObjType::RefDelta,
1849 0,
1850 )
1851 }
1852 // 🚨 **A stored `REF_DELTA` whose base is IN THIS PACK is
1853 // re-headed as an `OFS_DELTA`, and that is not an
1854 // optimisation.**
1855 //
1856 // gitoxide — which is `gunnar_client`, `gunnar`'s own
1857 // `receive-pack`, and every other gix-based receiver — reads
1858 // every `OBJ_REF_DELTA` in an incoming pack as naming an
1859 // object *the receiver already has*.
1860 // `gix_pack::data::input::LookupRefDeltaObjectsIter` consults
1861 // the local object database and the bases it has already
1862 // spliced in, and **nowhere else**: never inside the pack it
1863 // is reading. A base that is in the pack and not in the
1864 // receiver's store is `input::Error::NotFound`, and the fetch
1865 // dies with `The object <base> could not be decoded or wasn't
1866 // found` — with the whole pack rejected, on a clone whose
1867 // target is empty by definition, so *every* such entry is
1868 // fatal. That is `S-003` / gitoxide#2882, and `gunnar-wire`'s
1869 // `Selection::client_has` carries the same rule for gunnar's
1870 // own emitter.
1871 //
1872 // Stock git resolves it happily, which is why this survived
1873 // five green `git clone` runs against the store that could
1874 // not serve one `gunnar_client` fetch (`gunnar.rewrite`
1875 // round 0 and `gunnar.year` week 0, all four znippy columns,
1876 // 2026-08-14 — the server logged the same request `served`).
1877 //
1878 // The entry keeps its compressed delta stream byte for byte;
1879 // only the header changes, from "base named by oid" to "base
1880 // named by distance", which is the naming every receiver in
1881 // existence resolves inside the pack. This is where such an
1882 // entry comes from in the first place: `git index-pack
1883 // --fix-thin` appends the base into the pack and leaves the
1884 // ref-delta naming it by oid, so any repository pushed to
1885 // more than once holds them.
1886 //
1887 // # What it costs, stated rather than glossed
1888 //
1889 // `Owned`, so this is the one arm that gives up the zero-copy
1890 // property for an entry it does **not** rebuild: the
1891 // compressed delta stream is memcpy'd once, because a new
1892 // header concatenated with an old payload exists nowhere on
1893 // disk and no extent can address it. Bounded by the
1894 // compressed size of the ref-deltas whose base is in the
1895 // request — not by the repository, and not by the pack.
1896 //
1897 // It could be avoided: `emit_pack` re-encodes every header
1898 // from `EmitEntry::obj_type` while `header_len` finds the
1899 // payload from the STORED bytes' own type, so an `Extent`
1900 // over the untouched ref-delta declared as an `OfsDelta`
1901 // would emit the identical pack with no copy at all. That is
1902 // deliberately not done: it makes an `EmitEntry` disagree
1903 // with the bytes it points at, and the entry model being
1904 // trustworthy is worth more than the memcpy. If the RSS
1905 // budget ever says otherwise, the honest form is a third
1906 // `EntryBytes` variant that carries the new header beside
1907 // the extent, not a silent disagreement.
1908 (ObjType::RefDelta, Some(at)) if ofs_delta_ok => {
1909 let bytes = self.extent(&snap, row.offset, row.len)?;
1910 (
1911 crate::pack_walk::EntryBytes::Owned(Self::as_ofs_delta(
1912 &bytes, oid_len, row.offset, at,
1913 )?),
1914 ObjType::OfsDelta,
1915 at,
1916 )
1917 }
1918 // Everything else copies. A `REF_DELTA` left as one here is
1919 // the `!ofs_delta_ok` client, which cannot be handed a
1920 // distance at all — and which is stock git older than 1.4.4,
1921 // never a gix receiver, since gix has always advertised
1922 // `ofs-delta`.
1923 _ => (extent, row.obj_type, row.delta_base),
1924 };
1925 out.push(crate::pack_walk::EmitEntry {
1926 oid: oid.to_vec(),
1927 stored,
1928 obj_type,
1929 uncompressed_size: row.uncompressed_size,
1930 delta_base,
1931 offset: row.offset,
1932 recompressed: false,
1933 deltified: false,
1934 });
1935 continue;
1936 }
1937
1938 // ── the base is outside the request ──────────────────────────────
1939 //
1940 // **Thin first.** If the client consented to a thin pack and the
1941 // negotiation vouches that it already holds this base, the entry
1942 // goes out as a `REF_DELTA` naming that base by oid, carrying the
1943 // **same compressed delta stream**. Nothing is inflated, nothing is
1944 // deflated, and the pack is smaller than the whole object by exactly
1945 // the margin the delta was worth. That is what `caps.thin` has always
1946 // been the allowance for.
1947 //
1948 // An `OFS_DELTA` cannot survive this: it names its base by a
1949 // backwards distance *within the pack*, and the base is not in the
1950 // pack. Re-heading it as a `REF_DELTA` is the same header swap the
1951 // no-`ofs-delta` client path does, over an unchanged payload.
1952 if let Some(haves) = thin_haves {
1953 if client_bases.is_none() {
1954 client_bases = Some(self.client_bases(haves)?);
1955 }
1956 let ext = client_bases
1957 .as_ref()
1958 .expect("the client's bases were just built");
1959 let base_oid: Option<Vec<u8>> = match &base {
1960 // Unreachable: the arm above `continue`s on both. Matched
1961 // rather than `unreachable!()` so that a future arm falling
1962 // through here declines the thin path instead of panicking
1963 // inside a serving thread.
1964 BaseOf::Whole | BaseOf::Inside { .. } => None,
1965 BaseOf::Outside {
1966 oid: Some(named), ..
1967 } => ext
1968 .held
1969 .contains(&OidKey::new(named)?)
1970 .then(|| named.clone()),
1971 BaseOf::Outside {
1972 offset,
1973 oid: None,
1974 } => ext.by_offset.get(offset).map(|k| k.as_slice().to_vec()),
1975 };
1976 if let Some(base_oid) = base_oid {
1977 // A stored `REF_DELTA` already names its base by oid, so its
1978 // bytes go out untouched and stay an extent. An `OFS_DELTA`
1979 // has to be re-headed, and the result is new bytes.
1980 let stored = if row.obj_type == ObjType::OfsDelta {
1981 let bytes = self.extent(&snap, row.offset, row.len)?;
1982 crate::pack_walk::EntryBytes::Owned(Self::as_ref_delta(&bytes, &base_oid)?)
1983 } else {
1984 crate::pack_walk::EntryBytes::Extent {
1985 offset: row.offset,
1986 len: row.len,
1987 }
1988 };
1989 out.push(crate::pack_walk::EmitEntry {
1990 oid: oid.to_vec(),
1991 stored,
1992 obj_type: ObjType::RefDelta,
1993 uncompressed_size: row.uncompressed_size,
1994 // The base is NOT in this pack, so nothing may be ordered
1995 // after it: `topological_order` reads 0 as "names no base
1996 // in here", which for an external base is the truth.
1997 delta_base: 0,
1998 offset: row.offset,
1999 // Copied. The payload never left the archive's bytes.
2000 recompressed: false,
2001 deltified: false,
2002 });
2003 continue;
2004 }
2005 }
2006
2007 // The base is not being sent and the client cannot supply it, so the
2008 // stored bytes cannot go out as they are. Two answers remain, and
2009 // the first is worth ~4× the second on the wire.
2010 let (kind, body) = self.content(oid)?.ok_or_else(|| {
2011 anyhow!(
2012 "{} is a delta whose base is outside this request, so it must be sent whole, \
2013 and this repository cannot produce its content — neither §14's exploded \
2014 table nor a re-derivation from the verbatim packs answered",
2015 hex::encode(oid)
2016 )
2017 })?;
2018
2019 // ── re-delta against a base the request DOES carry ────────────────
2020 //
2021 // The candidate comes from the entry's own stored delta chain: the
2022 // nearest ancestor that is inside the request. That is the packer's
2023 // original similarity judgement, already made and already written
2024 // down — no window, no sort, no scan of the repository.
2025 let candidate = if crate::delta::enabled() {
2026 self.in_request_chain_ancestor(row.delta_base, row.offset, &in_set_offsets)?
2027 } else {
2028 None
2029 };
2030 if let Some(base_at) = candidate {
2031 let at = *in_set_offsets
2032 .get(&base_at)
2033 .expect("the ancestor was found by lookup in this very map");
2034 // Reuse the last base if this entry hangs off the same one.
2035 if base_content.as_ref().is_none_or(|(o, _)| *o != base_at) {
2036 let base_oid = asked[at];
2037 let (_, base_body) = self.content(base_oid)?.ok_or_else(|| {
2038 anyhow!(
2039 "{} is in this request and is the delta base chosen for {}, and this \
2040 repository cannot produce its content",
2041 hex::encode(base_oid),
2042 hex::encode(oid)
2043 )
2044 })?;
2045 base_content = Some((base_at, base_body));
2046 }
2047 let (_, base_body) = base_content.as_ref().expect("just filled");
2048
2049 // `None` is the encoder declining — an unrelated base, or a
2050 // delta that came out no smaller than the object. Falling
2051 // through to the whole rebuild is then strictly right.
2052 if let Some(d) = crate::delta::delta(base_body, &body)? {
2053 let stored = Self::ofs_delta_entry(&d, row.offset, base_at)?;
2054 // A client with no `ofs-delta` is served the same computed
2055 // delta under a `REF_DELTA` head naming the base by oid —
2056 // the identical header swap the base-inside arm above does,
2057 // over a payload that is already built.
2058 // **The `Owned` exception, in its primary shape.** These
2059 // bytes were computed here and exist on no disk, so no
2060 // extent addresses them; carrying them on the entry is
2061 // correct and is bounded by the boundary a narrowed request
2062 // cuts through (961 of 33 773 entries on the fixture below),
2063 // never by the repository.
2064 let (stored, obj_type, delta_base) = if ofs_delta_ok {
2065 (
2066 crate::pack_walk::EntryBytes::Owned(stored),
2067 ObjType::OfsDelta,
2068 base_at,
2069 )
2070 } else {
2071 (
2072 crate::pack_walk::EntryBytes::Owned(Self::as_ref_delta(
2073 &stored, asked[at],
2074 )?),
2075 ObjType::RefDelta,
2076 0,
2077 )
2078 };
2079 out.push(crate::pack_walk::EmitEntry {
2080 oid: oid.to_vec(),
2081 stored,
2082 obj_type,
2083 uncompressed_size: row.uncompressed_size,
2084 delta_base,
2085 offset: row.offset,
2086 // Inflated and re-deflated, so NOT a copy — and the
2087 // receipt says which of the two rebuilds ran.
2088 recompressed: true,
2089 deltified: true,
2090 });
2091 continue;
2092 }
2093 }
2094
2095 // Ship the object whole rather than dragging an unreachable base —
2096 // and its children — into a clone.
2097 out.push(crate::pack_walk::EmitEntry {
2098 oid: oid.to_vec(),
2099 // Inflated and re-deflated from §14's resolved content: the
2100 // `Owned` exception again, and again bounded by the boundary.
2101 stored: crate::pack_walk::EntryBytes::Owned(Self::whole_entry(kind, &body)?),
2102 obj_type: crate::serve::resolved_type(kind),
2103 uncompressed_size: body.len() as u64,
2104 // Whole: it names no base, and nothing may order it after one.
2105 delta_base: 0,
2106 offset: row.offset,
2107 recompressed: true,
2108 deltified: false,
2109 });
2110 }
2111 Ok(out)
2112 }
2113
2114 /// **PHASE 1 of emitting: resolve every entry's stored bytes, in parallel.**
2115 ///
2116 /// Emission splits in two, and the split is not a preference — it is where
2117 /// the ordering dependency actually is:
2118 ///
2119 /// | phase | what it does | why |
2120 /// |---|---|---|
2121 /// | 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** |
2122 /// | 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 |
2123 ///
2124 /// Phase 2 is not parallelisable and no attempt is made: the distance chain
2125 /// is a genuine serial dependency, and a running sha1 is another.
2126 ///
2127 /// # No lock, and why that is a fact rather than an intention
2128 ///
2129 /// [`Self::adopt_journal`] states the storage property this rests on — *"the
2130 /// blob file is append-only and `gc` truncates nothing, so a retired pack's
2131 /// bytes are still exactly where the journal says they are"*. Every byte a
2132 /// snapshot covers is therefore immutable for the life of the file, so the
2133 /// workers below share one `&Mapped` and read it concurrently with no
2134 /// synchronisation of any kind. There is no mutex here; if one were needed
2135 /// the data model would be wrong.
2136 ///
2137 /// # 🚨 This is a fan-out inside the serving tier, and `crate::serve`'s
2138 /// header says that tier is serial
2139 ///
2140 /// It did, and the reason it gave is still true and still respected: the
2141 /// constellation's fan-out primitive has no reentrancy detection, so an
2142 /// unbounded fan-out per request inside an admission gate that already runs
2143 /// 32 transfers spawns W² threads. That is why this is **bounded twice**:
2144 ///
2145 /// * it does not fan out at all below [`FAN_OUT_AT`] entries, so every fetch
2146 /// small enough for thread-spawn to dominate runs on the calling thread
2147 /// exactly as before, and
2148 /// * it never exceeds [`MAX_RESOLVE_WORKERS`] (override with
2149 /// `ZNIPPY_GIT_EMIT_WORKERS`), so the worst case is 32 × 4 and not
2150 /// 32 × ncores.
2151 ///
2152 /// # What the fan-out is worth — stated honestly
2153 ///
2154 /// Resolving a mapped extent is a bounds check and a pointer, so the work
2155 /// this parallelises is **not** the resolve itself. It is the two things
2156 /// beside it: the first-touch **page fault** on each entry's header (the
2157 /// grammar is parsed here, which is what forces that fault, and it validates
2158 /// the entry before phase 2 commits a byte to the wire), and the `pread`
2159 /// fallback for any extent the mapping does not cover. On a warm page cache
2160 /// with the whole archive mapped, phase 1 is nearly free and the fan-out
2161 /// buys correspondingly little — the 20 % of cycles this change is aimed at
2162 /// is recovered by *not copying*, not by threads. I could not measure the
2163 /// fan-out's own contribution separately: oden was at loadavg 25.9 while
2164 /// this was written (2026-08-14), and a shared box cannot answer a question
2165 /// that fine.
2166 pub(crate) fn resolve_emit_payloads<'a>(
2167 &self,
2168 entries: &'a [crate::pack_walk::EmitEntry],
2169 snap: &'a crate::archive_map::Mapped,
2170 ) -> Result<Vec<std::borrow::Cow<'a, [u8]>>>
2171 where
2172 Self: Sync,
2173 {
2174 let workers = if entries.len() < FAN_OUT_AT {
2175 1
2176 } else {
2177 emit_workers()
2178 };
2179 self.resolve_with(entries, snap, workers)
2180 }
2181
2182 /// [`Self::resolve_emit_payloads`] with the worker count named rather than
2183 /// decided — `1` runs inline on the calling thread.
2184 ///
2185 /// Split out so the fan-out is **testable at a size a test fixture can
2186 /// reach**. The production threshold is 4096 entries and the corpus these
2187 /// guards run on holds 2687, so a test that went through the front door
2188 /// would exercise the serial arm every time and the gatling arm never;
2189 /// `the_fan_out_resolves_the_identical_bytes_the_serial_pass_does` calls
2190 /// this with 1 and with 4 over the same set and requires the two to agree
2191 /// byte for byte.
2192 pub(crate) fn resolve_with<'a>(
2193 &self,
2194 entries: &'a [crate::pack_walk::EmitEntry],
2195 snap: &'a crate::archive_map::Mapped,
2196 workers: usize,
2197 ) -> Result<Vec<std::borrow::Cow<'a, [u8]>>>
2198 where
2199 Self: Sync,
2200 {
2201 use znippy_zoomies::gatling_forkjoin::gatling_for_each;
2202
2203 let one = |i: usize| -> Result<std::borrow::Cow<'a, [u8]>> {
2204 let e = &entries[i];
2205 let bytes = match &e.stored {
2206 // The bytes live on the entry — the rebuilt-delta exception.
2207 // Borrowed, never cloned: `EntryBytes::Owned` is already the
2208 // only allocation this path makes and copying it would double it.
2209 crate::pack_walk::EntryBytes::Owned(v) => std::borrow::Cow::Borrowed(v.as_slice()),
2210 crate::pack_walk::EntryBytes::Extent { offset, len } => {
2211 self.extent(snap, *offset, *len)?
2212 }
2213 };
2214 // Parse the entry's own header now. Two things fall out: a malformed
2215 // entry is refused **before** phase 2 has written any of the pack,
2216 // and the first cache line of every entry is touched here — on
2217 // whichever worker got it — rather than one at a time on the serial
2218 // writer.
2219 crate::pack_walk::type_and_size_of(&bytes).with_context(|| {
2220 format!(
2221 "resolving the stored bytes of {} for emission",
2222 hex::encode(&e.oid)
2223 )
2224 })?;
2225 Ok(bytes)
2226 };
2227
2228 if workers <= 1 {
2229 return (0..entries.len()).map(one).collect();
2230 }
2231 gatling_for_each(entries.len(), workers, one)
2232 .into_iter()
2233 .collect()
2234 }
2235
2236 /// **Both phases, one call** — the only way a caller in this crate turns an
2237 /// ordered emit set into pack bytes.
2238 ///
2239 /// It is three lines, and it is a function rather than three lines because
2240 /// LAW 5's *fix by construction* applies exactly here: the snapshot has to
2241 /// outlive the slices taken from it, and a caller that took its own snapshot
2242 /// per entry, or dropped it between the phases, would be writing a pack out
2243 /// of a mapping that no longer exists. Routing `emit_oids` and every test
2244 /// through one writer makes that impossible to get wrong twice.
2245 pub(crate) fn emit_ordered(
2246 &self,
2247 ordered: &[crate::pack_walk::EmitEntry],
2248 out: &mut dyn std::io::Write,
2249 ) -> Result<crate::pack_walk::EmitReport>
2250 where
2251 Self: Sync,
2252 {
2253 let snap = self
2254 .archive_snapshot()
2255 .context("mapping the archive to emit")?;
2256 let payloads = self
2257 .resolve_emit_payloads(ordered, &snap)
2258 .context("resolving the emit set's stored bytes")?;
2259 crate::pack_walk::emit_pack(
2260 ordered,
2261 self.hash_kind(),
2262 out,
2263 &|i| Ok(payloads[i].as_ref()),
2264 )
2265 }
2266
2267 /// **The nearest ancestor of `from`'s stored delta chain that is inside the
2268 /// request**, or `None` when the chain leaves the request and never comes
2269 /// back.
2270 ///
2271 /// `from` is the archive offset of a base already known to be *outside* the
2272 /// request; `entry_at` is the offset of the entry that named it.
2273 ///
2274 /// # Why this cannot make a cycle, and why that is not a comment but an
2275 /// invariant
2276 ///
2277 /// The answer is required to satisfy `answer < entry_at`. In a well-formed
2278 /// pack a base always precedes the delta that names it, so every edge in the
2279 /// stored graph already points from a higher offset to a lower one; a new
2280 /// edge with the same property keeps the emitted graph a DAG **by
2281 /// construction**, whatever else the request contains. That matters because
2282 /// the obvious better candidate — the entry's own delta *children*, one
2283 /// revision away instead of two to nine — is exactly the set that would
2284 /// close a cycle: a child's stored base is this entry, so pointing this
2285 /// entry at the child makes `topological_order` unable to order either, and
2286 /// `emit_pack` refuses the set. Measured cost of the restriction on
2287 /// `h2h-linear-sha1-2048c-1024f-16k`: ~1 470 bytes per boundary entry
2288 /// against ~314 for the cyclic candidate, and 5 800 for shipping it whole.
2289 ///
2290 /// An `OFS_DELTA` decreases the offset every hop, so the bound is free
2291 /// there; a `REF_DELTA` names its base by oid and could point anywhere, so
2292 /// the comparison is made rather than assumed.
2293 ///
2294 /// # Cost
2295 ///
2296 /// One 64-byte `pread` per hop, no inflate, no allocation sized by an
2297 /// object — the same walk and the same ceiling as
2298 /// [`crate::serve::GitStore::resolved_type_at`], for the same reason: a
2299 /// pushed pack's back-references are not under this server's control, so a
2300 /// cycle must terminate rather than spin. Measured hop counts on the fixture
2301 /// above: 2 for 392 of the 859 that resolve, 3 for 260, and 9 at the worst.
2302 fn in_request_chain_ancestor(
2303 &self,
2304 from: u64,
2305 entry_at: u64,
2306 in_request: &HashMap<u64, usize>,
2307 ) -> Result<Option<u64>> {
2308 use crate::index_layout::{ObjType, ObjectIndex as _};
2309
2310 /// git's own `pack.depth` ceiling, doubled — the same bound
2311 /// `resolved_type_at` carries and for the same reason.
2312 const MAX_LINKS: usize = 100;
2313 /// Enough of an entry to read its type/size varint plus either an
2314 /// `OFS_DELTA` distance or a `REF_DELTA` oid.
2315 const HEADER_PROBE: u64 = 64;
2316
2317 let oid_len = self.hash_kind().oid_len();
2318 let mut at = from;
2319 for _ in 0..MAX_LINKS {
2320 if at != 0 && at < entry_at && in_request.contains_key(&at) {
2321 return Ok(Some(at));
2322 }
2323 if at == 0 {
2324 return Ok(None);
2325 }
2326 let head = self.read_extent(at, HEADER_PROBE)?;
2327 let (t, _, n) = crate::pack_walk::type_and_size_of(&head)?;
2328 at = match t {
2329 // A whole object with no base: the chain ends here, and it was
2330 // not in the request.
2331 ObjType::Commit | ObjType::Tree | ObjType::Blob | ObjType::Tag => {
2332 return Ok(None)
2333 }
2334 ObjType::OfsDelta => {
2335 let (distance, _) = crate::pack_walk::ofs_distance_of(&head[n..])?;
2336 match at.checked_sub(distance) {
2337 Some(next) if next != 0 => next,
2338 // A base before the start of the archive is a corrupt
2339 // back-reference. The whole rebuild is always a correct
2340 // answer, so this declines rather than failing a clone
2341 // over an entry it was only ever going to optimise.
2342 _ => return Ok(None),
2343 }
2344 }
2345 ObjType::RefDelta => {
2346 let Some(base_oid) = head.get(n..n + oid_len) else {
2347 return Ok(None);
2348 };
2349 match self.index().lookup(base_oid) {
2350 Some(row) => row.offset,
2351 None => return Ok(None),
2352 }
2353 }
2354 };
2355 }
2356 Ok(None)
2357 }
2358
2359 /// An `OFS_DELTA` pack entry carrying `d`, deflated.
2360 ///
2361 /// The distance written here is the **archive** distance, which is the same
2362 /// coordinate every stored entry's distance is in;
2363 /// [`crate::pack_walk::emit_pack`] rewrites it into output coordinates for
2364 /// every entry it emits, copied or not, so this only has to be well-formed
2365 /// and self-consistent — [`crate::pack_walk::header_len`] parses it back to
2366 /// find where the payload starts.
2367 ///
2368 /// `Compression::fast` for the same reason [`Self::whole_entry`] uses it,
2369 /// and the trade is far better here: the buffer being deflated is the delta,
2370 /// which on the measured fixture is ~1.4 KB against the object's 16 KiB, so
2371 /// this path deflates roughly a **tenth** of the bytes the whole rebuild it
2372 /// replaces does.
2373 fn ofs_delta_entry(d: &[u8], entry_at: u64, base_at: u64) -> Result<Vec<u8>> {
2374 use crate::index_layout::ObjType;
2375 use std::io::Write as _;
2376
2377 let distance = entry_at.checked_sub(base_at).ok_or_else(|| {
2378 anyhow!(
2379 "a computed delta at archive offset {entry_at} names a base at {base_at}, which \
2380 is after it — an ofs-delta distance is backwards and this would not encode"
2381 )
2382 })?;
2383 let mut out = Vec::with_capacity(d.len() / 2 + 32);
2384 crate::pack_walk::encode_type_and_size(&mut out, ObjType::OfsDelta, d.len() as u64);
2385 crate::pack_walk::encode_ofs_distance(&mut out, distance);
2386 let mut z = flate2::write::ZlibEncoder::new(out, flate2::Compression::fast());
2387 z.write_all(d)?;
2388 Ok(z.finish()?)
2389 }
2390
2391 /// **The objects the receiver already held before this transfer**, in both
2392 /// coordinate systems a delta can name a base in.
2393 ///
2394 /// `haves` is the negotiated common **tips**, exactly as
2395 /// [`git_storage_trait::GitServe::emit_pack`] defines them, and the closure
2396 /// over them is this store's own — the contract deliberately passes tips
2397 /// rather than the walk's exclusion set, because the closure is
2398 /// repository-sized and the engine can rebuild it from its own bitmaps.
2399 ///
2400 /// # This is a VOUCHER, and the caller has to be able to stand behind it
2401 ///
2402 /// Every object named here may be pointed at by a delta the pack does not
2403 /// carry. If the receiver turns out **not** to hold one, its
2404 /// `index-pack --fix-thin` dies with `pack has N unresolved deltas`, so a
2405 /// caller that cannot vouch must not pass `caps.thin`. The case that matters
2406 /// is a **partial clone**: a `blob:none` client's `have` tips reach blobs it
2407 /// was deliberately never sent, so the closure over-states what it holds and
2408 /// a filtered fetch must not be marked thin.
2409 ///
2410 /// # Cost
2411 ///
2412 /// One bitmap union plus one point lookup per held object, and it happens at
2413 /// most once per request — and only on a request that has a base outside
2414 /// itself to place. The `by_offset` map exists because an `OFS_DELTA` knows
2415 /// its base only as an archive offset; building it over what the *client*
2416 /// holds rather than over the repository is what keeps this off the
2417 /// whole-repository scan the closure used to do.
2418 ///
2419 /// **And it is spent in raw bytes now, end to end.** This used to ask
2420 /// [`GitOps::reachable`] — hex out of the ordinal space, `hex::decode`d
2421 /// straight back to bytes, then a third `to_vec` per object to key the
2422 /// offset map, three heap allocations per held object for a set the size of
2423 /// the client's history. It asks [`GitStore::reachable_raw`] instead: one
2424 /// buffer for the closure, one hash table of [`OidKey`]s over it, and
2425 /// nothing per object.
2426 fn client_bases(&self, haves: &[Oid<'_>]) -> Result<ExternalBases> {
2427 use crate::index_layout::ObjectIndex as _;
2428
2429 let held = self.reachable_raw(haves, &[]).context(
2430 "closing over the negotiated common tips, to learn which bases the client can supply \
2431 for itself",
2432 )?;
2433 let refs: Vec<&[u8]> = held.iter().collect();
2434 let rows = self.index().lookup_batch(&refs);
2435 let mut by_offset = HashMap::with_capacity(refs.len());
2436 let mut keys = std::collections::HashSet::with_capacity(refs.len());
2437 for (oid, row) in refs.iter().zip(rows.iter()) {
2438 let key = OidKey::new(oid)?;
2439 if let Some(row) = row {
2440 by_offset.insert(row.offset, key);
2441 }
2442 keys.insert(key);
2443 }
2444 Ok(ExternalBases {
2445 by_offset,
2446 held: keys,
2447 })
2448 }
2449
2450 /// The oid of the entry at `offset`, looked up **in the request** — which is
2451 /// the only place it can be, because this is called for a base the request
2452 /// was just shown to contain.
2453 ///
2454 /// Linear over the request rather than a map, because it runs only on the
2455 /// `ofs-delta`-less client path: a capability no git since 1.4.4 omits, and
2456 /// one that has to be *advertised absent* to reach here at all.
2457 fn oid_at_offset(
2458 &self,
2459 offset: u64,
2460 asked: &[&[u8]],
2461 rows: &[Option<crate::index_layout::IndexRow>],
2462 ) -> Result<Vec<u8>> {
2463 asked
2464 .iter()
2465 .zip(rows)
2466 .find(|(_, r)| r.as_ref().is_some_and(|r| r.offset == offset))
2467 .map(|(oid, _)| oid.to_vec())
2468 .ok_or_else(|| {
2469 anyhow!(
2470 "the entry at archive offset {offset} was checked to be in this request and \
2471 then could not be found in it"
2472 )
2473 })
2474 }
2475
2476 /// Re-head a stored `OFS_DELTA` as a `REF_DELTA` naming `base_oid`.
2477 ///
2478 /// The compressed delta stream is copied unchanged; only the header differs,
2479 /// because the two forms differ in **how the base is named** and in nothing
2480 /// else. This is why a client without `ofs-delta` costs bytes and not CPU.
2481 fn as_ref_delta(stored: &[u8], base_oid: &[u8]) -> Result<Vec<u8>> {
2482 use crate::index_layout::ObjType;
2483 let (t, stated_size, n) = crate::pack_walk::type_and_size_of(stored)?;
2484 if t != ObjType::OfsDelta {
2485 bail!("only an ofs-delta can be re-headed as a ref-delta, this entry is {t:?}");
2486 }
2487 let (_, d) = crate::pack_walk::ofs_distance_of(stored.get(n..).unwrap_or(&[]))?;
2488 let mut out = Vec::with_capacity(stored.len() + base_oid.len());
2489 crate::pack_walk::encode_type_and_size(&mut out, ObjType::RefDelta, stated_size);
2490 out.extend_from_slice(base_oid);
2491 out.extend_from_slice(stored.get(n + d..).unwrap_or(&[]));
2492 Ok(out)
2493 }
2494
2495 /// Re-head a stored `REF_DELTA` as an `OFS_DELTA` naming the base that sits
2496 /// at `base_at` — **the inverse of [`Self::as_ref_delta`]**, and the fix for
2497 /// the one pack shape gitoxide cannot read.
2498 ///
2499 /// The compressed delta stream is copied unchanged; only the header differs,
2500 /// because the two forms differ in how the base is *named* and in nothing
2501 /// else. The reason this has to exist is in the caller: a `REF_DELTA` whose
2502 /// base is inside the same pack is resolved by stock git and refused by
2503 /// every gix-based receiver.
2504 ///
2505 /// # The distance written here is a placeholder, and that is by design
2506 ///
2507 /// Exactly as [`Self::ofs_delta_entry`] documents: the distance an
2508 /// `OFS_DELTA` carries is relative to a position in the pack it is *in*, so
2509 /// [`crate::pack_walk::emit_pack`] re-encodes it for every entry from the
2510 /// output offsets it is assigning, and [`crate::pack_walk::header_len`]
2511 /// parses whatever is written here only to find where the payload starts. It
2512 /// therefore has to be well-formed and self-consistent, and nothing more.
2513 ///
2514 /// It cannot simply be `entry_at - base_at`: a `REF_DELTA` is allowed to
2515 /// name a base that comes **after** it in the pack, and `git index-pack
2516 /// --fix-thin` produces exactly that — it appends the base at the end. So a
2517 /// forward reference encodes as `1`, and `emit_pack` writes the real
2518 /// backwards distance once `topological_order` has put the base first.
2519 fn as_ofs_delta(
2520 stored: &[u8],
2521 oid_len: usize,
2522 entry_at: u64,
2523 base_at: u64,
2524 ) -> Result<Vec<u8>> {
2525 use crate::index_layout::ObjType;
2526 let (t, stated_size, n) = crate::pack_walk::type_and_size_of(stored)?;
2527 if t != ObjType::RefDelta {
2528 bail!("only a ref-delta can be re-headed as an ofs-delta, this entry is {t:?}");
2529 }
2530 let payload = stored.get(n + oid_len..).ok_or_else(|| {
2531 anyhow!(
2532 "a ref-delta entry at archive offset {entry_at} is {} bytes, which is not even its \
2533 header plus a {oid_len}-byte base oid",
2534 stored.len()
2535 )
2536 })?;
2537 let mut out = Vec::with_capacity(payload.len() + 32);
2538 crate::pack_walk::encode_type_and_size(&mut out, ObjType::OfsDelta, stated_size);
2539 crate::pack_walk::encode_ofs_distance(
2540 &mut out,
2541 entry_at.checked_sub(base_at).filter(|d| *d > 0).unwrap_or(1),
2542 );
2543 out.extend_from_slice(payload);
2544 Ok(out)
2545 }
2546
2547 /// A whole (non-delta) pack entry for `body`: type/size header, then the
2548 /// body deflated.
2549 ///
2550 /// The **only** place in this crate that deflates on a serving path, and the
2551 /// only reason it exists is a delta whose base the request does not contain.
2552 /// `Compression::fast` deliberately: these bytes are re-compressed because
2553 /// they *cannot* be copied, so the trade is wire size against a clone's
2554 /// latency, and the entries this runs on are a small boundary fraction of any
2555 /// real request.
2556 fn whole_entry(kind: crate::object::GitObjectKind, body: &[u8]) -> Result<Vec<u8>> {
2557 use std::io::Write as _;
2558 let mut out = Vec::with_capacity(body.len() / 2 + 32);
2559 crate::pack_walk::encode_type_and_size(
2560 &mut out,
2561 crate::serve::resolved_type(kind),
2562 body.len() as u64,
2563 );
2564 let mut z = flate2::write::ZlibEncoder::new(out, flate2::Compression::fast());
2565 z.write_all(body)?;
2566 Ok(z.finish()?)
2567 }
2568
2569 /// Take the absorb gate and hold it.
2570 ///
2571 /// The only way to observe "a read that arrived before the drain" as a
2572 /// **state** rather than as a race: with this held, the worker is parked at
2573 /// the top of its absorb, so the pack is provably durable, provably queued
2574 /// and provably not indexed. Its guard is in this module's tests, and nothing
2575 /// on a serving path takes it.
2576 #[cfg(test)]
2577 pub(crate) fn hold_absorb_gate(&self) -> std::sync::MutexGuard<'_, ()> {
2578 self.absorber.gate.lock().expect("absorb gate poisoned")
2579 }
2580
2581 /// `pread` an extent out of the verbatim archive, into a fresh `Vec`.
2582 ///
2583 /// **The fallback and the owning path.** A caller that can hold a borrow
2584 /// wants [`Self::archive_snapshot`] plus [`Self::extent`] instead: this one
2585 /// costs a syscall, an allocation and a kernel→user copy, which is the shape
2586 /// [`crate::archive_map`] exists to get off the emit path.
2587 pub(crate) fn read_extent(&self, offset: u64, len: u64) -> Result<Vec<u8>> {
2588 self.absorber.read_extent(offset, len)
2589 }
2590
2591 /// One mapping of the verbatim archive, to be taken **once per operation**
2592 /// and sliced N times. See [`crate::archive_map::ArchiveMap::snapshot`].
2593 pub(crate) fn archive_snapshot(&self) -> Result<Arc<crate::archive_map::Mapped>> {
2594 self.absorber.map.snapshot()
2595 }
2596
2597 /// **The one resolver**: an extent's bytes, borrowed from `snap` when it
2598 /// covers them and `pread` into a fresh buffer when it does not.
2599 ///
2600 /// `Cow::Borrowed` is the ordinary answer and the whole point — no syscall,
2601 /// no allocation, no copy, just a bounds-checked slice of the page cache.
2602 /// `Cow::Owned` happens for an extent past the mapped end, which means the
2603 /// blob file grew after the snapshot was taken (a push landed mid-clone).
2604 /// `Mapped::get` returning `None` is *"go and pread it"* and never *"there
2605 /// are no bytes"*, so the two arms return the **same bytes** and differ only
2606 /// in what they cost — which is the property
2607 /// `an_extent_past_the_mapping_falls_back_and_returns_the_same_bytes`
2608 /// asserts.
2609 pub(crate) fn extent<'m>(
2610 &self,
2611 snap: &'m crate::archive_map::Mapped,
2612 offset: u64,
2613 len: u64,
2614 ) -> Result<std::borrow::Cow<'m, [u8]>> {
2615 match snap.get(offset, len) {
2616 Some(b) => Ok(std::borrow::Cow::Borrowed(b)),
2617 None => Ok(std::borrow::Cow::Owned(self.read_extent(offset, len)?)),
2618 }
2619 }
2620
2621 /// Record a durable pack as pending index work.
2622 ///
2623 /// The **bit going clear**, and the ack path's last act before it returns.
2624 pub(crate) fn queue(&self, pack_id: u64, extent: Extent) -> Result<()> {
2625 self.absorber.queue(pack_id, extent)
2626 }
2627
2628 fn reach_bitmaps(&self) -> Result<Arc<Vec<ReachEntry>>> {
2629 self.absorber.reach_bitmaps()
2630 }
2631
2632 /// [`Absorber::reach_bitmaps_with`], for the differential guard.
2633 pub(crate) fn reach_bitmaps_with(
2634 &self,
2635 policy: crate::reach::ReachPolicy,
2636 cache: bool,
2637 ) -> Result<Arc<Vec<ReachEntry>>> {
2638 self.absorber.reach_bitmaps_with(policy, cache)
2639 }
2640
2641 /// [`Absorber::reach_tables_with`] — the reach table paired with its oid
2642 /// index, for the negotiation.
2643 pub(crate) fn reach_tables_with(
2644 &self,
2645 policy: crate::reach::ReachPolicy,
2646 cache: bool,
2647 ) -> Result<(Arc<Vec<ReachEntry>>, Arc<HashMap<String, u32>>)> {
2648 self.absorber.reach_tables_with(policy, cache)
2649 }
2650
2651 /// The commit oid set, raw, shared. See [`Derived::commit_raw`].
2652 pub(crate) fn commit_oids_raw(
2653 &self,
2654 ) -> Result<Option<Arc<std::collections::HashSet<Vec<u8>>>>> {
2655 self.absorber.commit_oids_raw()
2656 }
2657
2658 pub(crate) fn refold(&self) -> Result<()> {
2659 self.absorber.refold()
2660 }
2661}
2662
2663impl<S: ObjectIndex> Absorber<S> {
2664 /// `shared` is the writer's descriptor on the same file
2665 /// ([`crate::archive_write::ArchiveWrite::blobs_shared`]); `None` opens one,
2666 /// which is what a caller with no writer does.
2667 fn open(
2668 blobs: &Path,
2669 shared: Option<Arc<File>>,
2670 hash: GitHashKind,
2671 objects: ObjectReadStack<S>,
2672 exploded: Arc<ExplodedCache>,
2673 ) -> Result<Self> {
2674 let reader = match shared {
2675 Some(f) => f,
2676 None => Arc::new(
2677 File::open(blobs)
2678 .with_context(|| format!("opening {} for reads", blobs.display()))?,
2679 ),
2680 };
2681 let label = blobs
2682 .parent()
2683 .map(|p| p.display().to_string())
2684 .unwrap_or_else(|| blobs.display().to_string());
2685 Ok(Self {
2686 blobs: blobs.to_path_buf(),
2687 reader,
2688 map: crate::archive_map::ArchiveMap::new(blobs),
2689 hash,
2690 objects,
2691 exploded,
2692 derived: RwLock::new(Derived::default()),
2693 graph_read: std::sync::atomic::AtomicU64::new(0),
2694 graph_recomputed: std::sync::atomic::AtomicU64::new(0),
2695 reach_carried: std::sync::atomic::AtomicU64::new(0),
2696 reach_rebuilt: std::sync::atomic::AtomicU64::new(0),
2697 ordinal_folded: std::sync::atomic::AtomicU64::new(0),
2698 ordinal_tail: std::sync::atomic::AtomicU64::new(0),
2699 packs: Mutex::new(PackState::default()),
2700 gate: Mutex::new(()),
2701 progress: crate::progress::IndexProgress::new(&label),
2702 cancel: std::sync::atomic::AtomicBool::new(false),
2703 label,
2704 watchdog: RwLock::new(crate::memguard::global()),
2705 scanned: Mutex::new(HashMap::new()),
2706 scan_build: Mutex::new(()),
2707 unindexable: Mutex::new(HashMap::new()),
2708 })
2709 }
2710
2711 /// **Tree payloads, on demand** — [`crate::reach::TreeSource`] over this
2712 /// store.
2713 ///
2714 /// One lookup per tree the walk actually visits, instead of a resident map
2715 /// of every tree in the repository. Where the bytes come from is
2716 /// [`Absorber::resolved`]'s business and nobody else's: the exploded cache
2717 /// if the tree is in it, its own delta chain out of the mmapped pack if it
2718 /// is not — and the read then writes it back, so a second walk finds it
2719 /// warm.
2720 ///
2721 /// It must not take the `derived` lock, and it does not: `resolved` reads
2722 /// the exploded pools, the `objects` index and the pending packs, none of
2723 /// which is `derived`. Both callers build one of these **inside** their
2724 /// `derived.write()` section, and that is only sound because of this
2725 /// sentence.
2726 fn tree_source(&self) -> TreesFromStore<'_, S> {
2727 TreesFromStore { abs: self }
2728 }
2729
2730 /// Packs whose objects are not in the index yet — §13.12's bit, counted.
2731 fn unindexed_packs(&self) -> usize {
2732 self.packs.lock().expect("pack state lock").unabsorbed
2733 }
2734
2735 /// **Index everything durable, here, now — the ack path's half of §13.9.**
2736 ///
2737 /// # The two hours, in one function
2738 ///
2739 /// MEASURED by lane P2 on the oden rig 2026-08-30. `git.receive_pack.checking`
2740 /// fired at 07:43:49; its very first `peel` of a pushed tip missed the
2741 /// exploded table and missed the `objects` index (the worker was minutes
2742 /// into a 13 948 244-object pack), and fell into [`Self::scan_pending`],
2743 /// which ran **a second, read-only, throw-away resolve of the whole 8.12 GiB
2744 /// pack on the connection thread**:
2745 ///
2746 /// ```text
2747 /// 09:20:21 index.scanned "a reader scanned a pending pack for itself:
2748 /// 13948244 objects in 5792442 ms"
2749 /// ```
2750 ///
2751 /// 1h36m32s on the 16 GB box and 1h44m14s on the 8 GB one, for rows it then
2752 /// threw away with the comment *"keeping them would be the index, and the
2753 /// worker is building that"*. That sentence is the defect: two resolvers
2754 /// over the same bytes, each holding its own O(pack) `PackWalk::entries` and
2755 /// oid table, on four vCPUs — which is also **why the watchdog paused**. The
2756 /// reader's second copy is what ate the `MemAvailable` the indexer tripped
2757 /// over, and neither pass ever finished the check.
2758 ///
2759 /// # Who may call this
2760 ///
2761 /// **The ack path, and nothing else.** It takes [`Absorber::gate`], so it
2762 /// waits for whoever is absorbing — and *"nobody waits on the indexer"* is a
2763 /// law with its own red test
2764 /// (`a_read_before_the_drain_is_served_from_the_pack_and_never_waits`): a
2765 /// clone, an `ls-remote`, a `control status` must be answered while the
2766 /// drain is parked, and they still are, through [`Self::scan_pending`].
2767 ///
2768 /// The connectivity check is the one caller for which that is the wrong
2769 /// trade. It has to prove that every object reachable from 2874 new tips is
2770 /// present, which means reading them; an entry's oid exists only once its
2771 /// delta chain has been applied; so the pack must be resolved before the
2772 /// check can answer, and the only question is whether it is resolved **once
2773 /// or twice**. This makes it once. `git receive-pack` makes the same choice
2774 /// and calls it `index-pack`.
2775 ///
2776 /// If the account worker already holds the gate, this blocks on it and then
2777 /// finds `is_absorbed` set and returns: whoever got there first did the work.
2778 ///
2779 /// Returns how many packs this call absorbed; `0` means it could not (it is
2780 /// nested inside an absorb of the only pending pack, or there was nothing
2781 /// to do).
2782 fn index_pending_now(&self) -> Result<usize> {
2783 if self.unindexed_packs() == 0 {
2784 return Ok(0);
2785 }
2786 // **Nested**: this thread is already inside `absorb_gated` and holds the
2787 // gate, so taking it again is a deadlock, not a wait. The other pending
2788 // packs are still ours to absorb inline — that is lane U's case, a thin
2789 // push whose base sits in a pack pushed a moment earlier — and the one
2790 // we are in the middle of is not.
2791 if let Some(current) = IN_ABSORB.with(|c| c.get()) {
2792 let jobs: Vec<PendingPack> = {
2793 let s = self.packs.lock().map_err(|_| anyhow!("pack state poisoned"))?;
2794 s.pending()
2795 };
2796 return self.absorb_each(&jobs, Some(current));
2797 }
2798 let _gate = self.gate.lock().map_err(|_| anyhow!("absorb gate poisoned"))?;
2799 let jobs: Vec<PendingPack> = {
2800 let s = self.packs.lock().map_err(|_| anyhow!("pack state poisoned"))?;
2801 s.pending()
2802 };
2803 // `absorb_gated` is a no-op for a pack somebody absorbed while we were
2804 // queued on the gate, so the loser of the race pays nothing.
2805 self.absorb_each(&jobs, None)
2806 }
2807
2808 /// **Absorb the pending packs only if nobody else is absorbing.**
2809 ///
2810 /// `Some(n)` = the gate was free and this caller did the work; `None` = the
2811 /// worker (or another reader) holds the gate — possibly paused by the
2812 /// memory watchdog for as long as the box is short — and this caller did
2813 /// **not** wait for it. Rickard's rule, 2026-08-30: *nobody waits on the
2814 /// indexer.* A read that needs the index now answers from the pack for
2815 /// itself ([`Self::scan_pending`]) and a read that needs the whole graph
2816 /// says so by name rather than blocking behind a kernel pack.
2817 fn try_absorb_pending(&self) -> Result<Option<usize>> {
2818 if self.unindexed_packs() == 0 {
2819 return Ok(Some(0));
2820 }
2821 let _gate = match self.gate.try_lock() {
2822 Ok(g) => g,
2823 Err(std::sync::TryLockError::WouldBlock) => return Ok(None),
2824 Err(std::sync::TryLockError::Poisoned(_)) => bail!("absorb gate poisoned"),
2825 };
2826 let jobs: Vec<PendingPack> = {
2827 let s = self
2828 .packs
2829 .lock()
2830 .map_err(|_| anyhow!("pack state poisoned"))?;
2831 s.pending()
2832 };
2833 self.absorb_each(&jobs, None).map(Some)
2834 }
2835
2836 /// **A read served from the pack on the reader's own request, never by
2837 /// waiting for the worker.**
2838 ///
2839 /// Every oid in `wants` is looked up in the `objects` table first — the
2840 /// rows the worker has already landed, batch by batch, are visible there
2841 /// with no lock in common with it. The ones still missing, if any pack is
2842 /// still un-indexed, are found by **resolving the pending packs read-only
2843 /// on this thread**: the same resolver the worker runs
2844 /// ([`crate::resolve::resolve_streaming`]), over the same mmapped bytes,
2845 /// with a bounded base set, but with **no sink, no gate and no rows
2846 /// written** — it produces rows for the caller and nothing else. The pass
2847 /// stops the moment every wanted oid has been seen.
2848 ///
2849 /// `capture` names one oid whose **content** the caller wants as well (a
2850 /// [`GitStore::content`] read); it rides through a
2851 /// [`crate::exploded::CaptureOne`] sink and comes back in the second slot.
2852 ///
2853 /// # Cost, stated
2854 ///
2855 /// An oid that is in a pending pack costs one read-only pass up to where it
2856 /// sits; an oid that is **absent** costs a full pass over every pending
2857 /// pack — for a kernel pack, minutes of one core. That is the price of
2858 /// never answering "absent" for an object whose bytes are durable, paid by
2859 /// the reader that asked and by nobody else. The rows it computes are not
2860 /// kept: keeping them would be the index, and the worker is building that.
2861 fn scan_pending(
2862 &self,
2863 wants: &[&[u8]],
2864 capture: Option<&[u8]>,
2865 ) -> Result<(Vec<Option<crate::index_layout::IndexRow>>, Option<(GitObjectKind, Vec<u8>)>)> {
2866 use crate::index_layout::IndexRow;
2867 let mut out: Vec<Option<IndexRow>> = vec![None; wants.len()];
2868 let mut missing: HashMap<&[u8], Vec<usize>> = HashMap::with_capacity(wants.len());
2869 for (i, w) in wants.iter().enumerate() {
2870 missing.entry(*w).or_default().push(i);
2871 }
2872 if missing.is_empty() && capture.is_none() {
2873 return Ok((out, None));
2874 }
2875 // **No nested BUILD.** The resolver asks this absorber (as its
2876 // `BaseSource`) for a thin pack's external base, and that base may
2877 // itself be un-indexed — a second *scan build* started from inside the
2878 // first, and a third from inside that, without end.
2879 //
2880 // The guard used to answer `None` outright when re-entered, and that
2881 // was wider than the recursion it was written for: a base sitting in
2882 // **another pack that is already scanned** is one binary search away,
2883 // and refusing it made a thin push on top of a just-acked one fail
2884 // `[remote rejected] … missing necessary objects` — lane U's rig
2885 // finding, 2026-08-30. So a nested call now consults the scans that
2886 // exist and builds none: a lookup terminates, a build is what does not.
2887 thread_local! { static SCANNING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) }; }
2888 let nested = SCANNING.with(|s| s.get());
2889 struct Unset(bool);
2890 impl Drop for Unset {
2891 fn drop(&mut self) {
2892 if !self.0 {
2893 SCANNING.with(|s| s.set(false));
2894 }
2895 }
2896 }
2897 SCANNING.with(|s| s.set(true));
2898 let _unset = Unset(nested);
2899 let pending: Vec<PendingPack> = {
2900 let s = self
2901 .packs
2902 .lock()
2903 .map_err(|_| anyhow!("pack state poisoned"))?;
2904 s.pending()
2905 };
2906 if pending.is_empty() {
2907 return Ok((out, None));
2908 }
2909 let snap = self.map.snapshot()?;
2910 let mut content: Option<(GitObjectKind, Vec<u8>)> = None;
2911 for job in &pending {
2912 if missing.is_empty() && (capture.is_none() || content.is_some()) {
2913 break;
2914 }
2915 let scanned = match self.cached_scan(job.offset) {
2916 Some(s) => s,
2917 // Inside another scan: no build, so the recursion the guard
2918 // exists for cannot happen, and a pack nobody has scanned yet
2919 // is simply not consulted for this base.
2920 None if nested => continue,
2921 None => self.scanned_pack(&snap, job)?,
2922 };
2923 missing.retain(|oid, slots| match scanned.row(oid) {
2924 Some(r) => {
2925 for &i in slots.iter() {
2926 out[i] = Some(r.clone());
2927 }
2928 false
2929 }
2930 None => true,
2931 });
2932 if let Some(want) = capture
2933 && content.is_none()
2934 && let Some((entry, _)) = scanned.find(want)
2935 {
2936 let bytes: std::borrow::Cow<'_, [u8]> = match snap.get(job.offset, job.len) {
2937 Some(b) => std::borrow::Cow::Borrowed(b),
2938 None => std::borrow::Cow::Owned(self.read_extent(job.offset, job.len)?),
2939 };
2940 let sp = scanned.clone();
2941 let offset_of = move |oid: &[u8]| sp.find(oid).map(|(e, _)| sp.walk.entries[e].offset);
2942 content = Some(crate::resolve::materialize_entry(
2943 &bytes,
2944 &scanned.walk,
2945 entry,
2946 self.hash.oid_len(),
2947 &offset_of,
2948 self,
2949 )?);
2950 }
2951 }
2952 Ok((out, content))
2953 }
2954
2955 /// The scan of one pending pack **if it is already built** — a lookup, and
2956 /// nothing that could recurse. What a nested `scan_pending` is allowed.
2957 fn cached_scan(&self, offset: u64) -> Option<Arc<ScannedPack>> {
2958 self.scanned
2959 .lock()
2960 .unwrap_or_else(|e| e.into_inner())
2961 .get(&offset)
2962 .cloned()
2963 }
2964
2965 /// The scan of one pending pack: cached, or built now by one read-only
2966 /// pass (the same resolver, bounded bases, no sink, no rows written).
2967 fn scanned_pack(&self, snap: &crate::archive_map::Mapped, job: &PendingPack) -> Result<Arc<ScannedPack>> {
2968 if let Some(s) = self.scanned.lock().unwrap_or_else(|e| e.into_inner()).get(&job.offset) {
2969 return Ok(s.clone());
2970 }
2971 let _one_at_a_time = self.scan_build.lock().unwrap_or_else(|e| e.into_inner());
2972 if let Some(s) = self.scanned.lock().unwrap_or_else(|e| e.into_inner()).get(&job.offset) {
2973 return Ok(s.clone());
2974 }
2975 let t0 = std::time::Instant::now();
2976 let bytes: std::borrow::Cow<'_, [u8]> = match snap.get(job.offset, job.len) {
2977 Some(b) => std::borrow::Cow::Borrowed(b),
2978 None => std::borrow::Cow::Owned(self.read_extent(job.offset, job.len)?),
2979 };
2980 let walk = crate::pack_walk::walk(&bytes, self.hash.oid_len())?;
2981 let mut rows: Vec<(Vec<u8>, u32, u64)> = Vec::with_capacity(walk.entries.len());
2982 // A reader's own budget: a quarter of the worker's, no watchdog —
2983 // the pause is the worker's and only the worker's.
2984 let limits = crate::resolve::ResolveLimits {
2985 base_budget: (crate::resolve::base_budget_from_env() / 4).max(1),
2986 cancel: Some(&self.cancel),
2987 label: &self.label,
2988 ..crate::resolve::ResolveLimits::none()
2989 };
2990 // Entry index = position in offset order; the rows come out in
2991 // resolution order, so map offset → index by binary search.
2992 let report = crate::resolve::resolve_streaming(
2993 &bytes,
2994 &walk,
2995 self.hash,
2996 job.offset,
2997 self,
2998 &crate::exploded::NoSink,
2999 limits,
3000 &mut |batch| {
3001 for r in batch.drain(..) {
3002 let pack_off = r.offset - job.offset;
3003 let idx = walk
3004 .entries
3005 .binary_search_by_key(&pack_off, |e| e.offset)
3006 .map_err(|_| anyhow!("resolved row at {pack_off} has no walk entry"))?;
3007 rows.push((r.oid, idx as u32, r.uncompressed_size));
3008 }
3009 Ok(())
3010 },
3011 )
3012 .context("resolving a pending pack read-only for lookups")?;
3013 rows.sort_unstable_by(|a, b| a.0.cmp(&b.0));
3014 let scanned = Arc::new(ScannedPack {
3015 walk,
3016 rows,
3017 archive_offset: job.offset,
3018 });
3019 tracing::info!(
3020 target: "gunnar::index",
3021 event = "index.scanned",
3022 repo = %self.label,
3023 pack_id = job.pack_id,
3024 objects = scanned.rows.len(),
3025 reinflations = report.reinflations,
3026 elapsed_ms = t0.elapsed().as_millis() as u64,
3027 "a reader scanned a pending pack for itself: {} objects in {} ms",
3028 scanned.rows.len(),
3029 t0.elapsed().as_millis()
3030 );
3031 self.scanned
3032 .lock()
3033 .unwrap_or_else(|e| e.into_inner())
3034 .insert(job.offset, scanned.clone());
3035 Ok(scanned)
3036 }
3037
3038 /// **Drop everything this store holds that can be rebuilt** — what the
3039 /// watchdog's flush reaches: the readers' scanned packs (`scanned`, ~1.2
3040 /// GB on linux, rebuilt by the next lookup's pass) and the exploded
3041 /// table's oid tree and batch cache. The rows on disk are untouched.
3042 ///
3043 /// **Returns the rows it actually dropped**, and that number is the point.
3044 /// Lane P2's rig logged this event 190 and 627 times with
3045 /// `scanned_rows_dropped=0` on every single one and kept waiting anyway,
3046 /// because the caller could not tell a spill that freed a gigabyte from one
3047 /// that freed nothing. Three zeros in a row now end the pause
3048 /// ([`crate::memguard::PauseEnd::NothingToGive`]).
3049 ///
3050 /// The exploded table's own caches are an mmap index and a small batch
3051 /// cache; they are dropped here too and deliberately not counted, because
3052 /// they are not where a kernel-sized pack's memory is.
3053 fn spill(&self) -> u64 {
3054 let dropped = {
3055 let mut s = self.scanned.lock().unwrap_or_else(|e| e.into_inner());
3056 let n: usize = s.values().map(|p| p.rows.len()).sum();
3057 s.clear();
3058 n
3059 };
3060 self.exploded.drop_caches();
3061 tracing::info!(
3062 target: "gunnar::index",
3063 event = "index.spilled",
3064 repo = %self.label,
3065 scanned_rows_dropped = dropped,
3066 "the store dropped its rebuildable caches for the memory watchdog"
3067 );
3068 dropped as u64
3069 }
3070
3071 /// This store's watchdog (see the field).
3072 fn watchdog(&self) -> Arc<crate::memguard::MemWatchdog> {
3073 self.watchdog.read().unwrap_or_else(|e| e.into_inner()).clone()
3074 }
3075
3076 /// One oid, never waiting: the index, then the pending packs.
3077 fn lookup_never_wait(&self, oid: &[u8]) -> Result<Option<crate::index_layout::IndexRow>> {
3078 if let Some(r) = self.objects.lookup(oid) {
3079 return Ok(Some(r));
3080 }
3081 if self.unindexed_packs() == 0 {
3082 return Ok(None);
3083 }
3084 let (mut rows, _) = self.scan_pending(&[oid], None)?;
3085 Ok(rows.pop().flatten())
3086 }
3087
3088 /// Many oids, never waiting: one index pass, then one read-only pass over
3089 /// the pending packs for whatever the index did not have.
3090 fn lookup_batch_never_wait(&self, oids: &[&[u8]]) -> Result<Vec<Option<crate::index_layout::IndexRow>>> {
3091 let mut rows = self.objects.lookup_batch(oids);
3092 if self.unindexed_packs() == 0 || rows.iter().all(Option::is_some) {
3093 return Ok(rows);
3094 }
3095 let missing: Vec<&[u8]> = oids
3096 .iter()
3097 .zip(&rows)
3098 .filter(|(_, r)| r.is_none())
3099 .map(|(o, _)| *o)
3100 .collect();
3101 let (found, _) = self.scan_pending(&missing, None)?;
3102 let mut by_oid: HashMap<&[u8], crate::index_layout::IndexRow> = HashMap::with_capacity(found.len());
3103 for (o, r) in missing.iter().zip(found) {
3104 if let Some(r) = r {
3105 by_oid.insert(*o, r);
3106 }
3107 }
3108 for (o, slot) in oids.iter().zip(rows.iter_mut()) {
3109 if slot.is_none()
3110 && let Some(r) = by_oid.get(*o)
3111 {
3112 *slot = Some(r.clone());
3113 }
3114 }
3115 Ok(rows)
3116 }
3117
3118 /// The bit going clear: this pack is durable and its objects are not in.
3119 ///
3120 /// Skips a pack the drain already absorbed — the ack path queues *after* it
3121 /// has handed the extent to the channel, so the worker can legitimately be
3122 /// finished before this runs, and clearing a bit for a pack that is already
3123 /// indexed would leave it clear for ever.
3124 fn queue(&self, pack_id: u64, extent: Extent) -> Result<()> {
3125 let mut s = self
3126 .packs
3127 .lock()
3128 .map_err(|_| anyhow!("pack state poisoned"))?;
3129 s.note(pack_id, extent);
3130 Ok(())
3131 }
3132
3133 /// **The bit, derived from the two durable facts, on open.**
3134 ///
3135 /// `journal` is every row this archive's journal holds, in append order.
3136 /// [`acked_packs`](crate::archive_write::acked_packs) is what turns that into
3137 /// the packs: its index *is* the pack ordinal — dense by construction, and
3138 /// re-derived the same way by every later open. For each one, the question is
3139 /// only *does this pack have any rows in the index*, which
3140 /// [`ObjectReadStack::extents_with_rows`] answers in one early-terminating
3141 /// scan rather than per pack.
3142 ///
3143 /// Returns the packs whose bit came up **clear**: durable bytes, no rows. The
3144 /// caller re-queues them on the account's channel, which is what makes a
3145 /// pack that was mid-absorb when the process died get absorbed by the next
3146 /// one instead of being durable-but-invisible for ever.
3147 ///
3148 /// The verbatim extents are also handed to [`Derived::packs`] — *all* of
3149 /// them, not only the unabsorbed ones. That is what lets [`BaseSource`]
3150 /// re-derive a delta base's content from the truth after a restart; without
3151 /// it a reopened store knows the rows of a pack it can no longer find the
3152 /// bytes of.
3153 ///
3154 /// # The one case where "no rows" does not mean "never absorbed" — and the
3155 /// third durable fact that now tells them apart
3156 ///
3157 /// [`GitOps::gc`] is the only thing that removes rows. A pack **all** of
3158 /// whose objects it finds dead therefore ends with a journal extent and no
3159 /// rows, which is byte for byte the state a pack that was in flight when the
3160 /// machine died leaves behind. This diff cannot tell them apart, and it must
3161 /// not guess: re-queueing the first resurrects every dead object, and *not*
3162 /// re-queueing the second loses a pack whose bytes were acked.
3163 ///
3164 /// So `gc` says which it is, durably, before it drops a row:
3165 /// [`retire_packs`](crate::archive_write::retire_packs) appends a tombstone
3166 /// naming the retired pack's offset, and a pack named by one is never
3167 /// re-queued here. The bit is still derived from durable facts only — there
3168 /// are now three of them (the journal's pack rows, the journal's tombstones,
3169 /// the index's rows) and none of them is a cache of another.
3170 ///
3171 /// A **partly** dead pack is not tombstoned and still has rows, so it takes
3172 /// the `absorbed` branch exactly as it always did.
3173 ///
3174 /// A tombstoned pack that *does* still have rows is a `gc` that was killed
3175 /// between the tombstone and the drop. It is treated as absorbed, which is
3176 /// what its rows say: nothing is lost, and the next `gc` finishes the job.
3177 ///
3178 /// Its extent still goes to [`Derived::packs`] either way. The blob file is
3179 /// append-only and `gc` truncates nothing, so a retired pack's bytes are
3180 /// still exactly where the journal says they are — and a live object may
3181 /// legitimately be a delta against a base that is itself unreachable, so
3182 /// dropping the extent from that list would break the resolve rather than
3183 /// tidy it.
3184 ///
3185 /// # The second durable fact: §14's exploded table
3186 ///
3187 /// A pack counts as absorbed only if the exploded table is **whole**, which
3188 /// is `exploded.rows() >= objects.len()` — one row per object, because §14's
3189 /// table is eager and skips nothing. If it is short, the table has been
3190 /// dropped (or a process died between the two commits) and **no** pack counts
3191 /// as absorbed: every one is re-queued and re-exploded from the verbatim
3192 /// truth. That is the whole of "droppable": deleting the file is a supported
3193 /// operation whose only cost is one re-ingest, and §13.12's rule holds
3194 /// unchanged over it — absent means fall back, never means wrong.
3195 ///
3196 /// `>=` and not `==` on purpose. A resolve that failed part way leaves rows
3197 /// for the objects it got to, and a `gc` retires both tables together, so the
3198 /// table can legitimately hold *more* than the index names; only being
3199 /// **short** is evidence of a gap.
3200 fn adopt_journal(&self, journal: &[Extent]) -> Result<Vec<PendingPack>> {
3201 if journal.is_empty() {
3202 return Ok(Vec::new());
3203 }
3204 // The packs, in ordinal order, and the offsets a `gc` retired. A
3205 // tombstone is a row and not a pack, so it holds no ordinal.
3206 let packs = crate::archive_write::acked_packs(journal);
3207 let retired = crate::archive_write::retired_offsets(journal);
3208 if packs.is_empty() {
3209 return Ok(Vec::new());
3210 }
3211 // Under `Graph` the table is short **by design** — commits and trees
3212 // only — so the row-count comparison would call every store dropped and
3213 // re-queue every pack on every open. It is weakened to "not empty",
3214 // which still catches the case this rule exists for (the file deleted),
3215 // and is stated rather than hidden: making it exact needs a durable
3216 // commit-and-tree count, which the index does not publish today.
3217 // Only `Full` explodes one row per object, so only `Full` can compare
3218 // counts. Under `Commits` (the shipped default) and `Graph` the eager
3219 // pool is short **by design**, and the comparison would call every store
3220 // dropped and re-queue every pack on every open — a Linux-sized re-index
3221 // per restart. The weakened test still catches what the rule exists for
3222 // (the file deleted) and is stated rather than hidden: making it exact
3223 // needs a durable per-kind count, which the index does not publish.
3224 //
3225 // The LAZY pool is deliberately not consulted. Its size is a function of
3226 // what has been READ, so it says nothing about whether a pack was
3227 // absorbed, and an eviction must never look like a lost index.
3228 let exploded_is_whole = match self.exploded.policy() {
3229 crate::exploded_arrow::ExplodePolicy::Full => {
3230 self.exploded.eager_rows()? >= self.objects.len() as u64
3231 }
3232 crate::exploded_arrow::ExplodePolicy::Off => true,
3233 _ => self.objects.len() == 0 || self.exploded.eager_rows()? > 0,
3234 };
3235 let has_rows = if exploded_is_whole {
3236 self.objects.extents_with_rows(&packs)?
3237 } else {
3238 vec![false; packs.len()]
3239 };
3240 {
3241 let mut d = self
3242 .derived
3243 .write()
3244 .map_err(|_| anyhow!("derived poisoned"))?;
3245 d.packs = packs.clone();
3246 }
3247 let mut s = self
3248 .packs
3249 .lock()
3250 .map_err(|_| anyhow!("pack state poisoned"))?;
3251 let mut requeue = Vec::new();
3252 for (i, (&extent, &absorbed)) in packs.iter().zip(&has_rows).enumerate() {
3253 let pack_id = i as u64;
3254 if absorbed {
3255 s.mark_absorbed(pack_id, extent);
3256 } else if retired.contains(&extent.0) {
3257 // A `gc` dropped every row this pack had and said so durably
3258 // *before* dropping them. There is no index work owed for it,
3259 // ever: re-queueing it is precisely how its dead objects would
3260 // come back. The bit goes **up** rather than being left clear,
3261 // because a clear bit is what makes a read fall back to
3262 // absorbing — which is the same resurrection by another door.
3263 s.mark_absorbed(pack_id, extent);
3264 } else {
3265 s.note(pack_id, extent);
3266 requeue.push(PendingPack {
3267 pack_id,
3268 offset: extent.0,
3269 len: extent.1,
3270 resumed: true,
3271 });
3272 }
3273 }
3274 Ok(requeue)
3275 }
3276
3277 /// **The indexer's half of the split**, and not one of the twelve.
3278 ///
3279 /// Reads each pending pack's verbatim bytes back out of the archive,
3280 /// resolves them to oids ([`crate::resolve`]), appends the rows to the
3281 /// `objects` table and re-folds the derived tables. This is what §13.9-12
3282 /// puts behind the channel; it runs after the ack, never before one.
3283 ///
3284 /// Two callers, one function: the account indexer's worker (through
3285 /// [`ObjectAbsorb`]) and a read that arrived before that worker did. The
3286 /// second is §13.12's fallback in its literal form — *slower, never wrong*.
3287 fn absorb_pending(&self) -> Result<usize> {
3288 // Nothing to do, and — this is the point — **no gate taken**: the common
3289 // case is a store whose drain has kept up, and it must not queue behind
3290 // one that is running.
3291 if self.unindexed_packs() == 0 {
3292 return Ok(0);
3293 }
3294 let _gate = self
3295 .gate
3296 .lock()
3297 .map_err(|_| anyhow!("absorb gate poisoned"))?;
3298 let jobs: Vec<PendingPack> = {
3299 let s = self
3300 .packs
3301 .lock()
3302 .map_err(|_| anyhow!("pack state poisoned"))?;
3303 s.pending()
3304 };
3305 self.absorb_each(&jobs, None)
3306 }
3307
3308 /// **Every pack it can, skipping the ones it has already given up on.**
3309 ///
3310 /// The one loop all four absorb entry points run (LAW 5): they were four
3311 /// copies of `for job in &jobs { self.absorb_gated(job)?; }`, and that `?`
3312 /// is the bug this function exists to remove. Two rules, and both are
3313 /// consequences of the same sentence — *work that can never succeed must
3314 /// stop and say why*:
3315 ///
3316 /// 1. **A failure does not abandon the packs behind it.** The loop runs to
3317 /// the end and reports the FIRST failure afterwards, so a pack that
3318 /// cannot be indexed no longer stops the push that arrived after it from
3319 /// being indexed and acked.
3320 /// 2. **A pack that has been given up on is not attempted again.** After
3321 /// [`MAX_ABSORB_ATTEMPTS`] it is recorded in
3322 /// [`Absorber::unindexable`] with the reason, logged once as
3323 /// `index.unindexable`, and skipped from then on. Its objects are then
3324 /// genuinely absent from the index, which every read path already knows
3325 /// how to say — a push that needs one is refused by name at
3326 /// `external_bases_exist`, and a clone that needs one fails the
3327 /// connectivity check. What no longer happens is the store re-resolving
3328 /// it on every request for the rest of the process's life.
3329 ///
3330 /// `skip` is the pack this thread is standing inside an absorb of
3331 /// ([`IN_ABSORB`]).
3332 fn absorb_each(&self, jobs: &[PendingPack], skip: Option<u64>) -> Result<usize> {
3333 let mut absorbed = 0usize;
3334 let mut first: Option<anyhow::Error> = None;
3335 for job in jobs {
3336 if skip == Some(job.pack_id) || self.given_up_on(job.pack_id) {
3337 continue;
3338 }
3339 match self.absorb_gated(job) {
3340 Ok(()) => absorbed += 1,
3341 Err(e) => {
3342 // Cancellation is the repository being deleted: there is no
3343 // queue left worth draining and nothing to record.
3344 if self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3345 return Err(e);
3346 }
3347 self.note_absorb_failure(job, &e);
3348 if first.is_none() {
3349 first = Some(e);
3350 }
3351 }
3352 }
3353 }
3354 match first {
3355 None => Ok(absorbed),
3356 Some(e) => Err(e),
3357 }
3358 }
3359
3360 /// Every pack this store has given up on, ordinal order.
3361 fn unindexable_packs(&self) -> Vec<Unindexable> {
3362 let mut v: Vec<Unindexable> = self
3363 .unindexable
3364 .lock()
3365 .unwrap_or_else(|e| e.into_inner())
3366 .values()
3367 .cloned()
3368 .collect();
3369 v.sort_by_key(|u| u.pack_id);
3370 v
3371 }
3372
3373 /// Has this store stopped trying to index that pack?
3374 fn given_up_on(&self, pack_id: u64) -> bool {
3375 self.unindexable
3376 .lock()
3377 .unwrap_or_else(|e| e.into_inner())
3378 .contains_key(&pack_id)
3379 }
3380
3381 /// Count one failed absorb, and stop at [`MAX_ABSORB_ATTEMPTS`].
3382 ///
3383 /// The reason recorded is `root_cause`, not the outermost wrapper: that is
3384 /// the sentence the backend actually has to offer, and lane Z's rule is that
3385 /// the client gets it verbatim rather than a layer's paraphrase of it.
3386 fn note_absorb_failure(&self, job: &PendingPack, err: &anyhow::Error) {
3387 let giving_up;
3388 {
3389 let mut map = self.unindexable.lock().unwrap_or_else(|e| e.into_inner());
3390 // A pack already given up on is not attempted, so an entry here is
3391 // always a fresh count.
3392 let e = map.entry(job.pack_id).or_insert_with(|| Unindexable {
3393 pack_id: job.pack_id,
3394 extent: (job.offset, job.len),
3395 attempts: 0,
3396 reason: String::new(),
3397 });
3398 e.attempts += 1;
3399 e.reason = format!("{}", err.root_cause());
3400 if e.attempts < MAX_ABSORB_ATTEMPTS {
3401 let attempts = e.attempts;
3402 map.remove(&job.pack_id);
3403 tracing::warn!(
3404 target: "znippy.git",
3405 store = %self.label,
3406 pack = job.pack_id,
3407 attempts,
3408 max = MAX_ABSORB_ATTEMPTS,
3409 error = %err.root_cause(),
3410 "a pack could not be indexed; it stays pending and will be tried again"
3411 );
3412 return;
3413 }
3414 giving_up = Some(e.clone());
3415 }
3416 if let Some(u) = giving_up {
3417 tracing::error!(
3418 target: "znippy.git",
3419 store = %self.label,
3420 event = "index.unindexable",
3421 pack = u.pack_id,
3422 offset = u.extent.0,
3423 len = u.extent.1,
3424 attempts = u.attempts,
3425 error = %u.reason,
3426 "this store has STOPPED trying to index this pack after {} attempts. Its bytes \
3427 are durable and untouched, and every object it introduced is absent from the \
3428 index — a push that deltas against one is refused by name and a clone that \
3429 needs one fails its connectivity check, both immediately. Nothing will retry \
3430 it: re-push the pack, or delete and re-create the repository.",
3431 u.attempts
3432 );
3433 }
3434 }
3435
3436 /// One pack, **with [`Absorber::gate`] already held**.
3437 ///
3438 /// The order at the end is the correctness argument: the rows are in the
3439 /// index *before* the bit is cleared. A read that finds the bit still clear
3440 /// blocks on the gate and then finds the rows; a read that finds it set finds
3441 /// the rows too. There is no interleaving in which a durable object answers
3442 /// "absent" — which during negotiation would make a client withhold objects
3443 /// and lose data.
3444 fn absorb_gated(&self, job: &PendingPack) -> Result<()> {
3445 if self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3446 // The repository is being deleted: no pack still queued is worth a
3447 // byte of work, and the drain must get through its queue fast so
3448 // the store can be dropped and the directory removed.
3449 return Err(crate::resolve::ResolveError::cancelled());
3450 }
3451 {
3452 let s = self
3453 .packs
3454 .lock()
3455 .map_err(|_| anyhow!("pack state poisoned"))?;
3456 if s.is_absorbed(job.pack_id) {
3457 // Already in, by whoever reached it first. Re-resolving is now
3458 // *correct* — the exploded table is keyed by oid and the graph is
3459 // folded from it rather than pushed at, so a second absorb of one
3460 // pack changes nothing (that idempotence is what makes dropping
3461 // the table and re-queueing every pack a safe operation). It is
3462 // still a whole pack re-read, re-inflated and re-hashed for
3463 // nothing, so it is skipped.
3464 return Ok(());
3465 }
3466 }
3467 // From here to the end of `absorb_one` this thread holds the gate, and
3468 // the resolver is about to ask this same store for delta bases. See
3469 // [`IN_ABSORB`]: the re-entry must not take the gate again and must not
3470 // try to absorb the pack it is standing in.
3471 let _in = InAbsorb::enter(job.pack_id);
3472 self.absorb_one(job).map_err(|e| {
3473 // The entry stays pending by construction — it was never removed —
3474 // so the bytes are durable, the pack is still un-indexed, and reads
3475 // keep falling back rather than answering absent.
3476 e.context(format!(
3477 "absorbing pack {} at ({}, {}) — its bytes are durable and it stays \
3478 un-indexed; reads will keep falling back rather than answer absent",
3479 job.pack_id, job.offset, job.len
3480 ))
3481 })?;
3482 let mut s = self
3483 .packs
3484 .lock()
3485 .map_err(|_| anyhow!("pack state poisoned"))?;
3486 s.mark_absorbed(job.pack_id, (job.offset, job.len));
3487 drop(s);
3488 // The index answers for it now; the reader's scan is dead weight.
3489 self.scanned.lock().unwrap_or_else(|e| e.into_inner()).remove(&job.offset);
3490 Ok(())
3491 }
3492
3493 /// One pack: verbatim bytes in, `objects` rows **and** §14's exploded rows
3494 /// out, from **one** walk and **one** resolve.
3495 ///
3496 /// The exploded rows are not a second pass and not a second resolver (LAW 5):
3497 /// [`crate::resolve::resolve_walked`] hands every object it produces to the
3498 /// sink as it produces it, including the blob payloads it is about to drop,
3499 /// so the side table falls out of the pass that was already being made.
3500 ///
3501 /// Ordering is the correctness argument, and it is the same one the drain
3502 /// makes one level up: **the exploded rows are committed before the fold that
3503 /// reads them**, and the fold runs before this returns, so there is no
3504 /// interleaving in which the graph is folded over a table that does not yet
3505 /// hold this pack's commits.
3506 fn absorb_one(&self, job: &PendingPack) -> Result<()> {
3507 let t0 = std::time::Instant::now();
3508 self.progress.begin(job.pack_id, job.len, job.resumed);
3509 // **A live row every 10 s for as long as this pack takes**, carrying
3510 // `peak_rss` and the re-inflation count. Those two rode only on
3511 // `index.finished`, so lane P2's two rig boxes — which ran for 2h14m
3512 // and 2h12m and never finished a pack — produced neither, in exactly
3513 // the failure they were built for. The ticker is a thread and not a
3514 // hook inside the resolve loop on purpose: it prints while the resolver
3515 // is PAUSED too, which is when an operator most needs it.
3516 let _tick = crate::progress::ProgressTicker::start(self.progress.clone());
3517 let result = self.absorb_one_inner(job, t0);
3518 self.progress.sample_memory();
3519 self.progress.finish();
3520 result
3521 }
3522
3523 /// The body of [`absorb_one`](Self::absorb_one), so `finish` runs on every
3524 /// exit and the progress row never sticks at 99 %.
3525 fn absorb_one_inner(&self, job: &PendingPack, t0: std::time::Instant) -> Result<()> {
3526 use crate::progress::Phase;
3527 let progress = &self.progress;
3528 // **The pack is read where it already is.** `read_extent` was a
3529 // `pread` of the whole extent into a fresh `Vec` — 8.12 GiB of
3530 // anonymous heap for torvalds/linux, before one byte was inflated, on a
3531 // 16 GB box. The mapping costs nothing the kernel cannot reclaim
3532 // ([`crate::archive_map`]), and `MemAvailable` — what the watchdog
3533 // reads — knows that.
3534 let snap = self.map.snapshot()?;
3535 let bytes: std::borrow::Cow<'_, [u8]> = match snap.get(job.offset, job.len) {
3536 Some(b) => std::borrow::Cow::Borrowed(b),
3537 // Past the mapping: the file grew after the snapshot. Same bytes,
3538 // one `pread` — the exception, not the shape.
3539 None => std::borrow::Cow::Owned(self.read_extent(job.offset, job.len)?),
3540 };
3541 progress.set_phase(Phase::Walk);
3542 let walked = crate::pack_walk::walk(&bytes, self.hash.oid_len())?;
3543 let t_walk = t0.elapsed();
3544 let dog = self.watchdog();
3545 let spill = || self.spill();
3546 let limits = crate::resolve::ResolveLimits::for_indexer(
3547 &self.label,
3548 &self.cancel,
3549 progress,
3550 dog,
3551 &spill,
3552 );
3553 let mut batches = 0u64;
3554 let mut appended = 0u64;
3555 let mut entries: Vec<crate::index_layout::IndexEntry> = Vec::with_capacity(limits.row_batch);
3556 let report = crate::resolve::resolve_streaming(
3557 &bytes,
3558 &walked,
3559 self.hash,
3560 job.offset,
3561 self,
3562 &*self.exploded,
3563 limits,
3564 &mut |rows| {
3565 // **One batch of rows lands, then the next.** The exploded
3566 // tier already flushed itself every 64 MiB; the index rows
3567 // used to wait for the whole pack. Now the batch is the unit:
3568 // rows go to the `objects` table as they are produced, and the
3569 // pack's own "indexed" bit still goes up only after the LAST
3570 // batch — `mark_absorbed` is the caller's, after this returns.
3571 entries.clear();
3572 entries.extend(rows.iter().map(|r| r.index_entry()));
3573 rows.clear();
3574 self.objects.append(&entries)?;
3575 appended += entries.len() as u64;
3576 batches += 1;
3577 Ok(())
3578 },
3579 )?;
3580 drop(entries);
3581 let t_resolve = t0.elapsed() - t_walk;
3582 progress.set_phase(Phase::Flush);
3583 // Down before anything folds over it.
3584 self.exploded.flush()?;
3585
3586 {
3587 let mut d = self
3588 .derived
3589 .write()
3590 .map_err(|_| anyhow!("derived poisoned"))?;
3591 // A reopen already listed every acked extent (`adopt_journal`), so
3592 // this is the first absorb of a pack pushed in *this* process — or a
3593 // re-queued one that is already listed. `find` is what reads this
3594 // list, so a duplicate would not be wrong, only unbounded.
3595 if !d.packs.contains(&(job.offset, job.len)) {
3596 d.packs.push((job.offset, job.len));
3597 }
3598 }
3599 progress.set_phase(Phase::Fold);
3600 let t_fold0 = std::time::Instant::now();
3601 // The graph and the tree payloads are folded from the exploded table, not
3602 // pushed at from here. That is what makes a re-absorb idempotent — which
3603 // it has to be, because dropping the table re-queues every pack.
3604 self.refold()?;
3605 let _ = self.objects.maybe_rebuild()?;
3606 let t_fold = t_fold0.elapsed();
3607 // **The audit line**: every number is applied output of this absorb.
3608 // `peak_rss` is the process's VmHWM off /proc/self/status — the high
3609 // water mark of the whole server, which on the appliance is the box.
3610 tracing::info!(
3611 target: "gunnar::index",
3612 event = "index.finished",
3613 repo = %self.label,
3614 pack_id = job.pack_id,
3615 pack_bytes = job.len,
3616 peak_rss = crate::memguard::peak_rss_bytes(),
3617 mem_available = self.watchdog().available().unwrap_or(0),
3618 objects = report.rows,
3619 batches = batches,
3620 rows_appended = appended,
3621 reinflations = report.reinflations,
3622 evictions = report.evictions,
3623 watchdog_drops = report.watchdog_drops,
3624 peak_base_bytes = report.peak_base_bytes,
3625 pauses = report.pauses,
3626 paused_ms = report.paused_ms,
3627 walk_ms = t_walk.as_millis() as u64,
3628 resolve_ms = t_resolve.as_millis() as u64,
3629 fold_ms = t_fold.as_millis() as u64,
3630 total_ms = t0.elapsed().as_millis() as u64,
3631 resumed = job.resumed,
3632 "indexed {} objects in {} batches: walk {} ms, resolve {} ms, fold {} ms; peak bases {}, {} evictions, {} re-inflations, {} pauses ({} ms); peak rss {}",
3633 report.rows,
3634 batches,
3635 t_walk.as_millis(),
3636 t_resolve.as_millis(),
3637 t_fold.as_millis(),
3638 crate::memguard::fmt_bytes(report.peak_base_bytes),
3639 report.evictions,
3640 report.reinflations,
3641 report.pauses,
3642 report.paused_ms,
3643 crate::memguard::fmt_bytes(crate::memguard::peak_rss_bytes())
3644 );
3645 Ok(())
3646 }
3647
3648 /// Recompute everything derived from the object set: the commit graph, the
3649 /// tree payloads, the ordinal space, the generation numbers, and (lazily) the
3650 /// bitmaps.
3651 ///
3652 /// **The graph is folded from §14's exploded table, not accumulated.** That
3653 /// is the whole fix for the clean-reopen hole: commit and tree payloads are
3654 /// on disk, so this call produces the same graph after a restart as before
3655 /// one, and it is called at the end of [`GitStore::open_with_arms`] for
3656 /// exactly that reason. It also makes a re-absorb idempotent — the old
3657 /// accumulate-into-a-`Vec` shape doubled every commit if a pack was absorbed
3658 /// twice, which is why dropping the table and re-queueing everything was not
3659 /// a safe operation before this.
3660 ///
3661 /// **Generations are stored and must be recomputed in any fold** (§13,
3662 /// decided). A generation number is `1 + max(parents)`, so it is invalidated
3663 /// the moment history's shape changes — which is exactly what absorbing a
3664 /// pack does. [`assign_generations`] is the one writer of that number and it
3665 /// is called here, over the whole graph, rather than incrementally: an
3666 /// incremental update would have to know which descendants moved, and being
3667 /// wrong about that produces an ancestry cutoff that skips real ancestors.
3668 ///
3669 /// # Cost
3670 ///
3671 /// One range scan of the exploded table's commits and one of its trees, per
3672 /// absorbed pack. It is the same order as the `tail_oids` scan below, which
3673 /// this function already made on every absorb, and it reads **no blob
3674 /// payload** — that is what the kind index in
3675 /// [`crate::exploded`] is for.
3676 ///
3677 /// ## And that order is the whole repository, on every push
3678 ///
3679 /// Said plainly, because "the same order as the scan below" reads like a
3680 /// reassurance and is not one: **nothing here is proportional to the push.**
3681 /// Every call rebuilds every tree payload, every commit node, every
3682 /// generation number, a `HashMap<String, u32>` over every oid in the
3683 /// repository and a sorted `Vec<String>` of the same — for a push of one
3684 /// commit exactly as for a push of ten thousand.
3685 ///
3686 /// MEASURED on oden 2026-08-11, release, quiet box (`some avg10` 0.00). The
3687 /// **same** 100-commit increment, pushed onto the same history seeded to
3688 /// four different depths, server CPU for the push:
3689 ///
3690 /// | objects already in the repository | CPU |
3691 /// |---:|---:|
3692 /// | 9 614 | 0.12 s |
3693 /// | 35 350 | 0.23 s |
3694 /// | 103 988 | 0.45 s |
3695 /// | 206 925 | 0.77 s |
3696 ///
3697 /// That is a straight line: **≈88 ms fixed, plus ≈3.3 µs per object already
3698 /// stored**, and the fit predicts the four points to within 20 ms. It is
3699 /// also why a push is slower than the ack path suggests — §13.9 puts the
3700 /// index behind a channel, but `put_refs` checks every ref target with
3701 /// `has`, `has` calls `lookup_one`, and `lookup_one` absorbs pending packs
3702 /// first. The drain is off the *ack* and squarely on the *push*.
3703 ///
3704 /// This is not fixed here and the numbers are recorded so the next attempt
3705 /// starts from them rather than from a guess. The obvious lever — fold
3706 /// incrementally — is the one the paragraph above rules out for
3707 /// generations, so it needs its own argument and its own gate.
3708 fn refold(&self) -> Result<()> {
3709 // Everything derived is computed **before** the write lock, which is then
3710 // held only for the swap: the fold is the long part and nothing that
3711 // reads `derived` should queue behind it.
3712 //
3713 // Ordinals first — **read, not re-scanned, when the tail has not moved.**
3714 // S-082's fix, mirroring the commit-graph memo one paragraph down:
3715 // `objects.ordinal` beside the pack holds `oids_raw` (every oid in the
3716 // store, in ordinal = sorted-raw order), and a cold open reads its couple
3717 // of megabytes back sequentially in milliseconds instead of walking the
3718 // whole redb B-tree — REFOLD measured that scan at ~65% (0.45 s) of a
3719 // 136k-object reopen, and proved a flat buffer measured the same, so the
3720 // cost is the SCAN, not the allocation. It is a memo, not a second truth:
3721 // the redb tail stays authoritative and the file is discarded unless its
3722 // (epoch, count) both match the live tail, so a push, a gc or a torn
3723 // write all fall back to the scan and re-persist.
3724 //
3725 // The raw concatenation is BOTH the ordinal space and the validity token
3726 // for the commit graph below, so the graph is read whenever the object
3727 // set is unchanged; the hex form and the ordinal map fold out of the same
3728 // bytes, so ordinal `o` names the same object in every representation by
3729 // construction rather than by agreeing computations (LAW 5). The tail
3730 // hands oids in oid order, which is byte order over fixed-width oids and
3731 // therefore already the sorted concatenation the memo stores.
3732 //
3733 // **2026-09-04: the memo is gone, because the scan it spared is gone.**
3734 // The read stack persists its projection now (`objects.index`), and the
3735 // sorted oid list IS that index's oid column — one contiguous copy out
3736 // of a mapped buffer, plus a merge of the few rows redb still holds
3737 // since the last fold. No file of its own, no header, no epoch to
3738 // validate: the projection is current by construction. What is kept is
3739 // the provenance — how many oids came off the index and how many off
3740 // the tail — because that is the only thing that can prove the space
3741 // was not a scan (both paths yield identical bytes).
3742 let space = self.objects.oids_raw()?;
3743 self.ordinal_folded
3744 .store(space.folded_rows, std::sync::atomic::Ordering::Relaxed);
3745 self.ordinal_tail
3746 .store(space.tail_rows, std::sync::atomic::Ordering::Relaxed);
3747 let oids_raw = space.raw;
3748 let token = graph_token(&oids_raw);
3749
3750 // The commit graph — **read, not recomputed, when the object set has not
3751 // moved.** S-065's fix: `objects.graph` beside the pack holds the folded
3752 // graph with generations, written by this same call at derive time and
3753 // read here. It is not a second truth — it is a memoized projection
3754 // validated on every read against the authoritative object set (the
3755 // token is a blake3 over every oid in the store), and an absent file, a
3756 // stale token or a decode failure all fall back to the fold and
3757 // re-persist. Same shape as lane REFCAS persisting a tag's peeled target.
3758 //
3759 // On a fold, commit payloads are read out of §14's exploded table (no
3760 // tree payload — that was lane Q's 28.24 GB and most of the 50.9 GiB RSS
3761 // on `torvalds/linux`), parsed, and generation-numbered. That is the
3762 // whole of the O(commits) open cost this persistence removes.
3763 let graph: Vec<CommitNode> = match self.load_persisted_graph()? {
3764 Some((tok, g)) if tok == token => {
3765 self.graph_read.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3766 g
3767 }
3768 _ => {
3769 self.graph_recomputed
3770 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3771 let commits = self.exploded.of_kind(GitObjectKind::Commit)?;
3772 let g: Vec<CommitNode> = commits
3773 .iter()
3774 .map(|(oid, payload)| {
3775 let h = crate::object::parse_commit(payload);
3776 CommitNode {
3777 oid: hex::encode(oid),
3778 parents: h.parents,
3779 tree: h.tree,
3780 committer_time: h.committer_time,
3781 generation: 0,
3782 }
3783 })
3784 .collect();
3785 let g = assign_generations(g);
3786 // Persist for the next open. A write failure is not fatal to the
3787 // fold — the graph is correct in memory and the next open simply
3788 // recomputes — so it is logged, not propagated.
3789 if let Err(e) = self.persist_graph(&token, &g) {
3790 tracing::warn!(store = %self.label, "could not persist objects.graph: {e:#}");
3791 }
3792 g
3793 }
3794 };
3795
3796 // The same commit set `select` refuses a `want` against, in the raw form
3797 // it tests. Folded once here instead of once per request; see the field.
3798 // A row that does not decode makes the whole set `None`, which is the
3799 // decline `select` used to reach by returning early out of its own loop.
3800 let commit_raw = graph
3801 .iter()
3802 .map(|n| hex::decode(&n.oid).ok())
3803 .collect::<Option<std::collections::HashSet<Vec<u8>>>>()
3804 .map(Arc::new);
3805 // No hex `Vec<String>` and no `HashMap<String, u32>` are folded here any
3806 // more (S-082): `oids_raw` is the whole ordinal space, and oid ⇄ ordinal
3807 // is served off it by [`crate::reach::OrdinalSpace`] — a slice one way, a
3808 // binary search the other. That removed the ~0.13 s + ~0.08 s this fold
3809 // spent building those two per-oid `String` tables, and their resident
3810 // cost with them.
3811 // The graph-by-oid lookup, folded here so `reachable` borrows it instead
3812 // of rebuilding it per negotiation. Keyed by hex, the form the walk
3813 // keys by. Same lifecycle as `commit_raw`: rebuilt on every fold, so it
3814 // cannot outlive the graph it indexes.
3815 let graph_index: HashMap<String, u32> = graph
3816 .iter()
3817 .enumerate()
3818 .map(|(i, n)| (n.oid.clone(), i as u32))
3819 .collect();
3820
3821 let mut d = self
3822 .derived
3823 .write()
3824 .map_err(|_| anyhow!("derived poisoned"))?;
3825 // ── carry the reachability table across the fold ──────────────────
3826 //
3827 // The table describes immutable commits; the fold only renumbers the
3828 // ordinal space. Carry it (renumbered) while the graph has not outgrown
3829 // the last full build by more than the carry rule allows — else drop it
3830 // and let the next reader rebuild, which is what every fold used to do.
3831 // See `REACH_CARRY_MAX_FRACTION` for the measurement behind this.
3832 let carried: Option<Vec<ReachEntry>> = if d.reach.is_empty() {
3833 None
3834 } else {
3835 let grown = graph.len().saturating_sub(d.reach_full_len);
3836 let allowed = (d.reach_full_len / REACH_CARRY_MAX_FRACTION).max(REACH_CARRY_MIN_COMMITS);
3837 if grown > allowed {
3838 None
3839 } else {
3840 let oid_len = self.hash.oid_len();
3841 let old_space = crate::reach::OrdinalSpace::new(&d.oids_raw, oid_len);
3842 let new_space = crate::reach::OrdinalSpace::new(&oids_raw, oid_len);
3843 crate::reach::carry_reach(&d.reach, &old_space, &new_space, |c| graph_index.contains_key(c))
3844 }
3845 };
3846 tracing::info!(
3847 store = %self.label,
3848 reach_len = d.reach.len(),
3849 reach_full_len = d.reach_full_len,
3850 graph_len = graph.len(),
3851 carried = carried.as_ref().map(Vec::len),
3852 "fold: reachability table carry decision"
3853 );
3854 d.graph = graph;
3855 d.graph_index = graph_index;
3856 d.commit_raw = commit_raw;
3857 d.oids_raw = oids_raw;
3858 match carried {
3859 Some(entries) => {
3860 let index: HashMap<String, u32> = entries
3861 .iter()
3862 .enumerate()
3863 .map(|(i, e)| (e.commit.clone(), i as u32))
3864 .collect();
3865 d.reach = Arc::new(entries);
3866 d.reach_index = Arc::new(index);
3867 self.reach_carried
3868 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3869 }
3870 None => {
3871 // Over the ordinal space that just changed and not carriable;
3872 // both are rebuilt lazily by `reach_bitmaps_with`.
3873 d.reach = Arc::default();
3874 d.reach_index = Arc::default();
3875 }
3876 }
3877 Ok(())
3878 }
3879
3880 /// `(reach_carried, reach_rebuilt)` — how many folds since open carried the
3881 /// reachability table across (renumbered) vs how many reads built it from
3882 /// scratch. See [`tests::a_push_carries_the_reach_table_across_the_fold`].
3883 fn reach_source_stats(&self) -> (u64, u64) {
3884 (
3885 self.reach_carried.load(std::sync::atomic::Ordering::Relaxed),
3886 self.reach_rebuilt.load(std::sync::atomic::Ordering::Relaxed),
3887 )
3888 }
3889
3890 /// Where the folded commit graph is cached, beside the verbatim pack:
3891 /// `<root>/objects.graph`.
3892 fn graph_cache_path(&self) -> PathBuf {
3893 self.blobs.with_file_name(GRAPH_CACHE_FILE)
3894 }
3895
3896 /// The persisted graph and the token it was written under, or `None` when
3897 /// the cache is absent, truncated, an unknown version, or fails to decode —
3898 /// every one of which is a signal to recompute and re-persist rather than an
3899 /// error, because the cache is a memo and the exploded table is the truth.
3900 fn load_persisted_graph(&self) -> Result<Option<([u8; 32], Vec<CommitNode>)>> {
3901 let bytes = match std::fs::read(self.graph_cache_path()) {
3902 Ok(b) => b,
3903 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
3904 Err(e) => {
3905 return Err(e).with_context(|| {
3906 format!("reading {}", self.graph_cache_path().display())
3907 });
3908 }
3909 };
3910 // Frame: b"GRPH" (4) + version (1) + token (32) + Arrow IPC graph.
3911 if bytes.len() < GRAPH_CACHE_HEADER || &bytes[..4] != GRAPH_CACHE_MAGIC || bytes[4] != 1 {
3912 return Ok(None);
3913 }
3914 let mut token = [0u8; 32];
3915 token.copy_from_slice(&bytes[5..GRAPH_CACHE_HEADER]);
3916 match crate::graph::decode_graph(&bytes[GRAPH_CACHE_HEADER..]) {
3917 Ok(g) => Ok(Some((token, g))),
3918 Err(_) => Ok(None),
3919 }
3920 }
3921
3922 /// Write the folded graph and its token durably: encode, write a temp file,
3923 /// fsync it, then rename over the cache — the rename is the atomic publish,
3924 /// so a reader never sees a half-written graph.
3925 fn persist_graph(&self, token: &[u8; 32], nodes: &[CommitNode]) -> Result<()> {
3926 use std::io::Write as _;
3927 let ipc = crate::graph::encode_graph(nodes)?;
3928 let mut out = Vec::with_capacity(GRAPH_CACHE_HEADER + ipc.len());
3929 out.extend_from_slice(GRAPH_CACHE_MAGIC);
3930 out.push(1);
3931 out.extend_from_slice(token);
3932 out.extend_from_slice(&ipc);
3933
3934 let path = self.graph_cache_path();
3935 let tmp = path.with_extension("graph.tmp");
3936 {
3937 let mut f = File::create(&tmp)
3938 .with_context(|| format!("creating {}", tmp.display()))?;
3939 f.write_all(&out)
3940 .with_context(|| format!("writing {}", tmp.display()))?;
3941 f.sync_all()
3942 .with_context(|| format!("fsync {}", tmp.display()))?;
3943 }
3944 std::fs::rename(&tmp, &path)
3945 .with_context(|| format!("publishing {}", path.display()))?;
3946 Ok(())
3947 }
3948
3949 /// `(graph_read_from_disk, graph_recomputed)` — how many folds since open
3950 /// read the persisted graph vs recomputed it. The only thing that can prove
3951 /// the persisted path ran; see
3952 /// [`tests::a_reopen_reads_the_persisted_graph_it_does_not_recompute`].
3953 fn graph_source_stats(&self) -> (u64, u64) {
3954 (
3955 self.graph_read.load(std::sync::atomic::Ordering::Relaxed),
3956 self.graph_recomputed
3957 .load(std::sync::atomic::Ordering::Relaxed),
3958 )
3959 }
3960
3961 /// `(folded_rows, tail_rows)` of the ordinal space the last fold built — how
3962 /// many oids came straight off the persisted index's oid column and how many
3963 /// off a scan of the redb tail. The only thing that can prove the space was
3964 /// not a scan (LAW 2), because both sources yield identical bytes; see
3965 /// [`tests::the_ordinal_space_comes_off_the_index_not_a_scan`].
3966 fn ordinal_source_stats(&self) -> (u64, u64) {
3967 (
3968 self.ordinal_folded.load(std::sync::atomic::Ordering::Relaxed),
3969 self.ordinal_tail.load(std::sync::atomic::Ordering::Relaxed),
3970 )
3971 }
3972
3973 /// The bitmaps, built on first use after a fold and cached until the next.
3974 fn reach_bitmaps(&self) -> Result<Arc<Vec<ReachEntry>>> {
3975 self.reach_bitmaps_with(live_reach_policy(), true)
3976 }
3977
3978 /// [`Absorber::reach_bitmaps`] with the policy **named** and the cache
3979 /// optional. The bitmaps alone; [`Absorber::reach_tables_with`] is the one
3980 /// builder and this drops the index it also returns, for callers that seal
3981 /// the table or compare two of them and never key into one by oid.
3982 fn reach_bitmaps_with(
3983 &self,
3984 policy: crate::reach::ReachPolicy,
3985 cache: bool,
3986 ) -> Result<Arc<Vec<ReachEntry>>> {
3987 Ok(self.reach_tables_with(policy, cache)?.0)
3988 }
3989
3990 /// **The reach table and its oid index, as one pair that is always in
3991 /// step.** [`crate::reach::accumulate`] keys the table by commit oid, and
3992 /// building that index per negotiation is exactly the O(commits) waste this
3993 /// lane removes — so it is built once, here, beside the table, and handed
3994 /// back with it. The two travel together (both cached, both returned) so a
3995 /// caller can never pair a fresh index against a stale table or the reverse.
3996 ///
3997 /// `cache` is false for the differential guard, which builds the table at two
3998 /// very different caps over one store and requires the two to answer
3999 /// identically; letting either poison `d.reach` would make the second arm
4000 /// read the first one's table and the comparison would be of a thing against
4001 /// itself.
4002 fn reach_tables_with(
4003 &self,
4004 policy: crate::reach::ReachPolicy,
4005 cache: bool,
4006 ) -> Result<(Arc<Vec<ReachEntry>>, Arc<HashMap<String, u32>>)> {
4007 {
4008 let d = self
4009 .derived
4010 .read()
4011 .map_err(|_| anyhow!("derived poisoned"))?;
4012 if cache && !d.reach.is_empty() {
4013 // Two refcount bumps. This was `d.reach.clone()` — one deep copy
4014 // of every commit's roaring bitmap, per call, twice per request
4015 // — plus, before this lane, a whole `HashMap<&str,_>` rebuilt
4016 // from the table on every call at the negotiation site.
4017 return Ok((Arc::clone(&d.reach), Arc::clone(&d.reach_index)));
4018 }
4019 if d.graph.is_empty() {
4020 return Ok((Arc::default(), Arc::default()));
4021 }
4022 }
4023 let mut d = self
4024 .derived
4025 .write()
4026 .map_err(|_| anyhow!("derived poisoned"))?;
4027 // Built before the facts so it outlives them, and deliberately NOT
4028 // holding the `derived` guard: `Absorber::resolved` does not take that
4029 // lock, which is what makes a live tree lookup legal from inside this
4030 // write section at all.
4031 let trees = self.tree_source();
4032 let oid_len = self.hash.oid_len();
4033 let facts = crate::reach::ObjectFacts {
4034 ordinal: crate::reach::OrdinalSpace::new(&d.oids_raw, oid_len),
4035 trees: &trees,
4036 oid_len,
4037 };
4038 let started = std::time::Instant::now();
4039 let built = Arc::new(crate::reach::build_reach(&d.graph, &facts, policy));
4040 self.reach_rebuilt
4041 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4042 tracing::info!(
4043 store = %self.label,
4044 entries = built.len(),
4045 commits = d.graph.len(),
4046 elapsed_ms = started.elapsed().as_millis() as u64,
4047 cache,
4048 "built the reachability table from scratch"
4049 );
4050 // The commit count this build covered — the denominator the next fold's
4051 // carry rule divides by (`REACH_CARRY_MAX_FRACTION`).
4052 d.reach_full_len = d.graph.len();
4053 // The index over the table just built — commit oid → row — so the
4054 // negotiation looks a bitmap up by a hash rather than by rebuilding this
4055 // map first.
4056 let index: Arc<HashMap<String, u32>> = Arc::new(
4057 built
4058 .iter()
4059 .enumerate()
4060 .map(|(i, e)| (e.commit.clone(), i as u32))
4061 .collect(),
4062 );
4063 if cache {
4064 // `Arc::new` once each, then a refcount bump into the cache — the
4065 // second full copy the table line used to make
4066 // (`d.reach = built.clone()`) is gone, and the index is cached in
4067 // lockstep so the read arm above can hand out both.
4068 d.reach = Arc::clone(&built);
4069 d.reach_index = Arc::clone(&index);
4070 }
4071 Ok((built, index))
4072 }
4073
4074 /// The commit oid set, raw, as folded. See [`Derived::commit_raw`].
4075 fn commit_oids_raw(&self) -> Result<Option<Arc<std::collections::HashSet<Vec<u8>>>>> {
4076 Ok(self
4077 .derived
4078 .read()
4079 .map_err(|_| anyhow!("derived poisoned"))?
4080 .commit_raw
4081 .clone())
4082 }
4083
4084 // `ordinal_space()` lived here: `d.oids.clone()`, a fresh `Vec<String>` of
4085 // every oid in the store, handed to `reachable_oids` twice per request. It
4086 // is gone, and so is `d.oids` itself (S-082): the serve path borrows
4087 // `d.oids_raw` as an `OrdinalSpace` under the read guard it was already
4088 // going to take, and reads oid ⇄ ordinal straight off it.
4089
4090 /// `pread` an extent out of the verbatim archive, **into uninitialised
4091 /// capacity** — no zero-fill before the read.
4092 ///
4093 /// This used to be `vec![0u8; len]` + `read_exact_at`, and under emit
4094 /// concurrency the zeroing was not a detail: profiled on oden 2026-08-12
4095 /// (gunnar serving 32 concurrent clones, perf on the server pid), ~44 % of
4096 /// serve CPU was memory zeroing/copying — `memset` 18.1 % plus kernel
4097 /// page-zeroing 7.8 % — against ~12 % for one clone, because 32 threads
4098 /// churning pack-scale buffers recycle mimalloc freelist blocks that must
4099 /// each be memset before `pread` immediately overwrites every byte.
4100 ///
4101 /// Soundness: no reference to uninitialised memory is ever formed. The
4102 /// bytes are written through a raw pointer into the `Vec`'s spare
4103 /// capacity, `EINTR` retries, a short read refuses, and `set_len` runs
4104 /// only after every one of `len` bytes has been written.
4105 fn read_extent(&self, offset: u64, len: u64) -> Result<Vec<u8>> {
4106 use std::os::fd::AsRawFd;
4107 let want = usize::try_from(len).context("extent length overflows usize")?;
4108 let mut buf: Vec<u8> = Vec::with_capacity(want);
4109 let fd = self.reader.as_raw_fd();
4110 let mut filled = 0usize;
4111 while filled < want {
4112 let n = unsafe {
4113 libc::pread(
4114 fd,
4115 buf.as_mut_ptr().add(filled).cast(),
4116 want - filled,
4117 i64::try_from(offset + filled as u64).context("extent offset overflows off_t")?,
4118 )
4119 };
4120 if n < 0 {
4121 let err = std::io::Error::last_os_error();
4122 if err.kind() == std::io::ErrorKind::Interrupted {
4123 continue;
4124 }
4125 return Err(err).with_context(|| {
4126 format!("reading ({offset}, {len}) out of {}", self.blobs.display())
4127 });
4128 }
4129 if n == 0 {
4130 bail!(
4131 "short read at ({offset}, {len}) out of {}: {filled} of {want} bytes before \
4132 EOF — the archive is truncated relative to its index",
4133 self.blobs.display()
4134 );
4135 }
4136 filled += n as usize;
4137 }
4138 // Every byte of `want` is now initialised.
4139 unsafe { buf.set_len(want) };
4140 Ok(buf)
4141 }
4142}
4143
4144/// **The drain's object-level ingress**, and the reason a push ends in object
4145/// rows without anybody reading first.
4146///
4147/// The account indexer holds this as an `Arc<dyn ObjectAbsorb>` and calls it once
4148/// per drained job, on its own thread, after the ack. It is the *same* absorb a
4149/// falling-back read takes, gate and all (LAW 5): there is no background copy of
4150/// this logic to drift.
4151impl<S: ObjectIndex> ObjectAbsorb for Absorber<S> {
4152 fn absorb(&self, job: IndexJob) -> Result<()> {
4153 // **The memory watchdog, BEFORE the pack and BEFORE the gate**
4154 // ([`crate::memguard`]). Inside the pack the resolver checks again
4155 // every few thousand objects; this is the check that keeps a burst of
4156 // packs from starting one more absorb on a box that is already short.
4157 // Taken before the gate on purpose: a paused worker holds nothing —
4158 // not the gate, not a row, not a base — so a reader that wants to
4159 // absorb for itself can, and every other reader scans the pack.
4160 if self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4161 return Err(crate::resolve::ResolveError::cancelled());
4162 }
4163 let dog = self.watchdog();
4164 if dog.breached().is_some() {
4165 self.progress.begin(job.pack_id, job.len, false);
4166 self.progress.set_phase(crate::progress::Phase::Paused);
4167 self.progress.add_pause();
4168 let r = dog.pause_until_ok(&self.label, Some(&self.cancel), &mut || Ok(self.spill()));
4169 self.progress.finish();
4170 let paused = r?;
4171 // The pause before the pack has the same rule as the one inside it:
4172 // a give-up is not a resume. Starting an 8 GiB pack on a box that
4173 // just told us it has nothing left is how the parked hours began.
4174 if paused.end.gave_up() {
4175 return Err(crate::resolve::ResolveError::out_of_memory(
4176 paused.end.as_str(),
4177 paused.available,
4178 paused.budget,
4179 0,
4180 0,
4181 ));
4182 }
4183 }
4184 let _gate = self
4185 .gate
4186 .lock()
4187 .map_err(|_| anyhow!("absorb gate poisoned"))?;
4188 self.absorb_gated(&PendingPack::from_job(job))
4189 }
4190}
4191
4192impl<S: ObjectIndex + 'static> GitStore<S> {
4193 /// The push path, for the two `put` halves.
4194 pub(crate) fn push_path(&self) -> &PushPath {
4195 &self.push
4196 }
4197
4198 pub(crate) fn account(&self) -> &str {
4199 &self.account
4200 }
4201
4202 pub(crate) fn ref_log(&self) -> &RefLog {
4203 &self.refs
4204 }
4205
4206 pub(crate) fn ref_gate(&self) -> &Mutex<()> {
4207 &self.ref_gate
4208 }
4209
4210 /// The closure check's index half: a `REF_DELTA`'s base has to exist —
4211 /// **in the store, or in this very pack**.
4212 ///
4213 /// This is the one part of receive-pack that reads `objects.oid` (§13's
4214 /// table), and on the path a real push takes it reads nothing else.
4215 ///
4216 /// # The second half of that sentence used to be missing, and it refused
4217 /// packs git itself writes
4218 ///
4219 /// [`crate::pack_walk::PackWalk::closure`] cannot do better on its own:
4220 /// a walk reports what the bytes say, and the bytes of a `REF_DELTA` name a
4221 /// base by **oid**, which is a fact about resolved content that no walk can
4222 /// know. So every ref base came back on `external_refs` and every one of
4223 /// them was required to be in the store already.
4224 ///
4225 /// A `REF_DELTA` naming a base *inside the same pack* is ordinary and git
4226 /// writes it constantly: `git index-pack --fix-thin` completes a pushed thin
4227 /// pack by **appending the base object to the pack** and leaving the delta
4228 /// naming it by oid. Every such pack was refused with *"this pack deltas
4229 /// against X, which this repository does not have"*, on a store that was
4230 /// being handed a perfectly ordinary, self-contained packfile — the base was
4231 /// entry 1 of 25 in the pack's own `.idx` (`gunnar.multi_pack_serve`,
4232 /// 2026-08-14). The same missing check is `S-003` / gitoxide#2882 in
4233 /// `LookupRefDeltaObjectsIter`; it is the same defect wearing the receiving
4234 /// hat instead of the sending one.
4235 ///
4236 /// # What it costs, and where
4237 ///
4238 /// **Nothing on any push that was going to succeed before.** Every base
4239 /// found in the store short-circuits exactly as it did, and a pack with no
4240 /// ref-delta at all never gets past the first loop.
4241 ///
4242 /// The pack is resolved only when the alternative is *refusing it*, and then
4243 /// the resolve is the cheapest honest answer available: an entry's oid is
4244 /// knowable only by applying its delta chain, which is precisely what
4245 /// [`crate::resolve::resolve_walked`] does — the same resolver the absorb is
4246 /// about to run over the same pack anyway, reused rather than twinned
4247 /// (LAW 5). A push that is genuinely thin against a base nobody has still
4248 /// fails, one resolve later, and still names the oid.
4249 pub(crate) fn external_bases_exist(&self, bytes: &[u8], w: &PackWalk) -> Result<()> {
4250 let c = w.closure();
4251 if !c.broken_offsets.is_empty() {
4252 bail!(
4253 "this pack is corrupt: {} delta base offset(s) do not land on an entry — first {}",
4254 c.broken_offsets.len(),
4255 c.broken_offsets[0]
4256 );
4257 }
4258 // The fast path, unchanged: a base the store already holds is settled
4259 // without reading one byte of the pack.
4260 let mut unheld: Vec<&[u8]> = Vec::new();
4261 for oid in &c.external_refs {
4262 if self.lookup_one(oid)?.is_none() {
4263 unheld.push(oid.as_slice());
4264 }
4265 }
4266 if unheld.is_empty() {
4267 return Ok(());
4268 }
4269
4270 // Only now, and only because the alternative is refusing the push: does
4271 // the pack supply these itself? `resolve_walked` answers with the
4272 // store behind it, so a base that really is external is still found the
4273 // cheap way and only a genuinely absent one fails.
4274 let resolved = crate::resolve::resolve_walked(
4275 bytes,
4276 w,
4277 self.hash_kind(),
4278 0,
4279 &*self.absorber,
4280 &crate::exploded::NoSink,
4281 )
4282 .with_context(|| {
4283 format!(
4284 "this pack deltas against {} object(s) this repository does not have, so it was \
4285 resolved to find out whether the pack carries them itself — first {}",
4286 unheld.len(),
4287 hex::encode(unheld[0])
4288 )
4289 })?;
4290 let in_pack: std::collections::HashSet<&[u8]> =
4291 resolved.iter().map(|r| r.oid.as_slice()).collect();
4292 for oid in unheld {
4293 if !in_pack.contains(oid) {
4294 bail!(
4295 "this pack deltas against {}, which this repository does not have and which \
4296 the pack does not carry either — the push is refused rather than stored with \
4297 a dangling base",
4298 hex::encode(oid)
4299 );
4300 }
4301 }
4302 Ok(())
4303 }
4304
4305 /// One oid, on the **serial** path. See [`lookup_path`].
4306 pub(crate) fn lookup_one(&self, oid: Oid<'_>) -> Result<Option<crate::index_layout::IndexRow>> {
4307 // **Never waits on the indexer.** The rows the worker has landed so far
4308 // are in the index; an oid it has not reached is resolved from the
4309 // pending pack on this thread (`Absorber::scan_pending`).
4310 self.absorber.lookup_never_wait(oid)
4311 }
4312
4313 /// Many oids, on the **batch** path, never waiting on the indexer.
4314 pub(crate) fn lookup_batch_never_wait(
4315 &self,
4316 oids: &[Oid<'_>],
4317 ) -> Result<Vec<Option<crate::index_layout::IndexRow>>> {
4318 self.absorber.lookup_batch_never_wait(oids)
4319 }
4320
4321 /// Absorb the pending packs on this thread **if the gate is free**;
4322 /// `None` = the worker holds it and this caller did not wait. See
4323 /// `Absorber::try_absorb_pending`.
4324 pub fn try_absorb_pending(&self) -> Result<Option<usize>> {
4325 self.absorber.try_absorb_pending()
4326 }
4327
4328 /// The named refusal for a request that needs the whole derived graph
4329 /// while a pack is still being indexed: `reachable` cannot be answered
4330 /// from a graph that does not hold the pack's commits yet, and waiting for
4331 /// the worker is forbidden. The message carries the progress row's
4332 /// numbers so the client knows when to come back.
4333 fn index_in_progress(&self) -> anyhow::Error {
4334 let p = self.index_progress();
4335 anyhow!(
4336 "index in progress for this repository: pack {} {}% ({}/{} objects, {}) — reachability \
4337 needs the whole graph; retry when the index row reaches 100 %",
4338 p.pack_id,
4339 p.percent(),
4340 p.objects_done,
4341 p.objects_total,
4342 p.phase.as_str()
4343 )
4344 }
4345
4346 /// The current ref namespace, as the log folds to it. The whole-snapshot
4347 /// accessor; [`ref_get`](Self::ref_get) is the keyed one.
4348 pub(crate) fn ref_state(&self) -> Result<BTreeMap<String, crate::refs::RefState>> {
4349 self.refs.current()
4350 }
4351
4352 /// One ref by name, O(log r), without cloning the namespace — for `head`
4353 /// and a single-ref CAS. See [`crate::refs::RefLog::get`].
4354 pub(crate) fn ref_get(&self, name: &str) -> Result<Option<crate::refs::RefState>> {
4355 self.refs.get(name)
4356 }
4357
4358 /// Reachability over the graph, as roaring bitmaps, in **this store's**
4359 /// ordinal space.
4360 ///
4361 /// Returns `(want ∪ closure) − (have ∪ closure)` as oid hex. Every object —
4362 /// commit, tree and blob — is in the bitmaps, because
4363 /// [`crate::reach::build_reach`] walks the trees; that is why this can answer
4364 /// with objects rather than only with commits.
4365 ///
4366 /// **Hex is for the maintenance path, not the serving one.** The answer is
4367 /// computed in raw bytes by [`GitStore::reachable_raw_with`] and encoded
4368 /// here, at one `String` per object; every caller that serves a request asks
4369 /// for the raw form instead. See [`Derived::oids_raw`] for what that cost.
4370 pub(crate) fn reachable_oids(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> Result<Vec<String>> {
4371 Ok(self
4372 .reachable_raw_with(want, have, live_reach_policy(), true)?
4373 .iter()
4374 .map(hex::encode)
4375 .collect())
4376 }
4377
4378 /// [`GitStore::reachable_oids`] with the bitmap policy **named** and the
4379 /// cache optional.
4380 ///
4381 /// One body, not two (LAW 5): both are [`GitStore::reachable_raw_with`], so
4382 /// the guarded path and the shipped path are the same code.
4383 #[cfg(test)]
4384 pub(crate) fn reachable_oids_with(
4385 &self,
4386 want: &[Oid<'_>],
4387 have: &[Oid<'_>],
4388 policy: crate::reach::ReachPolicy,
4389 cache: bool,
4390 ) -> Result<Vec<String>> {
4391 Ok(self
4392 .reachable_raw_with(want, have, policy, cache)?
4393 .iter()
4394 .map(hex::encode)
4395 .collect())
4396 }
4397
4398 /// `(want ∪ closure) − (have ∪ closure)`, in **raw oid bytes**, in one
4399 /// allocation.
4400 ///
4401 /// The answer every serving caller wants and the one the bitmaps naturally
4402 /// produce: an ordinal indexes straight into [`Derived::oids_raw`], so
4403 /// nothing is encoded, decoded or allocated per object on the way out.
4404 pub(crate) fn reachable_raw(
4405 &self,
4406 want: &[Oid<'_>],
4407 have: &[Oid<'_>],
4408 ) -> Result<git_storage_trait::OidList> {
4409 self.reachable_raw_with(want, have, live_reach_policy(), true)
4410 }
4411
4412 /// [`GitStore::reachable_raw`] with the bitmap policy **named** and the
4413 /// cache optional.
4414 ///
4415 /// It exists because the answer is defined to be **independent of the
4416 /// policy** — [`crate::reach::accumulate`] covers whatever the table does
4417 /// not — and a property like that can only be tested by driving the policy.
4418 /// A test that could not reach it would be asserting that one code path
4419 /// equals itself. `cache: false` keeps one arm's table from being read by
4420 /// the next, which is the same trap in a different hat.
4421 pub(crate) fn reachable_raw_with(
4422 &self,
4423 want: &[Oid<'_>],
4424 have: &[Oid<'_>],
4425 policy: crate::reach::ReachPolicy,
4426 cache: bool,
4427 ) -> Result<git_storage_trait::OidList> {
4428 // The graph is folded by the absorb, so a pending pack has to be
4429 // absorbed before reachability can be right. If the gate is free this
4430 // thread does it; if the worker holds it — hours into a kernel pack,
4431 // or paused by the watchdog — this is refused by name, not waited on.
4432 if self.try_absorb_pending()?.is_none() {
4433 return Err(self.index_in_progress());
4434 }
4435 // `reach_bitmaps` may take the derived WRITE lock to build, so it runs
4436 // to completion before the read guard below is taken. Getting that order
4437 // wrong is a self-deadlock, not a slow path.
4438 // The reach table AND its oid index, one pair from the same fold. Both
4439 // used to be rebuilt at this site on every call: the bitmap map
4440 // (`e.commit → &bitmap`) here, and the graph map below — O(commits)
4441 // apiece, per request, for tables that cannot change between folds. They
4442 // are folded once now and borrowed through a [`crate::reach::ReachView`].
4443 let (bitmaps, reach_index) = self.reach_tables_with(policy, cache)?;
4444 // **One read guard, held across the whole answer, and nothing cloned out
4445 // of it.** This used to be `ordinal_space()?` (a `Vec<String>` of every
4446 // oid in the store) plus `.ordinal.clone()` (a `HashMap<String, u32>` of
4447 // the same, again) — ~2 × 12 455 `String` allocations per call and two
4448 // calls per request, for two tables that cannot change between folds.
4449 // Borrowing them is not a lock held longer than the work: the guard is a
4450 // reader, folds take the writer, and a fold that lands mid-answer would
4451 // invalidate the ordinals this loop is translating.
4452 let d = self
4453 .absorber
4454 .derived
4455 .read()
4456 .map_err(|_| anyhow!("derived poisoned"))?;
4457 let space = &d.oids_raw;
4458 let oid_len = self.hash.oid_len();
4459 // oid ⇄ ordinal off the flat buffer — a binary search, no map to clone
4460 // out of the guard. `want`/`have` arrive as raw bytes, so those lookups
4461 // take the raw form directly (`ordinal.ordinal_of_raw`) and never encode.
4462 let ordinal = crate::reach::OrdinalSpace::new(space, oid_len);
4463
4464 // The graph-by-oid lookup for [`crate::reach::accumulate`]'s commit walk
4465 // is `d.graph_index`, folded beside `d.graph` and borrowed here — not
4466 // rebuilt per call. The reach table's index (`reach_index`) is paired
4467 // with `bitmaps` above by the same fold, so this view never mixes a
4468 // table with an index from a different one.
4469 let view = crate::reach::ReachView {
4470 reach: &bitmaps,
4471 reach_index: &reach_index,
4472 graph: &d.graph,
4473 graph_index: &d.graph_index,
4474 };
4475 let trees = self.absorber.tree_source();
4476 let facts = crate::reach::ObjectFacts {
4477 ordinal,
4478 trees: &trees,
4479 oid_len,
4480 };
4481
4482 let mut union = roaring::RoaringBitmap::new();
4483 for oid in want {
4484 let hex = hex::encode(oid);
4485 match view.bitmap(hex.as_str()) {
4486 // A commit with a bitmap: its whole closure, in one OR.
4487 Some(bm) => union |= bm,
4488 // **No bitmap. This is the case that used to be a wrong answer.**
4489 //
4490 // It contributed the object itself and nothing else — see
4491 // `crate::reach::accumulate`'s header for what that cost. Now a
4492 // commit the graph holds is WALKED, bounded by the first
4493 // bitmapped commit behind it, and only a `want` the store does
4494 // not hold at all is still refused.
4495 None => {
4496 if view.is_commit(hex.as_str()) {
4497 crate::reach::accumulate(hex.as_str(), &view, &facts, &mut union);
4498 } else if let Some(o) = ordinal.ordinal_of_raw(oid) {
4499 // Not a commit at all — a TAG or a blob asked for
4500 // directly.
4501 union.insert(o);
4502 // **A tag contributes what it points AT, and until
4503 // 2026-08-31 it did not.**
4504 //
4505 // The bitmaps are built over the commit graph and no
4506 // tree points at a tag, so a tag is in nobody's closure
4507 // and this arm was the only place its target could
4508 // enter. It did not, so an annotated tag shipped as one
4509 // object with its commit missing, and the far end said
4510 // `missing necessary objects … (peeling a tag)`. That
4511 // is exactly what gunnar's replication hit on six
4512 // repositories: 25 tags on one commit no branch carried.
4513 //
4514 // gunnar's own serve paths never had the bug because
4515 // they peel BEFORE they ask a store
4516 // (`gunnar-wire`'s `select_objects`, step 1). A store
4517 // that answers `reachable` has to peel here, because
4518 // its caller cannot know which oids are tags.
4519 //
4520 // A tag object is only ever reachable as a REF TIP —
4521 // nothing inside a commit or a tree points at one — so
4522 // peeling in the `want` loop is complete, not partial.
4523 peel_tag_into(
4524 self.absorber.as_ref(),
4525 &hex,
4526 &view,
4527 &facts,
4528 &mut union,
4529 );
4530 } else {
4531 bail!(
4532 "want {hex} is not in this repository — a negotiation must not be \
4533 answered from a partial set"
4534 );
4535 }
4536 }
4537 }
4538 }
4539 let mut had = roaring::RoaringBitmap::new();
4540 for oid in have {
4541 let hex = hex::encode(oid);
4542 if let Some(bm) = view.bitmap(hex.as_str()) {
4543 had |= bm;
4544 } else if view.is_commit(hex.as_str()) {
4545 // The same walk on the exclude side, and it must be the same
4546 // walk: an under-counted `have` over-sends (harmless, wasteful)
4547 // but an under-counted `want` under-sends, and answering the two
4548 // sides by different rules is how a `want − have` stops being a
4549 // subtraction of like for like.
4550 crate::reach::accumulate(hex.as_str(), &view, &facts, &mut had);
4551 } else if let Some(o) = ordinal.ordinal_of_raw(oid) {
4552 had.insert(o);
4553 }
4554 // An unknown `have` is normal: the client may have objects we do not.
4555 // It contributes nothing, which is the safe direction — we send more,
4556 // never less.
4557 }
4558
4559 let delta = union - had;
4560 // One allocation for the whole answer, sized before the loop. The
4561 // ordinal space is a flat buffer, so each object is a `copy_from_slice`
4562 // of `oid_len` bytes into it and nothing else.
4563 let mut out = git_storage_trait::OidList::with_capacity(delta.len() as usize, oid_len);
4564 for o in delta {
4565 let at = o as usize * oid_len;
4566 let raw = space
4567 .get(at..at + oid_len)
4568 .ok_or_else(|| anyhow!("ordinal {o} is outside this store's ordinal space"))?;
4569 out.push(raw)?;
4570 }
4571 Ok(out)
4572 }
4573
4574 /// Every object reachable from every ref: what a GC must keep.
4575 pub(crate) fn live_set(&self) -> Result<std::collections::HashSet<String>> {
4576 let refs = self.ref_state()?;
4577 let tips: Vec<Vec<u8>> = refs
4578 .values()
4579 .filter_map(|s| s.target.as_deref())
4580 .filter_map(|t| hex::decode(t).ok())
4581 .collect();
4582 if tips.is_empty() {
4583 bail!(
4584 "this repository has no ref pointing at an object, so every object in it would be \
4585 dead. A GC that would delete everything is refused: name a ref first"
4586 );
4587 }
4588 let borrowed: Vec<Oid<'_>> = tips.iter().map(|t| t.as_slice()).collect();
4589 let mut live: std::collections::HashSet<String> =
4590 self.reachable_oids(&borrowed, &[])?.into_iter().collect();
4591 // The peeled targets of annotated tags are reachable too, and a tag
4592 // object is not a commit so it has no bitmap of its own.
4593 for s in refs.values() {
4594 if let Some(p) = &s.peeled {
4595 live.insert(p.clone());
4596 }
4597 if let Some(t) = &s.target {
4598 live.insert(t.clone());
4599 }
4600 }
4601 Ok(live)
4602 }
4603
4604 /// Drop every row the live set does not name. The index is append-only for
4605 /// everything except this.
4606 ///
4607 /// **Both tables, in one call.** §14's exploded table is retired with the
4608 /// `objects` table because its row count against that one's is what says
4609 /// whether it is whole ([`Absorber::adopt_journal`]): dropping from one and
4610 /// not the other would leave every later open re-exploding the entire
4611 /// repository. The count returned is still the index's — that is what a
4612 /// [`GcReport`] means by a dropped row.
4613 /// Drop exactly the rows in `dead` — a set the GC computed as "seen at its
4614 /// start and not reachable", never "not in `live`". The distinction is a
4615 /// row that arrived after the GC took its snapshot: it is in neither set,
4616 /// and the predicate below keeps it. See [`dead_oids`].
4617 pub(crate) fn drop_dead_rows(&self, dead: &std::collections::HashSet<Vec<u8>>) -> Result<u64> {
4618 let dropped = self
4619 .absorber
4620 .objects
4621 .retain(&|oid: &[u8]| !dead.contains(oid))?;
4622 self.absorber
4623 .exploded
4624 .retain(&|oid: &[u8]| !dead.contains(oid))?;
4625 // **Everything folded from those two tables, refolded.** The commit
4626 // graph, the tree payloads, the ordinal space and the bitmaps are all
4627 // derivations of the rows that just went, and a derivation that still
4628 // names a dropped oid is *wrong* rather than merely stale — a `reachable`
4629 // over a graph holding dead commits selects objects the index can no
4630 // longer serve. This is the same [`Absorber::refold`] every absorb and
4631 // every open already calls, over the table this call just changed;
4632 // nothing about how the graph is rebuilt is touched here.
4633 //
4634 // The bitmaps go with it: they address **ordinals**, and dropping rows
4635 // renumbers the ordinal space, so `refold` clears them and the next
4636 // `reach_bitmaps()` builds them over the new numbering. Carrying them
4637 // across would be carrying an index into a different array.
4638 self.absorber.refold()?;
4639 Ok(dropped)
4640 }
4641
4642 /// **The journal's half of a GC: retire every pack whose objects are now all
4643 /// dead, before a single row is dropped.**
4644 ///
4645 /// Returns the offsets retired.
4646 ///
4647 /// # Why this exists
4648 ///
4649 /// §13.12's `indexed` bit is derived on open as *extent in the journal, rows
4650 /// not in the index* ([`Absorber::adopt_journal`]). Dropping every row of a
4651 /// pack produces that state exactly, so without this call the next open
4652 /// re-queues the pack and re-absorbs the objects a GC just decided were dead.
4653 /// A **partly** dead pack keeps rows and was never affected; this is only
4654 /// about the all-dead case.
4655 ///
4656 /// # What it does not do
4657 ///
4658 /// It does not truncate `objects.pack`. The blob file is append-only and a
4659 /// pack in the middle of it cannot be cut out without moving every extent
4660 /// after it — which would invalidate every index row in the archive. The dead
4661 /// pack's bytes stay; what changes is that nothing will ever index them
4662 /// again. Reclaiming those bytes is a rewrite of the blob file, and it is not
4663 /// this function.
4664 ///
4665 /// An arm with no journal ([`FastWriter`](crate::archive_write::FastWriter))
4666 /// has nothing to retire and nothing that re-queues, so it returns empty.
4667 ///
4668 /// # Cost
4669 ///
4670 /// One [`ObjectReadStack::extents_with_rows`] scan plus one batch lookup of
4671 /// the live set. Both are already the shape a GC pays elsewhere, and both run
4672 /// once per `gc()`, not per pack.
4673 /// Where the journal's acked extents end right now — the GC's snapshot of
4674 /// "packs that existed when I started". A pack acked past this point during
4675 /// the GC belongs to a push in flight and is not the GC's to judge.
4676 pub(crate) fn acked_end(&self) -> Result<u64> {
4677 let Some(journal) = self.arms.writer.journal(&self.blobs) else {
4678 return Ok(u64::MAX);
4679 };
4680 if !journal.exists() {
4681 return Ok(0);
4682 }
4683 let rows = crate::archive_write::read_journal(&journal)?;
4684 Ok(crate::archive_write::acked_packs(&rows)
4685 .iter()
4686 .map(|(o, l)| o + l)
4687 .max()
4688 .unwrap_or(0))
4689 }
4690
4691 /// Tombstone every acked pack that holds no live object — among the packs
4692 /// whose extent ends at or before `acked_end`, the GC's snapshot from
4693 /// [`acked_end`](Self::acked_end). A pack pushed after that snapshot has
4694 /// objects `live` never saw, and without the bound it would be retired for
4695 /// being unknown, which is how a GC would delete a push in flight.
4696 pub(crate) fn retire_dead_packs(
4697 &self,
4698 live: &std::collections::HashSet<String>,
4699 acked_end: u64,
4700 ) -> Result<Vec<u64>> {
4701 let Some(journal) = self.arms.writer.journal(&self.blobs) else {
4702 return Ok(Vec::new());
4703 };
4704 if !journal.exists() {
4705 return Ok(Vec::new());
4706 }
4707 let rows = crate::archive_write::read_journal(&journal)?;
4708 let packs: Vec<(u64, u64)> = crate::archive_write::acked_packs(&rows)
4709 .into_iter()
4710 .filter(|(o, l)| o + l <= acked_end)
4711 .collect();
4712 let already = crate::archive_write::retired_offsets(&rows);
4713 if packs.is_empty() {
4714 return Ok(Vec::new());
4715 }
4716
4717 // Which packs hold any row at all right now. A pack with none is either
4718 // already retired or was never absorbed — neither is this call's
4719 // business, and calling the second one dead would lose it.
4720 let occupied = self.absorber.objects.extents_with_rows(&packs)?;
4721
4722 // Which packs hold a row that survives. Placed by binary search over the
4723 // pack starts, the same way the scan above places a row.
4724 let mut order: Vec<usize> = (0..packs.len()).filter(|&i| packs[i].1 > 0).collect();
4725 order.sort_unstable_by_key(|&i| packs[i].0);
4726 let starts: Vec<u64> = order.iter().map(|&i| packs[i].0).collect();
4727 let live_oids: Vec<Vec<u8>> = live.iter().filter_map(|h| hex::decode(h).ok()).collect();
4728 let borrowed: Vec<&[u8]> = live_oids.iter().map(|o| o.as_slice()).collect();
4729 let mut has_live = vec![false; packs.len()];
4730 for (offset, _) in self
4731 .absorber
4732 .objects
4733 .extents_batch(&borrowed)
4734 .into_iter()
4735 .flatten()
4736 {
4737 let p = starts.partition_point(|&s| s <= offset);
4738 if p == 0 {
4739 continue;
4740 }
4741 let i = order[p - 1];
4742 if offset < packs[i].0 + packs[i].1 {
4743 has_live[i] = true;
4744 }
4745 }
4746
4747 let dead: Vec<u64> = (0..packs.len())
4748 .filter(|&i| occupied[i] && !has_live[i] && !already.contains(&packs[i].0))
4749 .map(|i| packs[i].0)
4750 .collect();
4751 // Durable before the rows go. The one ordering this function has.
4752 crate::archive_write::retire_packs(&journal, &dead)?;
4753 Ok(dead)
4754 }
4755
4756 /// **Write generation 0** at [`archive_path`](Self::archive_path), carrying
4757 /// the verbatim packs this store has acked plus `reserved`.
4758 ///
4759 /// The whole of the decision-making lives in
4760 /// [`seal_generation_zero`](crate::archive_write::seal_generation_zero); this
4761 /// is the three paths it needs, taken off the same fields the push path and
4762 /// the GC read, so a seal cannot address a different blob file or a different
4763 /// journal from the one that acked the packs.
4764 pub(crate) fn seal_archive(
4765 &self,
4766 reserved: Vec<ReservedSection>,
4767 ) -> Result<crate::archive_write::SealReport> {
4768 crate::archive_write::seal_generation_zero(
4769 &self.blobs,
4770 self.arms.writer.journal(&self.blobs).as_deref(),
4771 &self.archive,
4772 reserved,
4773 )
4774 }
4775
4776 /// The sections a sealed archive carries: the ref log, the commit graph and
4777 /// the bitmaps.
4778 pub(crate) fn reserved_sections(&self) -> Result<Vec<ReservedSection>> {
4779 use znippy_common::{GUNNAR_GRAPH_MODULE, GUNNAR_REACH_MODULE};
4780 let mut out = vec![self.refs.seal_section()?];
4781 let d = self
4782 .absorber
4783 .derived
4784 .read()
4785 .map_err(|_| anyhow!("derived poisoned"))?;
4786 if !d.graph.is_empty() {
4787 out.push(ReservedSection::arrow(
4788 GUNNAR_GRAPH_MODULE,
4789 crate::graph::graph_schema(),
4790 vec![crate::graph::build_graph_batch(&d.graph)?],
4791 ));
4792 }
4793 drop(d);
4794 let reach = self.reach_bitmaps()?;
4795 if !reach.is_empty() {
4796 out.push(ReservedSection::arrow(
4797 GUNNAR_REACH_MODULE,
4798 crate::reach::reach_schema(),
4799 vec![crate::reach::build_reach_batch(&reach)?],
4800 ));
4801 }
4802 Ok(out)
4803 }
4804}
4805
4806// ── the runtime selector ──────────────────────────────────────────────────────
4807
4808/// **A [`GitStore`] whose index arm was chosen at run time.**
4809///
4810/// The [`ObjectIndex`] arm is a *type*, so a value can only choose it by naming
4811/// every monomorphisation. This enum is that list, and it is an enum rather than
4812/// a `Box<dyn GitOps>` for two reasons:
4813///
4814/// * a boxed trait object can only offer the twelve, and the arms differ in
4815/// things the twelve deliberately do not expose — how many Arrow bytes the
4816/// projection actually materialised, which writer is on the ack path. A bench
4817/// or an operator dump needs those, and a guard needs them to assert that the
4818/// selector selected **on applied output** rather than on a label;
4819/// * there is no allocation and no vtable: the `match` is one predictable branch
4820/// at the top of a call, and everything under it — the `stree` probe, the
4821/// Arrow gather, the redb tail — is the same fully static code
4822/// [`GitStore::<S>::open_with_arms`](GitStore::open_with_arms) produces,
4823/// because `S` is known inside each arm.
4824///
4825/// # What it costs, stated rather than hidden
4826///
4827/// One branch per [`GitOps`] call — **not per object**. `extents(&[1000 oids])`
4828/// is one branch and a thousand monomorphised lookups behind it. The binary
4829/// carries three copies of the store, which is the price of picking a type at
4830/// run time and there is no version of this that does not pay it.
4831///
4832/// A caller that knows its arm at compile time should **not** come through here:
4833/// [`GitStore::open`] and [`GitStore::open_with_arms`] hand back a concrete
4834/// store with no branch at all, and that includes every caller that wants the
4835/// default.
4836pub enum SelectedStore {
4837 OneTableFourColumns(GitStore<OneTableFourColumns>),
4838 FourTables(GitStore<crate::index_layout::FourTables>),
4839 PackedPayload(GitStore<crate::index_layout::PackedPayload>),
4840}
4841
4842/// One expression, evaluated against whichever concrete store this is. The
4843/// delegation below is generated from it so there is no second copy of any
4844/// method body (LAW 5).
4845macro_rules! on_arm {
4846 ($self:ident, $s:ident => $body:expr) => {
4847 match $self {
4848 SelectedStore::OneTableFourColumns($s) => $body,
4849 SelectedStore::FourTables($s) => $body,
4850 SelectedStore::PackedPayload($s) => $body,
4851 }
4852 };
4853}
4854
4855impl SelectedStore {
4856 /// The arms this store was built with.
4857 pub fn arms(&self) -> StoreConfig {
4858 on_arm!(self, s => s.arms())
4859 }
4860
4861 /// **The explode band** — bytes held in each pool, the budget, and what has
4862 /// been evicted since this store was opened. `Admin.StorageInfo` and the
4863 /// dashboard's `explode 3.1G/8G` cell.
4864 pub fn explode_cache_use(&self) -> crate::exploded_cache::CacheUse {
4865 on_arm!(self, s => s.explode_cache_use())
4866 }
4867
4868 /// **This repository's explode quota, set live.** What the control plane
4869 /// calls when an account's plan changes. It never refuses a read and never
4870 /// deletes on the caller's thread: over budget evicts at the next rotation,
4871 /// and an account at its limit still serves every object — from the pack,
4872 /// which is slower and never wrong.
4873 pub fn set_explode_budget(&self, bytes: u64) {
4874 on_arm!(self, s => s.set_explode_budget(bytes))
4875 }
4876
4877 /// **`repo gc` reclaims the cache**: every lazy segment unlinked, the graph
4878 /// pool kept. Returns the bytes that left the filesystem.
4879 pub fn drop_explode_cache(&self) -> anyhow::Result<u64> {
4880 on_arm!(self, s => s.drop_explode_cache())
4881 }
4882
4883 /// The selected writer's name, as it goes on a bench row.
4884 pub fn writer_name(&self) -> &'static str {
4885 on_arm!(self, s => s.writer_name())
4886 }
4887
4888 /// What the selected writer's `append` promises.
4889 pub fn writer_durability(&self) -> &'static str {
4890 on_arm!(self, s => s.writer_durability())
4891 }
4892
4893 /// **Arrow IPC bytes the selected index arm actually materialised.**
4894 ///
4895 /// The applied output that distinguishes one layout from another: a packed
4896 /// 25-byte column, four columns in one section and four independent
4897 /// sections are three different numbers for the same objects. This is what
4898 /// a guard reads to prove the selector selected, because a *name* would
4899 /// prove only that a name was copied.
4900 pub fn index_ipc_bytes(&self) -> usize {
4901 on_arm!(self, s => s.index().ipc_bytes())
4902 }
4903
4904 /// **The name the built projection reports about itself** —
4905 /// `OneTableFourColumns`, `FourTables` or `PackedPayload`.
4906 ///
4907 /// The other half of the applied-output pair [`index_ipc_bytes`] starts:
4908 /// the byte count separates the arms once objects are in the store, and
4909 /// this separates them from the instant it is built, including on an empty
4910 /// store where all three materialise nothing. It is the value
4911 /// [`crate::arms::IndexArm::projection_name`] exists to be compared with —
4912 /// what the layout calls itself, never what the selector asked for — so a
4913 /// server logging it is stating what it built rather than repeating its own
4914 /// environment back.
4915 ///
4916 /// [`index_ipc_bytes`]: SelectedStore::index_ipc_bytes
4917 pub fn index_name(&self) -> &'static str {
4918 on_arm!(self, s => s.index().projection_name())
4919 }
4920
4921 /// The oids a pushed extent introduced — [`GitStore::oids_in_extent`],
4922 /// delegated.
4923 ///
4924 /// Not one of the twelve (a pack's extent is a znippy concept), so it does
4925 /// not arrive with the [`GitOps`] impl below and has to be forwarded by
4926 /// hand. A server that reports the objects a push introduced needs it, and
4927 /// a server that picked its index arm at run time still needs it.
4928 pub fn oids_in_extent(&self, extent: Extent) -> Result<Vec<Vec<u8>>> {
4929 on_arm!(self, s => s.oids_in_extent(extent))
4930 }
4931
4932 /// Objects in the `objects` table.
4933 pub fn object_count(&self) -> usize {
4934 on_arm!(self, s => s.object_count())
4935 }
4936
4937 /// Block until the background drain has absorbed everything pushed.
4938 pub fn wait_indexed(&self) {
4939 on_arm!(self, s => s.wait_indexed())
4940 }
4941
4942 /// Stop this store's index work; see [`GitStore::cancel_index`].
4943 pub fn cancel_index(&self) {
4944 on_arm!(self, s => s.cancel_index())
4945 }
4946
4947 /// This store's index progress; see [`GitStore::index_progress`].
4948 pub fn index_progress(&self) -> crate::progress::Snapshot {
4949 on_arm!(self, s => s.index_progress())
4950 }
4951
4952 /// Rebuild the projection, so the layout under test is what answers rather
4953 /// than the redb tail.
4954 pub fn rebuild_projection(&self) -> Result<()> {
4955 on_arm!(self, s => s.index().rebuild())
4956 }
4957
4958 /// The twelfth method — inherent, not on [`GitOps`], because it returns
4959 /// Arrow `ReservedSection`s a gix backend has no analog for.
4960 pub fn seal(&self) -> Result<Vec<ReservedSection>> {
4961 on_arm!(self, s => s.seal())
4962 }
4963}
4964
4965impl GitOps for SelectedStore {
4966 fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> Result<TxId> {
4967 on_arm!(self, s => s.put(pack, refs))
4968 }
4969 fn put_pack(&self, bytes: &[u8]) -> Result<TxId> {
4970 on_arm!(self, s => s.put_pack(bytes))
4971 }
4972 // Delegated like every other method, and that is the point: without these
4973 // two lines a `SelectedStore` would silently inherit the trait's *default*
4974 // `put_pack_observed` — the one that publishes nothing — and every store
4975 // gunnar actually opens goes through this enum. The count would have been
4976 // implemented in `store.rs` and unreachable in production.
4977 fn put_pack_observed(
4978 &self,
4979 bytes: &[u8],
4980 progress: &dyn git_storage_trait::PackProgress,
4981 ) -> Result<TxId> {
4982 on_arm!(self, s => s.put_pack_observed(bytes, progress))
4983 }
4984 fn publishes_pack_progress(&self) -> bool {
4985 on_arm!(self, s => s.publishes_pack_progress())
4986 }
4987 fn put_refs(&self, updates: &[RefUpdate]) -> Result<TxId> {
4988 on_arm!(self, s => s.put_refs(updates))
4989 }
4990 fn get(&self, oid: Oid<'_>) -> Result<Option<Stored>> {
4991 on_arm!(self, s => s.get(oid))
4992 }
4993 fn has(&self, oid: Oid<'_>) -> Result<bool> {
4994 on_arm!(self, s => s.has(oid))
4995 }
4996 fn size(&self, oid: Oid<'_>) -> Result<Option<u64>> {
4997 on_arm!(self, s => s.size(oid))
4998 }
4999 fn extents(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<Extent>>> {
5000 on_arm!(self, s => s.extents(oids))
5001 }
5002 fn refs(&self) -> Result<Vec<RefRow>> {
5003 on_arm!(self, s => s.refs())
5004 }
5005 fn update_ref(&self, name: &str, old: Option<Oid<'_>>, new: Option<Oid<'_>>) -> Result<TxId> {
5006 on_arm!(self, s => s.update_ref(name, old, new))
5007 }
5008 fn put_refs_cas(&self, edits: &[git_storage_trait::RefCas<'_>]) -> Result<TxId> {
5009 on_arm!(self, s => s.put_refs_cas(edits))
5010 }
5011 fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> Result<Vec<Vec<u8>>> {
5012 on_arm!(self, s => s.reachable(want, have))
5013 }
5014 fn gc(&self) -> Result<GcReport> {
5015 on_arm!(self, s => s.gc())
5016 }
5017}
5018
5019/// The reading contract, delegated the same way [`GitOps`] is.
5020///
5021/// Every method is one predictable branch over a monomorphised body — the same
5022/// arrangement, and the same cost argument, [`SelectedStore`] makes for the
5023/// eleven. It is here rather than in [`crate::serve`] only because the macro
5024/// that generates it lives in this module.
5025impl crate::serve::GitServe for SelectedStore {
5026 fn read(&self, oid: Oid<'_>) -> Result<Option<(crate::index_layout::ObjType, Vec<u8>)>> {
5027 on_arm!(self, s => crate::serve::GitServe::read(s, oid))
5028 }
5029 fn header(&self, oid: Oid<'_>) -> Result<Option<(crate::index_layout::ObjType, u64)>> {
5030 on_arm!(self, s => crate::serve::GitServe::header(s, oid))
5031 }
5032 fn sizes(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<u64>>> {
5033 on_arm!(self, s => crate::serve::GitServe::sizes(s, oids))
5034 }
5035 fn index_now(&self) -> Result<usize> {
5036 on_arm!(self, s => crate::serve::GitServe::index_now(s))
5037 }
5038 fn head(&self) -> Result<Option<RefRow>> {
5039 on_arm!(self, s => crate::serve::GitServe::head(s))
5040 }
5041 fn set_head(&self, target: &str) -> Result<TxId> {
5042 on_arm!(self, s => crate::serve::GitServe::set_head(s, target))
5043 }
5044 /// `objects` is the **exact set to emit**, not tips to close over — the
5045 /// contract's word, and this forwarder uses it so that the name cannot drift
5046 /// back into `want` one arm at a time. See [`crate::serve::GitServe::emit_pack`].
5047 fn emit_pack(
5048 &self,
5049 objects: &[Oid<'_>],
5050 have: &[Oid<'_>],
5051 caps: &git_storage_trait::Caps,
5052 out: &mut dyn std::io::Write,
5053 ) -> Result<git_storage_trait::PackStats> {
5054 on_arm!(self, s => crate::serve::GitServe::emit_pack(s, objects, have, caps, out))
5055 }
5056 fn select(
5057 &self,
5058 want: &[Oid<'_>],
5059 have: &[Oid<'_>],
5060 ) -> Result<Option<git_storage_trait::ReachSet>> {
5061 on_arm!(self, s => crate::serve::GitServe::select(s, want, have))
5062 }
5063}
5064
5065/// **Open a store with all three arms chosen at run time.**
5066///
5067/// The one place [`crate::arms::IndexArm`] — a value — is turned into `S` — a
5068/// type. See [`SelectedStore`] for what the resulting handle costs.
5069pub fn open_selected(
5070 root: &Path,
5071 account: &str,
5072 hash: GitHashKind,
5073 arms: StoreConfig,
5074) -> Result<SelectedStore> {
5075 use crate::arms::IndexArm;
5076 use crate::index_layout::{FourTables, PackedPayload};
5077 Ok(match arms.index {
5078 IndexArm::OneTableFourColumns => {
5079 SelectedStore::OneTableFourColumns(GitStore::<OneTableFourColumns>::open_with_arms(
5080 root, account, hash, arms,
5081 )?)
5082 }
5083 IndexArm::FourTables => SelectedStore::FourTables(GitStore::<FourTables>::open_with_arms(
5084 root, account, hash, arms,
5085 )?),
5086 IndexArm::PackedPayload => SelectedStore::PackedPayload(
5087 GitStore::<PackedPayload>::open_with_arms(root, account, hash, arms)?,
5088 ),
5089 })
5090}
5091
5092/// **The env-driven front door**: read the three variables **once**, here, and
5093/// build the store they name.
5094///
5095/// This is the whole runtime selector a server or a bench needs — one call, one
5096/// triple of `getenv`s, and from then on the store holds built objects and reads
5097/// nothing. There is deliberately no per-operation lookup of any kind: a
5098/// `std::env::var` inside a serving loop would be one syscall per object served,
5099/// which is a real defect that was found and fixed in gunnar the day before this
5100/// was written. [`crate::arms::env_reads`] counts every read this crate makes so
5101/// that "once, at construction" is an assertion rather than a claim.
5102///
5103/// An unset variable takes the shipping default, so a process with a clean
5104/// environment gets exactly [`GitStore::open`]'s combination.
5105pub fn open_from_env(root: &Path, account: &str, hash: GitHashKind) -> Result<SelectedStore> {
5106 let arms = StoreConfig::from_env()?;
5107 open_selected(root, account, hash, arms)
5108}
5109
5110impl<S: ObjectIndex> Absorber<S> {
5111 /// **One object's resolved content, and §14's table is what answers.**
5112 ///
5113 /// Two paths, and they are not interchangeable even though they return
5114 /// identical bytes:
5115 ///
5116 /// 1. **the exploded table** — one point lookup, no inflate, no delta chain;
5117 /// 2. **re-derived from the verbatim truth** — find which pack holds the
5118 /// object, read the whole pack back, resolve it. Correct and slow, and it
5119 /// exists because the table is droppable: a store whose table has been
5120 /// deleted, or whose re-explosion has not caught up, must still answer.
5121 ///
5122 /// The counters are the only way to tell them apart. Identical bytes come
5123 /// back either way, so a byte comparison proves nothing about which path ran
5124 /// — that is the identity-value trap in its exact form, and it is why
5125 /// [`crate::exploded::ExplodedStats::rederived`] is bumped here rather than
5126 /// inferred.
5127 ///
5128 /// On the [`Absorber`] rather than on [`GitStore`] because that is where the
5129 /// absorb runs: the background worker has no store handle, and a base lookup
5130 /// that needed one would put the whole store behind the drain.
5131 fn resolved(&self, oid: &[u8]) -> Result<Option<(GitObjectKind, Vec<u8>)>> {
5132 Ok(self
5133 .resolved_costed(oid)?
5134 .map(|(kind, payload, _)| (kind, payload)))
5135 }
5136
5137 /// [`Self::resolved`] and the bill. `None` for the cost means a pool
5138 /// answered and nothing was rebuilt.
5139 #[allow(clippy::type_complexity)]
5140 fn resolved_costed(
5141 &self,
5142 oid: &[u8],
5143 ) -> Result<Option<(GitObjectKind, Vec<u8>, Option<crate::resolve::ReadCost>)>> {
5144 // (1) §14's table.
5145 if let Some((kind, payload)) = self.exploded.content(oid)? {
5146 return Ok(Some((kind, payload, None)));
5147 }
5148 // (2) the verbatim truth, re-derived.
5149 let Some(row) = self.objects.lookup(oid) else {
5150 // (3) not indexed yet: the pending packs, read-only, on this
5151 // reader's own thread — never by waiting for the worker.
5152 //
5153 // **This arm is the ack path's trap and it is not the ack path's
5154 // fix.** `a_read_before_the_drain_is_served_from_the_pack_and_never_waits`
5155 // is the law here: a clone must be answered while the drain is
5156 // parked, so a read may not take the absorb gate. What must not
5157 // happen is the ACK arriving here, and it no longer does —
5158 // `GitServe::index_now` runs before the connectivity check, so by
5159 // the time the check reads there is nothing pending to scan. See
5160 // [`Absorber::index_pending_now`].
5161 if self.unindexed_packs() == 0 {
5162 return Ok(None);
5163 }
5164 self.exploded.note_rederived();
5165 let (_, content) = self.scan_pending(&[], Some(oid))?;
5166 // The pending scan resolves through a `PackWalk` rather than a
5167 // chain walk, so it does not produce a bill. `None` is honest here
5168 // and is not "it was free".
5169 return Ok(content.map(|(k, p)| (k, p, None)));
5170 };
5171 // ── ONE CHAIN, not one pack ─────────────────────────────────────
5172 //
5173 // The row's `offset` addresses the entry in the archive directly, so the
5174 // object is materialised by following its own delta chain out of the
5175 // mapping — `d` inflates and `d − 1` delta applications, nothing else
5176 // touched. This used to `read_extent` the WHOLE containing pack and
5177 // re-resolve it: for `torvalds/linux` an 8.12 GiB read and a 13.9 M
5178 // object resolve **per object served**, which is the cost that forced
5179 // the exploded tier to be built eagerly at push time — and that tier
5180 // MEASURED 141.7 GiB for an 8.12 GiB pack (17×). Lazy explode is only
5181 // affordable because of this function
5182 // (`ideas/lazy-explode-index-only-at-push.md`).
5183 //
5184 // It also does not care whether the containing pack has been ABSORBED:
5185 // the bytes were fsynced before the ack, so an object whose row is in
5186 // the index is readable the instant the row is. The pending-pack scan
5187 // below is for the window before the row exists at all.
5188 self.exploded.note_rederived();
5189 let snap = self.map.snapshot()?;
5190 let src = ArchiveWindow { abs: self, snap: &snap };
5191 let offset_of = |base: &[u8]| self.objects.lookup(base).map(|r| r.offset);
5192 match crate::resolve::materialize_at(
5193 &src,
5194 row.offset,
5195 self.hash.oid_len(),
5196 &offset_of,
5197 self,
5198 ) {
5199 Ok((kind, payload, cost)) => {
5200 self.keep_for_the_next_reader(oid, kind, &payload, cost);
5201 Ok(Some((kind, payload, Some(cost))))
5202 }
5203 Err(e) => {
5204 // A chain that cannot be followed out of the mapping is not a
5205 // wrong answer to hand back: fall through to the pending scan,
5206 // which re-derives from the pack the row came in on.
5207 //
5208 // **And if that finds nothing either, the ORIGINAL error is
5209 // what comes back — never `Ok(None)`.** The row says this store
5210 // holds the object; a failed read of an object we hold is a
5211 // store defect, and answering "absent" for it turned that defect
5212 // into a refusal of somebody else's push (lane AI, 2026-08-30 —
5213 // see `crate::resolve::MAX_DELTA_LINKS`). Absence is a fact the
5214 // index states, not a shape an error decays into.
5215 if self.unindexed_packs() == 0 {
5216 return Err(e);
5217 }
5218 match self.scan_pending(&[], Some(oid))? {
5219 (_, Some((k, p))) => Ok(Some((k, p, None))),
5220 (_, None) => Err(e),
5221 }
5222 }
5223 }
5224 }
5225}
5226
5227impl<S: ObjectIndex> Absorber<S> {
5228 /// **The write-back, and it is off the reader's path.**
5229 ///
5230 /// The reader has its bytes already — this buffers a copy into the lazy
5231 /// pool's newest segment (a `memcpy` under one lock, no I/O, no fsync, no
5232 /// eviction) and, once 4 MiB have accumulated, starts a thread to do the
5233 /// writing. That is Rickard's law from the design note in one function:
5234 /// *nobody waits*. A failure here is not the read's failure — the bytes
5235 /// were correct before the cache was consulted and are correct after it
5236 /// refuses — so it is counted and swallowed rather than propagated.
5237 ///
5238 /// **It does not always write.** A shallow, small object is cheaper to
5239 /// inflate again than to keep — a whole object is one `inflate` of one zlib
5240 /// stream at one offset — so the cache declines it and counts the read as
5241 /// `direct_reads` (`exploded_cache::worth_caching`). That is Rickard's
5242 /// refinement, 2026-08-30: *do not explode uniformly*. The reader that just
5243 /// paid the bill is the only thing that knows what it was, which is why the
5244 /// cost comes back out of [`crate::resolve::materialize_at`] rather than
5245 /// being predicted from an index column.
5246 ///
5247 /// Two readers racing on one oid is benign: identical bytes, and the table
5248 /// keeps the first row it filed under that key.
5249 fn keep_for_the_next_reader(
5250 &self,
5251 oid: &[u8],
5252 kind: GitObjectKind,
5253 payload: &[u8],
5254 cost: crate::resolve::ReadCost,
5255 ) {
5256 match self.exploded.write_back(oid, kind, payload, cost) {
5257 Ok(true) => self.exploded.kick(),
5258 Ok(false) => {}
5259 Err(e) => {
5260 tracing::debug!(
5261 target: "znippy.git",
5262 store = %self.label,
5263 error = %e,
5264 "the exploded cache would not take a write-back; the read is unaffected"
5265 );
5266 }
5267 }
5268 }
5269}
5270
5271/// [`crate::reach::TreeSource`] over a live store — see
5272/// [`Absorber::tree_source`].
5273/// The oid a tag object points at, out of its header.
5274///
5275/// A tag object's payload begins `object <hex>\n` — that is the format, not a
5276/// heuristic, and it is the whole parse. Deliberately NOT reached for with a
5277/// git object library: this crate is a shipped archiver and `gix-object` is a
5278/// dev-dependency here, so pulling it into the read path to read one line would
5279/// put the pack pipeline back in an archive-only build (S-071).
5280fn tag_target_hex(payload: &[u8], oid_len: usize) -> Option<String> {
5281 let line = payload.split(|b| *b == b'\n').next()?;
5282 let rest = line.strip_prefix(b"object ")?;
5283 // Exactly the hash width this store uses, and hex. A shorter or longer
5284 // field is a malformed tag, and guessing at one would put a wrong oid into
5285 // a reachability answer — the one place a wrong answer is silent.
5286 if rest.len() != oid_len * 2 || !rest.iter().all(u8::is_ascii_hexdigit) {
5287 return None;
5288 }
5289 std::str::from_utf8(rest).ok().map(str::to_owned)
5290}
5291
5292/// Add what a tag points at — and what THAT reaches — to a reachability union.
5293///
5294/// See the call site in `reachable_raw_with` for why this exists and why doing
5295/// it in the `want` loop is complete rather than partial.
5296///
5297/// A tag of a tag is legal git, so this follows the chain; `MAX_TAG_HOPS`
5298/// bounds it because a corrupt archive could point one at itself and a
5299/// reachability walk must terminate on bad data, not hang on it.
5300fn peel_tag_into<S: ObjectIndex>(
5301 abs: &Absorber<S>,
5302 tag_hex: &str,
5303 view: &crate::reach::ReachView<'_>,
5304 facts: &crate::reach::ObjectFacts<'_>,
5305 union: &mut roaring::RoaringBitmap,
5306) {
5307 const MAX_TAG_HOPS: usize = 8;
5308 let mut cursor = tag_hex.to_owned();
5309 for _ in 0..MAX_TAG_HOPS {
5310 let Ok(raw) = hex::decode(&cursor) else { return };
5311 // Not a tag, unreadable, or absent: nothing to peel. An unreadable
5312 // object makes the closure SHORT, and a short closure is refused
5313 // downstream by name (`missing necessary objects`) rather than served
5314 // as though it were complete — the same contract `TreesFromStore::tree`
5315 // documents.
5316 let Ok(Some((GitObjectKind::Tag, payload))) = abs.resolved(&raw) else { return };
5317 let Some(target) = tag_target_hex(&payload, facts.oid_len) else { return };
5318 if let Some(bm) = view.bitmap(target.as_str()) {
5319 *union |= bm;
5320 return;
5321 }
5322 if view.is_commit(target.as_str()) {
5323 crate::reach::accumulate(target.as_str(), view, facts, union);
5324 return;
5325 }
5326 let Some(o) = facts.ordinal.ordinal_of_hex(target.as_str()) else { return };
5327 union.insert(o);
5328 // A tag may point at a TREE, whose subtrees and blobs are then just as
5329 // necessary as a commit's would be. Inserting the tree alone would be
5330 // the same bug one level down.
5331 match abs.resolved(&hex::decode(&target).unwrap_or_default()) {
5332 Ok(Some((GitObjectKind::Tree, _))) => {
5333 crate::reach::accumulate_tree(target.as_str(), facts, union);
5334 return;
5335 }
5336 Ok(Some((GitObjectKind::Tag, _))) => cursor = target,
5337 // A blob, or unreadable: it is in, and it has no closure.
5338 _ => return,
5339 }
5340 }
5341}
5342
5343struct TreesFromStore<'a, S: ObjectIndex> {
5344 abs: &'a Absorber<S>,
5345}
5346
5347impl<S: ObjectIndex> crate::reach::TreeSource for TreesFromStore<'_, S> {
5348 fn tree(&self, oid_hex: &str) -> Option<std::borrow::Cow<'_, [u8]>> {
5349 let raw = hex::decode(oid_hex).ok()?;
5350 // A read that fails is `None`, which is what an object this archive
5351 // does not hold has always answered here — the walk stops at it. It is
5352 // NOT silently dropping an error that matters: an unreadable tree makes
5353 // the closure short, and a short closure is refused downstream by name
5354 // (`missing necessary objects`) rather than served as if it were
5355 // complete.
5356 match self.abs.resolved(&raw) {
5357 Ok(Some((GitObjectKind::Tree, payload))) => Some(std::borrow::Cow::Owned(payload)),
5358 // A blob, a tag, a gitlink, or absent: all "not a tree here".
5359 Ok(_) => None,
5360 Err(e) => {
5361 tracing::debug!(
5362 target: "znippy.git",
5363 store = %self.abs.label,
5364 oid = oid_hex,
5365 error = %e,
5366 "a reachability walk could not read a tree; the walk stops there"
5367 );
5368 None
5369 }
5370 }
5371 }
5372}
5373
5374/// The verbatim archive as [`crate::resolve::ArchiveBytes`]: **the mapping
5375/// where it reaches, a `pread` past its end.**
5376///
5377/// One snapshot is taken per read and sliced as many times as the chain is
5378/// deep, which is [`crate::archive_map::ArchiveMap::snapshot`]'s contract. The
5379/// `pread` arm is the one a push that landed after the snapshot takes; both
5380/// arms return the same bytes and differ only in what they cost.
5381struct ArchiveWindow<'a, S: ObjectIndex> {
5382 abs: &'a Absorber<S>,
5383 snap: &'a crate::archive_map::Mapped,
5384}
5385
5386impl<S: ObjectIndex> crate::resolve::ArchiveBytes for ArchiveWindow<'_, S> {
5387 fn at(&self, at: u64, len: u64) -> Result<std::borrow::Cow<'_, [u8]>> {
5388 // A probe near the end of the archive legitimately gets fewer bytes
5389 // than it asked for, so the window is clamped rather than refused —
5390 // the parsers read a grammar, not a fixed width.
5391 let mapped = self.snap.len();
5392 if at < mapped {
5393 let have = (mapped - at).min(len);
5394 if let Some(b) = self.snap.get(at, have) {
5395 return Ok(std::borrow::Cow::Borrowed(b));
5396 }
5397 }
5398 let end = self
5399 .abs
5400 .reader
5401 .metadata()
5402 .map(|m| m.len())
5403 .unwrap_or(mapped);
5404 if at >= end {
5405 return Ok(std::borrow::Cow::Owned(Vec::new()));
5406 }
5407 let have = (end - at).min(len);
5408 Ok(std::borrow::Cow::Owned(self.abs.read_extent(at, have)?))
5409 }
5410}
5411
5412/// A thin pack's external delta base, answered from §14's exploded table.
5413///
5414/// Before that table existed this had to read and re-resolve a whole pack per
5415/// base, and it deliberately cached nothing — because a cache here *would* have
5416/// been the undecided table. It is decided now (eager, 2026-08-08), so the cache
5417/// is the table and this is one point lookup.
5418///
5419/// **`None` means absent and `Err` means unreadable, and they are not the same
5420/// answer.** A base this store does not hold still comes back `Ok(None)` and
5421/// [`crate::resolve::ResolveError::missing_base`] refuses the pack by name.
5422///
5423/// # 🔴 This was `.ok().flatten()`
5424///
5425/// A store that HELD the base and could not reconstruct it answered exactly
5426/// like one that never had it, so the resolver blamed the pushed pack: *"this
5427/// repository genuinely does not have that object"*, about an object one index
5428/// lookup away. That sentence went to the client on the `ng` line and to the
5429/// operator in the log, and it named the wrong party — lane AI, 2026-08-30, 21
5430/// of 140 ordinary pushes. The error travels now.
5431impl<S: ObjectIndex> BaseSource for Absorber<S> {
5432 fn content(&self, oid: &[u8]) -> Result<Option<(GitObjectKind, Vec<u8>)>> {
5433 self.resolved(oid).with_context(|| {
5434 format!(
5435 "reading the delta base {} out of this store — it is not absent, it could not \
5436 be reconstructed",
5437 hex::encode(oid)
5438 )
5439 })
5440 }
5441}
5442
5443#[cfg(test)]
5444mod tests {
5445 use super::*;
5446 use crate::archive_write::{read_journal, SafeWriter};
5447 use crate::object::GitObjectKind;
5448 use crate::store::tests::{one_blob_pack, corpus_pack, tmpdir};
5449 use std::collections::HashSet;
5450 use std::sync::atomic::{AtomicBool, AtomicU64, Ordering as AtomicOrdering};
5451 use std::time::{Duration, Instant};
5452 use znippy_zoomies::background::Job;
5453 use znippy_zoomies::gatling_forkjoin::gatling_for_each;
5454
5455 fn loadavg() -> String {
5456 std::fs::read_to_string("/proc/loadavg")
5457 .unwrap_or_default()
5458 .split_whitespace()
5459 .take(3)
5460 .collect::<Vec<_>>()
5461 .join(" ")
5462 }
5463
5464 /// **The one representation answers exactly what the two it replaced did.**
5465 ///
5466 /// Before S-082 the store carried the ordinal space three times: `oids_raw`
5467 /// (flat bytes), `oids` (hex `Vec<String>`) and `ordinal`
5468 /// (`HashMap<String, u32>`). The last two are gone; oid ⇄ ordinal is now a
5469 /// slice and a binary search over `oids_raw`, through
5470 /// [`crate::reach::OrdinalSpace`]. This rebuilds those two OLD tables inline
5471 /// from the SAME `oids_raw` the store folded, and asserts the new lookups
5472 /// return the identical answer **for every oid in a real corpus** — the
5473 /// property that lets the field be removed without a server ever naming the
5474 /// wrong object in a pack, which no exit code could see.
5475 ///
5476 /// Seen RED by an off-by-one binary search (`Equal => Some(mid as u32 + 1)`
5477 /// in [`crate::reach::OrdinalSpace::ordinal_of_raw`]): every
5478 /// `ordinal_of_raw` / `ordinal_of_hex` below disagreed with the old
5479 /// `HashMap`, and the closure comparison at the end disagreed as well.
5480 #[test]
5481 fn the_one_ordinal_space_answers_what_the_two_it_replaced_did() {
5482 let dir = tmpdir("ordinal-space-one-representation");
5483 let (pack, rows) = corpus_pack();
5484 let store = GitStore::open(&dir, "rickard").unwrap();
5485 store.put_pack(&pack).unwrap();
5486 store.wait_indexed();
5487
5488 let oid_len = store.hash_kind().oid_len();
5489 let raw = {
5490 let d = store.absorber.derived.read().unwrap();
5491 d.oids_raw.clone()
5492 };
5493 // The two tables `refold` used to build, rebuilt here from the same
5494 // bytes — this IS the "old" answer the new lookups must equal.
5495 let old_oids: Vec<String> = raw.chunks_exact(oid_len).map(hex::encode).collect();
5496 let old_ordinal: std::collections::HashMap<String, u32> = old_oids
5497 .iter()
5498 .enumerate()
5499 .map(|(i, o)| (o.clone(), i as u32))
5500 .collect();
5501
5502 assert!(
5503 !old_oids.is_empty(),
5504 "the fixture pack ({} entries) folded to an EMPTY ordinal space, so every \
5505 comparison below would hold vacuously",
5506 rows.len()
5507 );
5508 assert_eq!(
5509 raw.len(),
5510 old_oids.len() * oid_len,
5511 "the raw ordinal space is {} bytes for {} oids of {oid_len} bytes",
5512 raw.len(),
5513 old_oids.len()
5514 );
5515
5516 let space = crate::reach::OrdinalSpace::new(&raw, oid_len);
5517 assert_eq!(space.len(), old_oids.len(), "the space holds a different count");
5518 let mut checked = 0usize;
5519 for (o, h) in old_oids.iter().enumerate() {
5520 let want_ord = old_ordinal[h];
5521 // oid → ordinal, hex and raw, both == the old HashMap answer.
5522 assert_eq!(
5523 space.ordinal_of_hex(h),
5524 Some(want_ord),
5525 "ordinal_of_hex({h}) != the old HashMap answer {want_ord}"
5526 );
5527 let raw_oid = hex::decode(h).unwrap();
5528 assert_eq!(
5529 space.ordinal_of_raw(&raw_oid),
5530 Some(want_ord),
5531 "ordinal_of_raw disagrees with the old HashMap at {h}"
5532 );
5533 // ordinal → oid == the old hex Vec's slice, in both spellings.
5534 assert_eq!(
5535 space.oid(o as u32),
5536 Some(raw_oid.as_slice()),
5537 "oid({o}) != the old oids_raw slice"
5538 );
5539 assert_eq!(
5540 space.oid(o as u32).map(hex::encode).as_deref(),
5541 Some(h.as_str()),
5542 "oid({o}) hex != the old hex Vec entry {h}"
5543 );
5544 checked += 1;
5545 }
5546 assert_eq!(checked, old_oids.len());
5547
5548 // A non-member answers None both ways, exactly as `HashMap::get` did for
5549 // an absent key — the binary search must not land on a neighbour.
5550 let absent = vec![0xffu8; oid_len];
5551 assert_eq!(space.ordinal_of_raw(&absent), None, "an absent oid resolved to an ordinal");
5552 assert_eq!(
5553 space.ordinal_of_hex(&hex::encode(&absent)),
5554 None,
5555 "an absent hex oid resolved to an ordinal"
5556 );
5557 // A wrong-width key is absent, not a panic.
5558 assert_eq!(space.ordinal_of_raw(&absent[..oid_len - 1]), None);
5559 assert_eq!(space.oid(old_oids.len() as u32), None, "an out-of-range ordinal named an oid");
5560
5561 // And the answer built out of it: the raw closure IS the hex closure,
5562 // for a real tip rather than for an empty request.
5563 let tip = store
5564 .graph_snapshot()
5565 .into_iter()
5566 .max_by_key(|c| c.generation)
5567 .expect("a graph");
5568 let tip_raw = hex::decode(&tip.oid).unwrap();
5569 let flat = store.reachable_raw(&[&tip_raw], &[]).unwrap();
5570 let hexed = store.reachable_oids(&[&tip_raw], &[]).unwrap();
5571 assert!(
5572 flat.len() > 1,
5573 "the tip's closure is {} object(s) — too small to tell an ordering bug from a \
5574 coincidence",
5575 flat.len()
5576 );
5577 assert_eq!(
5578 flat.iter().map(hex::encode).collect::<Vec<_>>(),
5579 hexed,
5580 "the serving answer and the maintenance answer name different objects"
5581 );
5582 assert!(flat.contains(&tip_raw), "a closure without its own tip");
5583 eprintln!(
5584 "load {}; {} ordinals agree in both spellings, tip closure {} objects",
5585 loadavg(),
5586 checked,
5587 flat.len()
5588 );
5589 }
5590
5591 /// Distinct commits in a resolved pack — what the graph must hold exactly
5592 /// once.
5593 fn commits_in(rows: &[crate::resolve::Resolved]) -> usize {
5594 rows.iter()
5595 .filter(|r| r.kind == GitObjectKind::Commit)
5596 .map(|r| r.oid.clone())
5597 .collect::<HashSet<Vec<u8>>>()
5598 .len()
5599 }
5600
5601 /// **A push ends in object rows, and no read asked for them.**
5602 ///
5603 /// This is the seam this file wires: before it, `put` produced a *pack* row
5604 /// on the account indexer's channel and the object rows appeared only when
5605 /// somebody read (or called `absorb_pending` by hand). Here nothing is read
5606 /// between `put_pack` and `wait_indexed` — the only thing that could have
5607 /// built the rows is the drain.
5608 ///
5609 /// Asserted on applied output: one row per pack entry, every oid resolving
5610 /// out of the index, and the pack's indexed bit up. `absorb_failures` is
5611 /// asserted too, because a drain that absorbed nothing and recorded the
5612 /// failure would otherwise look like a drain that was never called.
5613 ///
5614 /// Seen RED by making the drain skip the absorber
5615 /// (`match absorber.as_deref()` → `match None::<&dyn ObjectAbsorb>` in
5616 /// `indexer::index_worker`): "the drain built no object rows: 0 of 2687".
5617 /// Restored.
5618 ///
5619 /// MEASURED on oden, 2026-08-08, release, three runs at **1-minute loadavg
5620 /// 5.77** (another tenant's work — oden is shared, so this is not an idle
5621 /// box): 2687 objects out of a 5 653 302-byte real pack absorbed in
5622 /// 161–164 ms, **16 421–16 673 object rows/s**, behind an ack of 29.1–29.4 ms.
5623 /// An earlier set at loadavg 16.94 gave 15 528 rows/s, so the figure moves
5624 /// about 7% with the box.
5625 ///
5626 /// Read it as a *floor* rather than as the engine's speed: the drain here is
5627 /// one pack, so it never uses more than one absorb, and the 162 ms covers a
5628 /// full inflate-and-delta-apply of every entry, a SHA-1 per object, the redb
5629 /// append and a whole-tail re-fold. Nothing about it has been optimised and
5630 /// no arm of it has been isolated. The rate is printed with the loadavg of
5631 /// its own run so a later number is never compared against a different
5632 /// machine state by accident.
5633 #[test]
5634 fn a_push_ends_in_object_rows_and_no_read_asked_for_them() {
5635 let dir = tmpdir("drain-rows");
5636 let store = GitStore::open(&dir, "rickard").unwrap();
5637 let (pack, rows) = corpus_pack();
5638
5639 let t = Instant::now();
5640 let tx = store.put_pack(&pack).unwrap();
5641 let ack = t.elapsed();
5642 let pack_id = tx.pack_id.expect("a pack push assigns an id");
5643
5644 // Nothing is read here. The drain is the only thing that can move.
5645 store.wait_indexed();
5646 let drained = t.elapsed();
5647
5648 assert_eq!(
5649 store.object_count(),
5650 rows.len(),
5651 "the drain built no object rows: {} of {}",
5652 store.object_count(),
5653 rows.len()
5654 );
5655 assert_eq!(store.unindexed_packs(), 0, "the bit is still clear");
5656 assert!(
5657 store.indexer().is_indexed(pack_id),
5658 "the pack's indexed bit is not up after the drain"
5659 );
5660 assert_eq!(
5661 store.indexer().absorb_failures(),
5662 0,
5663 "the drain recorded an absorb failure: {:?}",
5664 store.indexer().last_absorb_error()
5665 );
5666
5667 // Every oid answers **out of the rows**, not out of a fallback: the
5668 // index is asked directly, so nothing can absorb behind this loop.
5669 for (i, r) in rows.iter().enumerate() {
5670 let row = store
5671 .index()
5672 .lookup(&r.oid)
5673 .unwrap_or_else(|| panic!("object {i} {} has no row", hex::encode(&r.oid)));
5674 assert_eq!(
5675 (row.offset, row.len),
5676 (tx.extent.unwrap().0 + r.offset, r.len)
5677 );
5678 assert_eq!(row.uncompressed_size, r.uncompressed_size);
5679 }
5680 assert_eq!(
5681 store.absorb_pending().unwrap(),
5682 0,
5683 "the drain left work for a read to do"
5684 );
5685 assert_eq!(
5686 store.commit_count(),
5687 commits_in(&rows),
5688 "the commit graph does not match the pack"
5689 );
5690
5691 let secs = (drained - ack).as_secs_f64();
5692 eprintln!(
5693 "load {}; {} objects in {} bytes: ack {:.0} µs, drain {:.1} ms, {:.0} object rows/s",
5694 loadavg(),
5695 rows.len(),
5696 pack.len(),
5697 ack.as_secs_f64() * 1e6,
5698 (drained - ack).as_secs_f64() * 1e3,
5699 rows.len() as f64 / secs.max(1e-9),
5700 );
5701 }
5702
5703 // ── the bit across a restart ─────────────────────────────────────────────
5704
5705 /// Where the child of the kill guard finds its fixture and leaves its marker.
5706 const KILL_DIR: &str = "GUNNAR_KILL_MID_ABSORB_DIR";
5707
5708 /// **The other half of [`a_store_reopened_after_a_kill_mid_absorb_requeues_the_pack`]:
5709 /// the process that dies.**
5710 ///
5711 /// It runs as a child of that test and it **aborts on purpose**, which is why
5712 /// it is `#[ignore]`d: `SIGABRT` from inside a test binary is a failing test
5713 /// run, and the point is for the *parent* to see the signal.
5714 ///
5715 /// The kill lands where it has to land — after `put_pack` has returned, so
5716 /// the bytes and the journal row are on the platter, and before any row is in
5717 /// the index, which the held absorb gate makes a **state** rather than a
5718 /// hope. Nothing is simulated: no injected error, no fault flag, no early
5719 /// return. The process is gone.
5720 #[test]
5721 #[ignore = "spawned by a_store_reopened_after_a_kill_mid_absorb_requeues_the_pack; it aborts on purpose"]
5722 fn the_child_that_dies_between_durable_bytes_and_indexed_rows() {
5723 let Ok(dir) = std::env::var(KILL_DIR) else {
5724 // Run by hand with `--ignored` and no fixture: there is nothing to
5725 // kill over, so do nothing rather than abort somebody's test run.
5726 return;
5727 };
5728 let dir = PathBuf::from(dir);
5729 let pack = std::fs::read(dir.join("fixture.pack")).expect("the parent's fixture");
5730
5731 let store = GitStore::open(&dir, "rickard").expect("open");
5732 // The drain is parked in front of its absorb for as long as this lives,
5733 // so no row can land before the abort below.
5734 let _gate = store.hold_absorb_gate();
5735 let tx = store.put_pack(&pack).expect("the ack path");
5736 assert_eq!(tx.pack_id, Some(0), "a fresh archive starts at ordinal 0");
5737 assert_eq!(store.unindexed_packs(), 1, "the pack is not queued");
5738 assert_eq!(store.index().len(), 0, "a row landed before the kill");
5739 std::fs::write(dir.join("ready"), b"durable, not indexed").expect("marker");
5740
5741 // Not `panic!`, not `exit`: the process stops here with a live gate, a
5742 // live worker and a live redb handle, which is what a machine that dies
5743 // mid-absorb leaves behind.
5744 std::process::abort();
5745 }
5746
5747 /// **A store reopened over a pack that was still being absorbed re-queues
5748 /// it, answers correctly the whole way through, and gets its derived tables
5749 /// back.**
5750 ///
5751 /// The interruption is a **real** one: a child process is spawned, pushes the
5752 /// pack, and is killed by `SIGABRT` between "bytes durable" and "rows
5753 /// indexed". The parent then asserts what the kill left on disk — one journal
5754 /// row, the pack verbatim, **zero** index rows — before opening a store over
5755 /// it, so the recovery is being tested against the state it claims to
5756 /// recover from and not against a lucky one.
5757 ///
5758 /// Three things are then asserted, all on applied output:
5759 ///
5760 /// 1. **the pack is re-queued** — the account indexer of the *new* process
5761 /// publishes ordinal 0, which it can only do for a job somebody submitted;
5762 /// 2. **a read is correct throughout** — `has` is asked the instant the store
5763 /// is open, before any drain has been waited for, and must answer `true`;
5764 /// 3. **`commit_count()` and `reachable()` come back** — the derived tables
5765 /// are rebuilt by the same absorb, so the graph holds exactly the pack's
5766 /// commits and a tip's closure is non-empty.
5767 ///
5768 /// Seen RED by making `open_with` re-queue nothing (`absorber
5769 /// .adopt_journal(&acked)?` → an empty `Vec`, which is what this store did
5770 /// before this change): "a durable object read `absent` from a store reopened
5771 /// over the pack that holds it — the crash-recovery bit did not re-queue
5772 /// anything". Restored.
5773 ///
5774 /// Seen RED a second time by inverting the diff in
5775 /// [`Absorber::adopt_journal`] (`if absorbed` → `if !absorbed`), so a pack
5776 /// with **no** rows has its bit set and a pack with rows is re-queued: the
5777 /// same first assertion fires, because a set bit over an empty index is
5778 /// exactly the wrong answer a bloom filter's false positive would have
5779 /// produced — the read stops falling back and says `absent` about an object
5780 /// whose bytes are durable. Restored.
5781 ///
5782 /// **Why the assertions are not immediately after the reopen.** The trap the
5783 /// drain guards already record applies here too, mirrored: right after
5784 /// `GitStore::open` the drain may legitimately have finished the re-queued
5785 /// pack already, so `unindexed_packs() == 1` is not assertable and is not
5786 /// asserted. What *is* assertable at that instant is the answer to a read,
5787 /// which must be `true` whichever side of the drain it lands on — and that is
5788 /// exactly the property §13.12 promises.
5789 ///
5790 /// **And it still holds now that a `gc` can retire a pack's journal row.**
5791 /// The tombstone tells "indexed then garbage-collected" from "never
5792 /// indexed", and this pack is the second: nothing retired it, so it must
5793 /// still be re-queued. Seen RED by making `adopt_journal` treat *every* pack
5794 /// as retired (`retired.contains(&extent.0)` → `true`): "a durable object
5795 /// read `absent` from a store reopened over the pack that holds it — the
5796 /// crash-recovery bit did not re-queue anything". Restored.
5797 #[test]
5798 fn a_store_reopened_after_a_kill_mid_absorb_requeues_the_pack() {
5799 use std::os::unix::process::ExitStatusExt;
5800
5801 let dir = tmpdir("kill-mid-absorb");
5802 let (pack, rows) = corpus_pack();
5803 std::fs::write(dir.join("fixture.pack"), &pack).unwrap();
5804
5805 let status = std::process::Command::new(std::env::current_exe().unwrap())
5806 .args([
5807 "--exact",
5808 "--ignored",
5809 "--nocapture",
5810 "git_ops::tests::the_child_that_dies_between_durable_bytes_and_indexed_rows",
5811 ])
5812 .env(KILL_DIR, &dir)
5813 .status()
5814 .expect("spawn the child that dies");
5815 assert_eq!(
5816 status.signal(),
5817 Some(6),
5818 "the child did not die by SIGABRT — it {status:?}, so nothing was interrupted"
5819 );
5820 assert!(
5821 dir.join("ready").exists(),
5822 "the child never reached the kill point"
5823 );
5824
5825 // ── what the kill left on disk ───────────────────────────────────────
5826 let blobs = dir.join("objects.pack");
5827 let acked = read_journal(&SafeWriter::journal_path(&blobs)).unwrap();
5828 assert_eq!(
5829 acked.len(),
5830 1,
5831 "the pack's extent is not durable: {acked:?}"
5832 );
5833 assert_eq!(acked[0].1, pack.len() as u64);
5834 let on_disk = std::fs::read(&blobs).unwrap();
5835 assert_eq!(
5836 &on_disk[acked[0].0 as usize..(acked[0].0 + acked[0].1) as usize],
5837 &pack[..],
5838 "the verbatim bytes did not survive the kill"
5839 );
5840 {
5841 // Opened and dropped before the store takes redb's file lock.
5842 let tail = ObjectReadStack::<OneTableFourColumns>::open(
5843 &dir.join("objects.tail"),
5844 RebuildTriggers::default(),
5845 crate::arms::DEFAULT_REDB_CACHE_BYTES,
5846 )
5847 .unwrap();
5848 assert_eq!(
5849 tail.len(),
5850 0,
5851 "the kill landed after the rows were indexed, not between the bytes and the rows \
5852 — this guard would then be testing nothing"
5853 );
5854 }
5855
5856 // ── the reopen ───────────────────────────────────────────────────────
5857 let store = GitStore::open(&dir, "rickard").unwrap();
5858 assert!(
5859 store.has(&rows[0].oid).unwrap(),
5860 "a durable object read `absent` from a store reopened over the pack that holds it — \
5861 the crash-recovery bit did not re-queue anything"
5862 );
5863
5864 store.wait_indexed();
5865 assert_eq!(
5866 store.object_count(),
5867 rows.len(),
5868 "the re-queued pack's objects never reached the index — left: {}, right: {}",
5869 store.object_count(),
5870 rows.len()
5871 );
5872 assert!(
5873 store.indexer().is_indexed(0),
5874 "the re-queued pack's indexed bit never went up in the new process"
5875 );
5876 assert_eq!(store.unindexed_packs(), 0);
5877 assert_eq!(store.absorb_pending().unwrap(), 0);
5878 for r in rows.iter().take(512) {
5879 let row = store
5880 .index()
5881 .lookup(&r.oid)
5882 .unwrap_or_else(|| panic!("{} has no row after recovery", hex::encode(&r.oid)));
5883 assert_eq!((row.offset, row.len), (acked[0].0 + r.offset, r.len));
5884 }
5885
5886 // ── the derived tables, which were the second symptom ────────────────
5887 assert_eq!(
5888 store.commit_count(),
5889 commits_in(&rows),
5890 "the commit graph did not come back: {} of {}",
5891 store.commit_count(),
5892 commits_in(&rows)
5893 );
5894 let tip = store
5895 .graph_snapshot()
5896 .into_iter()
5897 .max_by_key(|c| c.generation)
5898 .expect("a graph");
5899 let tip_raw = hex::decode(&tip.oid).unwrap();
5900 let closure = store.reachable(&[&tip_raw], &[]).unwrap();
5901 assert!(
5902 !closure.is_empty(),
5903 "reachable() came back empty after the reopen"
5904 );
5905 assert!(closure.contains(&tip_raw));
5906 eprintln!(
5907 "load {}; killed mid-absorb: {} journal extent(s) recovered {} object rows, {} \
5908 commits, closure of the tip {} objects",
5909 loadavg(),
5910 acked.len(),
5911 store.object_count(),
5912 store.commit_count(),
5913 closure.len(),
5914 );
5915 }
5916
5917 /// **A clean reopen re-queues nothing, and the next push gets a fresh
5918 /// ordinal.**
5919 ///
5920 /// The other side of the diff. A store whose pack was fully absorbed before
5921 /// it closed must come back with the bit **set**: re-absorbing would be a
5922 /// whole pack re-resolved for nothing, and — because the commit graph is a
5923 /// `Vec` — it is the same doubling the drain's own guard exists to prevent.
5924 ///
5925 /// "Re-queued nothing" is asserted deterministically rather than by a timing
5926 /// window: the account indexer in this process publishes a pack only for a
5927 /// job somebody submitted, so `is_indexed(0)` being **false** after
5928 /// `wait_indexed` is proof that open submitted none.
5929 ///
5930 /// The second half is the ordinal. Pack ids are the journal's row numbers, so
5931 /// a reopened store must resume after them. Asserted on applied output rather
5932 /// than on the id alone: the newly pushed pack's object has to be findable,
5933 /// and it is exactly what goes missing when a fresh pack is handed an
5934 /// absorbed pack's ordinal — `queue` skips it, the drain skips it, and its
5935 /// durable bytes are never indexed by anybody.
5936 ///
5937 /// Seen RED by making `Absorber::adopt_journal` treat every journal extent as
5938 /// unabsorbed (`let has_rows = vec![false; journal.len()]`): "a fully
5939 /// absorbed pack was re-queued on open — the diff is not exact".
5940 ///
5941 /// Seen RED a second time by `AtomicU64::new(packs_already_acked(...)?)` →
5942 /// `AtomicU64::new(0)` in `PushPath::with_absorber`: "a reopened store handed
5943 /// a fresh pack the ordinal of an absorbed one — left: Some(0), right:
5944 /// Some(1)". With that same edit **and the id assertion deleted**, the
5945 /// applied-output one goes too — "the second push's object never reached the
5946 /// index — its ordinal collided with an absorbed pack's, so every writer
5947 /// skipped it" — which is what says this guard is about the objects and not
5948 /// about a counter. Restored.
5949 #[test]
5950 fn a_clean_reopen_requeues_nothing_and_the_next_push_gets_a_fresh_ordinal() {
5951 let dir = tmpdir("clean-reopen");
5952 let (pack, rows) = corpus_pack();
5953 let (second, blob_oid) = one_blob_pack(b"a push that arrives after the restart");
5954
5955 {
5956 let store = GitStore::open(&dir, "rickard").unwrap();
5957 store.put_pack(&pack).unwrap();
5958 store.wait_indexed();
5959 assert_eq!(
5960 store.object_count(),
5961 rows.len(),
5962 "the first run did not index"
5963 );
5964 }
5965
5966 let store = GitStore::open(&dir, "rickard").unwrap();
5967 store.wait_indexed();
5968 assert!(
5969 !store.indexer().is_indexed(0),
5970 "a fully absorbed pack was re-queued on open — the diff is not exact"
5971 );
5972 assert_eq!(store.unindexed_packs(), 0);
5973 assert_eq!(
5974 store.absorb_pending().unwrap(),
5975 0,
5976 "the reopen left index work"
5977 );
5978 assert_eq!(
5979 store.object_count(),
5980 rows.len(),
5981 "the reopened store lost rows"
5982 );
5983 assert!(store.has(&rows[0].oid).unwrap());
5984
5985 let tx = store.put_pack(&second).unwrap();
5986 assert_eq!(
5987 tx.pack_id,
5988 Some(1),
5989 "a reopened store handed a fresh pack the ordinal of an absorbed one"
5990 );
5991 store.wait_indexed();
5992 assert!(
5993 store.has(&blob_oid).unwrap(),
5994 "the second push's object never reached the index — its ordinal collided with an \
5995 absorbed pack's, so every writer skipped it"
5996 );
5997 assert_eq!(store.object_count(), rows.len() + 1);
5998 assert!(store.indexer().is_indexed(1));
5999 // And the journal is a log: both packs' extents are in it, in order.
6000 let acked = read_journal(&SafeWriter::journal_path(store.blobs_path())).unwrap();
6001 assert_eq!(
6002 acked.len(),
6003 2,
6004 "the reopen truncated the journal: {acked:?}"
6005 );
6006 assert_eq!(acked[1], tx.extent.unwrap());
6007 }
6008
6009 /// **What the derive-on-open diff costs at a realistic pack count.**
6010 ///
6011 /// Not a guard — a measurement, with two assertions on it that keep it from
6012 /// measuring the wrong thing: every absorbed pack must come back `true`, and
6013 /// the one pack that has no rows must come back `false`.
6014 ///
6015 /// Both cases are timed because they are different algorithms in practice.
6016 /// The **clean reopen** — every pack absorbed — terminates as soon as each
6017 /// extent has been hit once, which happens after a few rows per pack because
6018 /// the tail is in oid order and an oid says nothing about which pack its
6019 /// object came from. The **crash** case has one pack with no rows at all, and
6020 /// proving an absence means reading the tail to the end.
6021 ///
6022 /// MEASURED on oden 2026-08-08, release, 1000 real pushed packs / 3686 object
6023 /// rows, 1-minute loadavg 2.77: **0.38 ms** clean and 0.39 ms with one pack
6024 /// un-absorbed (0.40 / 0.39 ms at loadavg 13.06, so the figure barely moves
6025 /// with the box). At this row count the two cases are the same, because a
6026 /// 3686-row tail is scanned to the end faster than the early exit saves
6027 /// anything; they diverge with the object count, and the table on
6028 /// [`ObjectReadStack::extents_with_rows`] carries that at a million rows
6029 /// (0.74 ms against 109 ms).
6030 ///
6031 /// The printed line carries the loadavg of its own run, the pack count and
6032 /// the row count, because none of the three is comparable across machines or
6033 /// across a busy box.
6034 #[test]
6035 fn the_derive_on_open_diff_costs_milliseconds_at_a_realistic_pack_count() {
6036 const PACKS: usize = 1000;
6037 let dir = tmpdir("diff-cost");
6038 let (real, rows) = corpus_pack();
6039
6040 {
6041 let store = GitStore::open(&dir, "rickard").unwrap();
6042 store.put_pack(&real).unwrap();
6043 for i in 0..PACKS - 1 {
6044 let (p, _) = one_blob_pack(format!("pack number {i}").as_bytes());
6045 store.put_pack(&p).unwrap();
6046 }
6047 store.wait_indexed();
6048 assert_eq!(
6049 store.object_count(),
6050 rows.len() + PACKS - 1,
6051 "the fixture did not absorb"
6052 );
6053 }
6054
6055 let acked = read_journal(&SafeWriter::journal_path(&dir.join("objects.pack"))).unwrap();
6056 assert_eq!(acked.len(), PACKS, "one journal row per pack");
6057 // The tail on its own, so what is timed is the diff rather than redb's
6058 // open and the projection build that a store's open also pays for.
6059 let tail = ObjectReadStack::<OneTableFourColumns>::open(
6060 &dir.join("objects.tail"),
6061 RebuildTriggers::default(),
6062 crate::arms::DEFAULT_REDB_CACHE_BYTES,
6063 )
6064 .unwrap();
6065
6066 let t = Instant::now();
6067 let hit = tail.extents_with_rows(&acked).unwrap();
6068 let clean = t.elapsed();
6069 assert!(
6070 hit.iter().all(|&h| h),
6071 "an absorbed pack was reported unabsorbed"
6072 );
6073
6074 // One pack in flight when the machine died: durable bytes, no rows.
6075 let mut in_flight = acked.clone();
6076 let end = acked[PACKS - 1].0 + acked[PACKS - 1].1;
6077 in_flight.push((end, 4096));
6078 let t = Instant::now();
6079 let hit = tail.extents_with_rows(&in_flight).unwrap();
6080 let crashed = t.elapsed();
6081 assert_eq!(
6082 hit.iter().filter(|h| !**h).count(),
6083 1,
6084 "the pack with no rows is not the only one reported unabsorbed"
6085 );
6086
6087 eprintln!(
6088 "load {}; {PACKS} packs / {} object rows: derive-on-open diff {:.2} ms clean \
6089 (early exit), {:.2} ms with one pack un-absorbed (full tail scan)",
6090 loadavg(),
6091 tail.len(),
6092 clean.as_secs_f64() * 1e3,
6093 crashed.as_secs_f64() * 1e3,
6094 );
6095 }
6096
6097 /// **The bit is a bitset over dense ordinals, and one bit is one pack.**
6098 ///
6099 /// Ordinals that straddle word boundaries (63, 64, 65) are the whole point:
6100 /// a shift that ignores the word split, or a word index that does not, sets
6101 /// a neighbour's bit — and a neighbour's bit set means a durable pack is
6102 /// declared absorbed with no rows behind it.
6103 ///
6104 /// Seen RED by `>> (i % 64) & 1` → `>> (i % 63) & 1` in
6105 /// `PackState::is_absorbed`: "the bit for ordinal 64 did not go up". Restored.
6106 ///
6107 /// Seen RED a second time by `if self.extent[i].is_none()` → `if
6108 /// self.extent[i].is_none() || true` in `PackState::note`, i.e. letting a
6109 /// re-queue of a pack that is already recorded count as new work — which is
6110 /// the race the ack path really produces, since the drain can absorb before
6111 /// `queue` runs: "an absorbed pack was queued again — left: 6, right: 5".
6112 /// Restored.
6113 #[test]
6114 fn the_indexed_bit_is_a_bitset_over_dense_pack_ordinals() {
6115 let ids = [0u64, 1, 63, 64, 65, 4095];
6116 let mut s = PackState::default();
6117 for id in ids {
6118 s.note(id, (id * 4096 + 12, 4096));
6119 }
6120 assert_eq!(s.unabsorbed, ids.len());
6121 assert!(
6122 ids.iter().all(|&i| !s.is_absorbed(i)),
6123 "a bit is up already"
6124 );
6125
6126 s.mark_absorbed(64, (64 * 4096 + 12, 4096));
6127 assert!(s.is_absorbed(64), "the bit for ordinal 64 did not go up");
6128 for other in [0u64, 1, 63, 65, 4095] {
6129 assert!(
6130 !s.is_absorbed(other),
6131 "setting ordinal 64 also set ordinal {other} — one bit spilled onto a neighbour"
6132 );
6133 }
6134 assert_eq!(s.unabsorbed, ids.len() - 1);
6135 assert_eq!(
6136 s.pending().iter().map(|p| p.pack_id).collect::<Vec<_>>(),
6137 vec![0, 1, 63, 65, 4095],
6138 "the pending list is not the clear bits"
6139 );
6140 assert_eq!(
6141 s.pending()[0],
6142 PendingPack {
6143 pack_id: 0,
6144 offset: 12,
6145 len: 4096,
6146 resumed: true,
6147 },
6148 "a pending pack lost the extent it must be absorbed from"
6149 );
6150
6151 // Both writers are idempotent, because both of them race.
6152 s.mark_absorbed(64, (64 * 4096 + 12, 4096));
6153 s.note(64, (64 * 4096 + 12, 4096));
6154 assert!(s.is_absorbed(64));
6155 assert_eq!(
6156 s.unabsorbed,
6157 ids.len() - 1,
6158 "an absorbed pack was queued again"
6159 );
6160
6161 // One bit per pack, and that is the whole cost: 4096 ordinals is 64
6162 // words. A hash set of the same ordinals is an order of magnitude more,
6163 // and a bloom would be smaller and wrong.
6164 assert_eq!(s.words.len(), 64);
6165 assert_eq!(s.words.len() * 8, 512, "4096 packs is 512 bytes of bits");
6166 }
6167
6168 /// **A read that arrives before the drain is served from the pack, on its
6169 /// own thread, and never waits for the worker.**
6170 ///
6171 /// Rickard's rule, 2026-08-30: *nobody waits on the indexer.* Until then
6172 /// this test asserted the opposite — that a read parked behind the absorb
6173 /// gate stayed unanswered until the drain released it, which on a kernel
6174 /// pack is hours, and under the memory watchdog is *as long as the box is
6175 /// short*. Now the gate is the absorbers' and only the absorbers': the read
6176 /// finds no row, sees a pending pack, and resolves that pack read-only
6177 /// from the mmapped bytes for the one oid it wants
6178 /// (`Absorber::scan_pending`). The drain is still parked, the index is
6179 /// still empty, and the answer is **present** — from the verbatim truth.
6180 ///
6181 /// Seen RED by making `lookup_never_wait` return `Ok(None)` when the index
6182 /// misses: "a read with the drain parked answered absent for a durable
6183 /// object". And by putting `self.gate.lock()` back in front of it: the
6184 /// 2-second bound below trips with the reader still parked.
6185 #[test]
6186 fn a_read_before_the_drain_is_served_from_the_pack_and_never_waits() {
6187 let dir = tmpdir("drain-before");
6188 let store = Arc::new(GitStore::open(&dir, "rickard").unwrap());
6189 let (pack, rows) = corpus_pack();
6190 let (second, blob_oid) = one_blob_pack(b"a second push while the drain is parked");
6191
6192 // The drain is parked here, before it can absorb anything.
6193 let gate = store.hold_absorb_gate();
6194 let tx = store.put_pack(&pack).unwrap();
6195 let pack_id = tx.pack_id.unwrap();
6196 assert_eq!(store.unindexed_packs(), 1, "the pack is not queued");
6197 assert_eq!(store.index().len(), 0, "the index already holds rows");
6198 assert!(!store.indexer().is_indexed(pack_id), "the bit is already up");
6199
6200 // (1) a second push does not wait either: the ack path never meets the gate.
6201 let t = Instant::now();
6202 let tx2 = store.put_pack(&second).unwrap();
6203 assert_eq!(tx2.pack_id, Some(pack_id + 1));
6204 assert!(t.elapsed() < Duration::from_secs(2), "the push waited on the parked drain");
6205
6206 // (2) reads into exactly that state: present, from the pack, bounded.
6207 let oid = rows[0].oid.clone();
6208 let last = rows[rows.len() - 1].oid.clone();
6209 let t = Instant::now();
6210 assert!(store.has(&oid).unwrap(), "a read with the drain parked answered absent for a durable object");
6211 assert!(store.has(&last).unwrap(), "the last object of the pack answered absent");
6212 assert!(store.has(&blob_oid).unwrap(), "the second (still pending) pack's object answered absent");
6213 let (kind, body) = store.content(&blob_oid).unwrap().expect("content from the pending pack");
6214 assert_eq!(kind, GitObjectKind::Blob);
6215 assert_eq!(body, b"a second push while the drain is parked");
6216 let stored = store.get(&oid).unwrap().expect("get from the pending pack");
6217 assert_eq!(stored.extent.0, rows[0].offset + tx.extent.unwrap().0);
6218 assert_eq!(
6219 store.size(&last).unwrap(),
6220 Some(rows[rows.len() - 1].uncompressed_size),
6221 "size from the pending pack"
6222 );
6223 let absent = vec![0xEEu8; 20];
6224 assert!(!store.has(&absent).unwrap(), "an oid in no pack must still be absent");
6225 let elapsed = t.elapsed();
6226 assert!(elapsed < Duration::from_secs(20), "reads took {elapsed:?} with the drain parked");
6227 // The drain is STILL parked and the index STILL empty: nothing above
6228 // absorbed anything, and nothing waited.
6229 assert_eq!(store.index().len(), 0, "a read absorbed the pack behind the parked gate");
6230 assert!(!store.indexer().is_indexed(pack_id));
6231 assert_eq!(store.unindexed_packs(), 2);
6232 eprintln!("{} reads served from two pending packs with the drain parked, in {elapsed:?}", 6);
6233
6234 // (3) a whole-graph request is refused by name, not waited on.
6235 let err = store.reachable(&[oid.as_slice()], &[]).unwrap_err();
6236 assert!(err.to_string().contains("index in progress"), "{err:#}");
6237
6238 drop(gate);
6239 // And afterwards it is served from the rows.
6240 store.wait_indexed();
6241 assert_eq!(store.unindexed_packs(), 0);
6242 assert!(store.indexer().is_indexed(pack_id));
6243 assert_eq!(store.index().len(), rows.len() + 1);
6244 assert_eq!(store.absorb_pending().unwrap(), 0);
6245 for r in rows.iter().take(256) {
6246 assert!(
6247 store.index().lookup(&r.oid).is_some(),
6248 "{} is not in the rows after the drain",
6249 hex::encode(&r.oid)
6250 );
6251 }
6252 assert!(store.reachable(&[oid.as_slice()], &[]).is_ok(), "reachable after the drain");
6253 }
6254
6255 /// **The memory watchdog pauses the indexer worker and nobody else.**
6256 ///
6257 /// A fake `/proc/meminfo` under the floor is put under the process-wide
6258 /// watchdog; the worker parks in front of its batch (`index_worker`'s
6259 /// pre-batch check) and stays parked. Meanwhile a push is acked, every
6260 /// kind of read answers from the pack, and the progress snapshot returns —
6261 /// all bounded, none of them behind the pause. Then the fake is raised,
6262 /// the worker resumes, and the pack is indexed normally.
6263 ///
6264 /// Seen RED with the pre-batch check taking the absorb gate before pausing
6265 /// (the reads' scan does not need the gate, but `absorb_pending`-based
6266 /// reads did): every read parked for the length of the pause.
6267 #[test]
6268 fn a_paused_indexer_blocks_no_push_and_no_read() {
6269 let dir = tmpdir("paused-indexer");
6270 let meminfo = dir.join("meminfo");
6271 let write = |avail_kib: u64| {
6272 std::fs::write(&meminfo, format!("MemTotal: 16000000 kB\nMemAvailable: {avail_kib} kB\n"))
6273 .unwrap()
6274 };
6275 write(1_000_000);
6276 let dog = Arc::new(crate::memguard::MemWatchdog::with(
6277 &meminfo,
6278 3_200_000 * 1024,
6279 Duration::from_millis(10),
6280 ));
6281 let store = Arc::new(GitStore::open(&dir, "rickard").unwrap());
6282 // This store's worker only: every other store in the process keeps
6283 // the real watchdog.
6284 store.set_mem_watchdog(dog.clone());
6285 let (pack, rows) = corpus_pack();
6286 let (second, blob_oid) = one_blob_pack(b"pushed while the indexer is paused");
6287
6288 let t_push = Instant::now();
6289 let tx = store.put_pack(&pack).unwrap();
6290 let pack_id = tx.pack_id.unwrap();
6291 // Give the worker time to reach its pre-batch check and park.
6292 let mut parked = false;
6293 for _ in 0..200 {
6294 if dog.is_paused() {
6295 parked = true;
6296 break;
6297 }
6298 std::thread::sleep(Duration::from_millis(10));
6299 }
6300 assert!(parked, "the worker never paused under a meminfo 1 GB < 3.2 GB");
6301 assert!(!store.indexer().is_indexed(pack_id), "indexed while paused");
6302 assert_eq!(store.index().len(), 0);
6303
6304 // The ack path: a push while paused.
6305 let tx2 = store.put_pack(&second).unwrap();
6306 assert_eq!(tx2.pack_id, Some(pack_id + 1));
6307 assert!(t_push.elapsed() < Duration::from_secs(5), "a push waited on the paused worker");
6308
6309 // Reads while paused: present, from the pack.
6310 let t = Instant::now();
6311 assert!(store.has(&rows[0].oid).unwrap());
6312 assert!(store.has(&blob_oid).unwrap());
6313 let (kind, body) = store.content(&blob_oid).unwrap().unwrap();
6314 assert_eq!((kind, body.as_slice()), (GitObjectKind::Blob, &b"pushed while the indexer is paused"[..]));
6315 assert_eq!(
6316 store.extents(&[rows[0].oid.as_slice(), rows[1].oid.as_slice(), blob_oid.as_slice()])
6317 .unwrap()
6318 .iter()
6319 .filter(|e| e.is_some())
6320 .count(),
6321 3,
6322 "the batch path answered absent while paused"
6323 );
6324 assert!(t.elapsed() < Duration::from_secs(20), "reads waited on the paused worker");
6325 // The progress row: information, never a gate.
6326 let snap = store.index_progress();
6327 assert_eq!(snap.root, store.root().display().to_string());
6328 assert!(dog.is_paused(), "the worker resumed on its own — the fake still says 1 GB");
6329 assert!(!store.indexer().is_indexed(pack_id), "indexed while still paused");
6330
6331 // A whole-graph request while the worker is paused: the gate is free
6332 // (a paused worker holds nothing), so the READER absorbs the pending
6333 // packs on its own thread and answers — the worker is still parked.
6334 let reach = store.reachable(&[rows[0].oid.as_slice()], &[]).unwrap();
6335 assert!(!reach.is_empty());
6336 assert_eq!(store.unindexed_packs(), 0, "the reader did not absorb for itself");
6337 assert!(dog.is_paused(), "the worker is still parked; the reader did the work");
6338 assert_eq!(store.index().len(), rows.len() + 1);
6339
6340 // Memory comes back: the worker resumes, finds nothing to do, and the
6341 // bits go up through the ordinary drain.
6342 write(9_000_000);
6343 store.wait_indexed();
6344 assert!(store.indexer().is_indexed(pack_id));
6345 assert!(store.indexer().is_indexed(pack_id + 1));
6346 assert_eq!(dog.pauses(), 1, "one pause, one resume");
6347 assert!(!dog.is_paused());
6348 }
6349
6350 /// **The bit is cleared after the rows go in, never before.**
6351 ///
6352 /// The ordering inside `absorb_gated` is the whole reason a read cannot be
6353 /// told "absent" while the drain is running, and the only way to see it is to
6354 /// read *during* one. Four gatling workers (LAW 3 — never rayon, and never a
6355 /// raw thread pool) hammer `has` over the pack's oids from the moment
6356 /// `put_pack` returns until the pack's bit is up.
6357 ///
6358 /// Asserted on applied output: not one of those reads may answer absent for
6359 /// an object whose bytes are already durable.
6360 ///
6361 /// Seen RED by clearing the bit *before* absorbing (a
6362 /// `s.pending.retain(|p| p.pack_id != job.pack_id)` added in `absorb_gated`
6363 /// above the `self.absorb_one(job)` call): "4 reader(s) were told a durable
6364 /// object was absent — **32 865 593** absent answers over 2687 objects".
6365 /// Restored. The count is that large because the readers spin until the bit
6366 /// goes up and, with the bit cleared first, every pass over the sample missed
6367 /// for the whole length of the absorb.
6368 ///
6369 /// The honest limit: this is a race, so a green run proves the window was not
6370 /// hit, not that no window exists. That is why the printed line carries how
6371 /// many of the reads actually landed while the pack was still un-indexed —
6372 /// **a run where that number is 0 proved nothing**, and it is reported rather
6373 /// than asserted because forcing it would mean pausing the drain, which is
6374 /// the one thing this test must not do. MEASURED 2026-08-08 on oden at
6375 /// loadavg 16.94: 336 of 336 reads landed early.
6376 #[test]
6377 fn no_read_is_told_absent_while_the_drain_is_running() {
6378 let dir = tmpdir("drain-during");
6379 let store = GitStore::open(&dir, "rickard").unwrap();
6380 let (pack, rows) = corpus_pack();
6381 let oids: Vec<&[u8]> = rows.iter().map(|r| r.oid.as_slice()).collect();
6382
6383 let tx = store.put_pack(&pack).unwrap();
6384 let pack_id = tx.pack_id.unwrap();
6385
6386 let early = AtomicU64::new(0);
6387 let absent = AtomicU64::new(0);
6388 let reads = AtomicU64::new(0);
6389 let workers = 4usize;
6390 gatling_for_each(workers, workers, |w| loop {
6391 let still_pending = store.unindexed_packs() > 0;
6392 for oid in oids.iter().skip(w).step_by(workers * 8) {
6393 if still_pending {
6394 early.fetch_add(1, AtomicOrdering::Relaxed);
6395 }
6396 reads.fetch_add(1, AtomicOrdering::Relaxed);
6397 if !store.has(oid).unwrap() {
6398 absent.fetch_add(1, AtomicOrdering::Relaxed);
6399 }
6400 }
6401 if store.indexer().is_indexed(pack_id) {
6402 break;
6403 }
6404 });
6405
6406 let absent = absent.load(AtomicOrdering::Acquire);
6407 assert_eq!(
6408 absent,
6409 0,
6410 "{workers} reader(s) were told a durable object was absent — {absent} absent answers \
6411 over {} objects",
6412 rows.len()
6413 );
6414 assert_eq!(store.object_count(), rows.len());
6415 eprintln!(
6416 "load {}; {} reads across {workers} gatling workers, {} of them while the pack was \
6417 still un-indexed",
6418 loadavg(),
6419 reads.load(AtomicOrdering::Acquire),
6420 early.load(AtomicOrdering::Acquire),
6421 );
6422 }
6423
6424 /// **One pack is absorbed once, whichever of the two callers gets there
6425 /// first.**
6426 ///
6427 /// Both callers are made to attempt it, deterministically: with the gate
6428 /// held, the drain is parked inside `ObjectAbsorb::absorb` and a reader is
6429 /// parked inside `absorb_pending`, both past their own early exits. Releasing
6430 /// the gate lets exactly one do the work; the other must take the skip.
6431 ///
6432 /// The observable is the **commit graph**, not the object table:
6433 /// `ObjectReadStack::append` is keyed by oid and would swallow a second
6434 /// identical row, but the graph is a `Vec` and a second absorb pushes every
6435 /// commit into it again — which silently doubles the fold every reachability
6436 /// bitmap is built from.
6437 ///
6438 /// Seen RED by disabling the `s.absorbed.contains(&job.pack_id)` skip in
6439 /// `absorb_gated`: "the commit graph carries **1102** commits, the pack has
6440 /// **551**" — exactly double, both callers having done the whole fold.
6441 /// Restored.
6442 #[test]
6443 fn a_pack_is_absorbed_once_even_when_both_callers_race_for_it() {
6444 let dir = tmpdir("drain-once");
6445 let store = Arc::new(GitStore::open(&dir, "rickard").unwrap());
6446 let (pack, rows) = corpus_pack();
6447
6448 let gate = store.hold_absorb_gate();
6449 store.put_pack(&pack).unwrap();
6450
6451 let s = store.clone();
6452 let reader = Job::spawn(move || s.absorb_pending().unwrap());
6453 // Both are now blocked on the gate: the drain inside `absorb`, the reader
6454 // inside `absorb_pending`, each having already decided there is work.
6455 std::thread::sleep(Duration::from_millis(100));
6456 drop(gate);
6457
6458 let by_read = reader.join().unwrap();
6459 store.wait_indexed();
6460
6461 assert_eq!(
6462 store.commit_count(),
6463 commits_in(&rows),
6464 "the commit graph carries {} commits, the pack has {}",
6465 store.commit_count(),
6466 commits_in(&rows)
6467 );
6468 assert_eq!(store.object_count(), rows.len());
6469 assert_eq!(store.unindexed_packs(), 0);
6470 eprintln!(
6471 "load {}; the read absorbed {by_read} pack(s), the drain absorbed the rest",
6472 loadavg()
6473 );
6474 }
6475
6476 // ── §14's exploded table ─────────────────────────────────────────────────
6477
6478 /// **A push produces an exploded row for every object in the pack, and each
6479 /// one hashes back to its own oid.**
6480 ///
6481 /// Eager, decided 2026-08-08: no threshold, no size cap, no type skipped —
6482 /// so the row count is the *object* count and not some subset of it. Nothing
6483 /// is read between `put_pack` and `wait_indexed`, so the only thing that
6484 /// could have built the rows is the background drain, which is the half of
6485 /// §14 that says "built by the same background indexer".
6486 ///
6487 /// The assertion is applied output in its strongest available form: every
6488 /// object's content is read back out of the table, re-serialised canonically
6489 /// and **re-hashed**, and the hash has to be the oid it was filed under. A
6490 /// row count alone could be satisfied by 2687 rows of the wrong bytes; a
6491 /// re-hash cannot.
6492 ///
6493 /// Seen RED by deleting the `sink.explode(&oid, kind, &payload)?` call in
6494 /// `resolve::resolve_walked`: "the drain built no exploded rows: 0 of 2687".
6495 /// Restored.
6496 ///
6497 /// Seen RED a second time by making the sink skip blobs in
6498 /// `ExplodedTable::explode` (`if kind == GitObjectKind::Blob { return Ok(()) }`
6499 /// at the top — which is what a *lazy* or size-capped table would look like
6500 /// from here): "the drain built no exploded rows: 1420 of 2687". Restored,
6501 /// because §14's open question was closed EAGER and disk is explicitly not a
6502 /// consideration — and because the 1267 rows that edit removes are exactly
6503 /// the blobs a content read wants most.
6504 #[test]
6505 fn a_push_produces_an_exploded_row_for_every_object_in_the_pack() {
6506 let dir = tmpdir("exploded-every-object");
6507 let store = GitStore::open(&dir, "rickard").unwrap();
6508 let (pack, rows) = corpus_pack();
6509
6510 let t = Instant::now();
6511 store.put_pack(&pack).unwrap();
6512 let ack = t.elapsed();
6513 store.wait_indexed();
6514 // **Drain, printed beside ack**, because the two answer different
6515 // questions and only one of them was ever in doubt. Ack is unchanged by
6516 // construction — nothing on it touches this table. Drain is where the
6517 // eager resolve and the sink both live, and it is the column redb's
6518 // 2026-08-08 measurement recorded as 316.2–329.8 ms against 166.7–168.6
6519 // ms with the sink stubbed out. Without it here, "did the medium buy
6520 // anything but disk" has no answer in this repository.
6521 let drain = t.elapsed();
6522
6523 let distinct: HashSet<Vec<u8>> = rows.iter().map(|r| r.oid.clone()).collect();
6524 let stats = store.exploded_stats();
6525 // **The push explodes the GRAPH, not the content.** Under the shipped
6526 // policy the drain writes commits and trees and leaves blobs in the
6527 // pack; the assertion is therefore two-sided, because "some rows" would
6528 // pass on a table that wrote one row and on one that wrote all of them,
6529 // and it is exactly the second that costs 17× the pack.
6530 let graph_rows = store.exploded_of_kind(GitObjectKind::Commit).unwrap().len();
6531 assert_eq!(
6532 store.exploded_eager_rows().unwrap(),
6533 graph_rows as u64,
6534 "the drain wrote {} eager rows and there are {} commits — the eager pool \
6535 holds something other than the graph",
6536 store.exploded_eager_rows().unwrap(),
6537 graph_rows
6538 );
6539 assert!(
6540 stats.rows > 0 && stats.rows < distinct.len() as u64,
6541 "the eager pool holds {} of {} objects; lazy explode means neither 0 \
6542 (the graph would be blind) nor all of them (that is the 22.4x tier)",
6543 stats.rows,
6544 distinct.len()
6545 );
6546
6547 // Every one of them, re-hashed. Bytes that came back wrong cannot pass.
6548 let hash = store.hash_kind();
6549 let mut checked = 0usize;
6550 for oid in &distinct {
6551 let (kind, payload) = store
6552 .content(oid)
6553 .unwrap()
6554 .unwrap_or_else(|| panic!("{} has no exploded row", hex::encode(oid)));
6555 let rehashed = hash.oid_of(&crate::object::canonical(kind, &payload));
6556 assert_eq!(
6557 &rehashed,
6558 oid,
6559 "the exploded row filed under {} contains an object that hashes to {}",
6560 hex::encode(oid),
6561 hex::encode(&rehashed)
6562 );
6563 checked += 1;
6564 }
6565 assert_eq!(checked, distinct.len());
6566 // **Every object came back byte-correct**, and the ones the eager pool
6567 // does not hold came back by inflating their own delta chain out of the
6568 // mapped pack ([`crate::resolve::materialize_at`]). So the re-derivation
6569 // count is the blob count and not zero — asserting zero here is what a
6570 // test written for the eager tier does, and it would now be asserting
6571 // that lazy explode is off.
6572 let after_reads = store.exploded_stats();
6573 assert!(
6574 after_reads.rederived > 0,
6575 "no read re-derived anything, so either every object is eager or the \
6576 lazy path never ran"
6577 );
6578 assert_eq!(
6579 after_reads.rederived + after_reads.served,
6580 checked as u64,
6581 "reads {} = served {} + rederived {} must account for every object",
6582 checked,
6583 after_reads.served,
6584 after_reads.rederived
6585 );
6586 // **What it costs on disk, measured rather than assumed.** §14 says the
6587 // price is "both copies are held" and the decision was taken with "we
6588 // don't care if disk is tripled" — so the real multiple belongs in the
6589 // output where anybody can read it, not in a sentence. It is not 3x on a
6590 // real pack: the verbatim bytes are deflated *and* delta-encoded, the
6591 // table holds raw inflated content, and redb pages it.
6592 let inflated: u64 = rows.iter().map(|r| r.uncompressed_size).sum();
6593 let table = std::fs::metadata(store.exploded_path())
6594 .map(|m| m.len())
6595 .unwrap_or(0);
6596 eprintln!(
6597 "load {}; {} pack entries → {} exploded rows, all {} re-hashed to their own oid; \
6598 ack {:.1} ms, drain {:.1} ms, {:.0} rows/s; disk: pack {:.1} MiB verbatim, objects \
6599 inflate to {:.1} MiB, table file {:.1} MiB ({:.0}x the pack, {:.3}x the payload)",
6600 loadavg(),
6601 rows.len(),
6602 stats.rows,
6603 checked,
6604 ack.as_secs_f64() * 1e3,
6605 drain.as_secs_f64() * 1e3,
6606 stats.rows as f64 / drain.as_secs_f64(),
6607 pack.len() as f64 / (1 << 20) as f64,
6608 inflated as f64 / (1 << 20) as f64,
6609 table as f64 / (1 << 20) as f64,
6610 table as f64 / pack.len() as f64,
6611 table as f64 / inflated.max(1) as f64,
6612 );
6613 }
6614
6615 /// **The amplification measurement, at whatever scale you point it at.**
6616 ///
6617 /// [`a_push_produces_an_exploded_row_for_every_object_in_the_pack`] prints
6618 /// the same ratio on a 5.4 MiB fixture and is a *guard*, so it has to stay
6619 /// fast. This is the same arithmetic with no assertion about size and no
6620 /// fixture of its own: give it a pack and it reports what the table cost.
6621 ///
6622 /// ```text
6623 /// ZNIPPY_EXPLODE_BENCH_PACK=/path/to/pack-….pack \
6624 /// cargo test -p znippy-plugin-git --lib the_table_costs_what_it_holds \
6625 /// -- --ignored --nocapture
6626 /// ```
6627 ///
6628 /// It exists because the 12× figure that killed redb — 16.8 GB of resolved
6629 /// `linux.git` content in a 204 GB file — was taken by hand off a bench run
6630 /// that no longer exists, and a number nobody can re-take is a number that
6631 /// rots. `#[ignore]`d and pack-less by default: with no pack named it
6632 /// returns, so it never fails a suite for the absence of a corpus nobody
6633 /// promised to keep.
6634 #[test]
6635 #[ignore = "needs a pack: ZNIPPY_EXPLODE_BENCH_PACK=<path> cargo test … -- --ignored --nocapture"]
6636 fn the_table_costs_what_it_holds() {
6637 let Ok(path) = std::env::var("ZNIPPY_EXPLODE_BENCH_PACK") else {
6638 eprintln!("no ZNIPPY_EXPLODE_BENCH_PACK named; nothing measured");
6639 return;
6640 };
6641 let pack = std::fs::read(&path).unwrap_or_else(|e| panic!("reading {path}: {e}"));
6642 let dir = tmpdir("exploded-cost");
6643 let store = GitStore::open(&dir, "rickard").unwrap();
6644
6645 let t = Instant::now();
6646 store.put_pack(&pack).unwrap();
6647 let ack = t.elapsed();
6648 store.wait_indexed();
6649 let drain = t.elapsed();
6650
6651 let stats = store.exploded_stats();
6652 let table = std::fs::metadata(store.exploded_path())
6653 .map(|m| m.len())
6654 .unwrap_or(0);
6655 // The payload the table actually holds, asked of the table rather than
6656 // of the pack — a pack entry's `uncompressed_size` is what git recorded,
6657 // and this is what we stored. Only the second one can be divided into
6658 // the file size and mean anything.
6659 let mut held = 0u64;
6660 for kind in [
6661 GitObjectKind::Commit,
6662 GitObjectKind::Tree,
6663 GitObjectKind::Blob,
6664 GitObjectKind::Tag,
6665 ] {
6666 held += store
6667 .exploded_of_kind(kind)
6668 .unwrap()
6669 .iter()
6670 .map(|(_, p)| p.len() as u64)
6671 .sum::<u64>();
6672 }
6673 let mib = |n: u64| n as f64 / (1 << 20) as f64;
6674 eprintln!(
6675 "load {}; pack {} — {:.1} MiB verbatim → {} rows holding {:.1} MiB, table file \
6676 {:.1} MiB = {:.3}x the payload and {:.1}x the pack; ack {:.0} ms, drain {:.0} ms",
6677 loadavg(),
6678 path,
6679 mib(pack.len() as u64),
6680 stats.rows,
6681 mib(held),
6682 mib(table),
6683 table as f64 / held.max(1) as f64,
6684 table as f64 / pack.len() as f64,
6685 ack.as_secs_f64() * 1e3,
6686 drain.as_secs_f64() * 1e3,
6687 );
6688 }
6689
6690 /// Where the child of the clean-shutdown guard finds its fixture and leaves
6691 /// its markers.
6692 const CLEAN_DIR: &str = "GUNNAR_CLEAN_SHUTDOWN_DIR";
6693
6694 /// **The other half of
6695 /// [`a_clean_shutdown_and_reopen_keeps_the_graph_and_reachable`]: the process
6696 /// that exits normally.**
6697 ///
6698 /// A real process, a real push, a real drain, and then a real `drop` — every
6699 /// worker joined, every redb handle closed, no signal, exit status 0. That is
6700 /// what makes the parent's reopen a *clean* reopen rather than a recovery,
6701 /// and it is the case the crash guard cannot reach: after a clean shutdown
6702 /// every pack's bit is legitimately set and **nothing is re-queued**, so the
6703 /// derived tables have to have been on disk or they are gone.
6704 ///
6705 /// `#[ignore]`d because it is a fixture, not a guard — run on its own it has
6706 /// no directory to work in and returns.
6707 #[test]
6708 #[ignore = "spawned by a_clean_shutdown_and_reopen_keeps_the_graph_and_reachable"]
6709 fn the_child_that_pushes_and_shuts_down_cleanly() {
6710 let Ok(dir) = std::env::var(CLEAN_DIR) else {
6711 return;
6712 };
6713 let dir = PathBuf::from(dir);
6714 let pack = std::fs::read(dir.join("fixture.pack")).expect("the parent's fixture");
6715
6716 let store = GitStore::open(&dir, "rickard").expect("open");
6717 store.put_pack(&pack).expect("the ack path");
6718 store.wait_indexed();
6719 assert_eq!(store.unindexed_packs(), 0, "the child shut down mid-index");
6720
6721 let tip = store
6722 .graph_snapshot()
6723 .into_iter()
6724 .max_by_key(|c| c.generation)
6725 .expect("the child built no graph at all");
6726 let tip_raw = hex::decode(&tip.oid).unwrap();
6727 let closure = store.reachable(&[&tip_raw], &[]).unwrap();
6728 assert!(!closure.is_empty(), "the child's own reachable() was empty");
6729
6730 std::fs::write(dir.join("tip"), &tip.oid).expect("marker");
6731 std::fs::write(dir.join("closure"), closure.len().to_string()).expect("marker");
6732 std::fs::write(dir.join("commits"), store.commit_count().to_string()).expect("marker");
6733 std::fs::write(dir.join("objects"), store.object_count().to_string()).expect("marker");
6734
6735 // **The clean shutdown.** Not a signal and not an abort: the store drops,
6736 // which closes the channel, joins the account indexer's worker and closes
6737 // redb. The process then returns normally with status 0.
6738 drop(store);
6739 std::fs::write(dir.join("clean"), b"closed").expect("marker");
6740 }
6741
6742 /// **`commit_count()` and `reachable()` are correct after a CLEAN shutdown
6743 /// and reopen.** This is the guard the exploded table exists to make pass.
6744 ///
6745 /// It was the live bug: the commit graph was folded from commit and tree
6746 /// *payloads* which were stored nowhere, accumulated in RAM as packs were
6747 /// absorbed. A store killed mid-absorb got them back only because the
6748 /// interrupted pack was re-queued and re-resolved
6749 /// ([`a_store_reopened_after_a_kill_mid_absorb_requeues_the_pack`]); a store
6750 /// that shut down **cleanly** re-queued nothing, and MEASURED on oden
6751 /// 2026-08-08 came back with 2687 rows and **0 of 551 commits** —
6752 /// `reachable()` returning a commit alone instead of its closure, and `gc`
6753 /// seeing an empty live set. Both are quiet wrong answers.
6754 ///
6755 /// The shape is the crash guard's: a **real** child process, which here exits
6756 /// normally, and the parent asserts against what is on disk. Two assertions
6757 /// carry it and they have to be in this order:
6758 ///
6759 /// 1. **`commit_count()` immediately after `open`**, before anything is
6760 /// waited on — so the graph can only have come from the fold over the
6761 /// exploded table that `open` performs, not from a re-absorb;
6762 /// 2. **`is_indexed(0)` is false after `wait_indexed()`** — the account
6763 /// indexer in *this* process publishes a pack only for a job somebody
6764 /// submitted, so a clear bit proves open re-queued nothing and the graph
6765 /// above is not a recovery artefact.
6766 ///
6767 /// `reachable()` is then asserted against the closure the child computed on
6768 /// the same tip, so it is the same answer and not merely a non-empty one.
6769 ///
6770 /// Seen RED by deleting `store.refold()?` from the end of
6771 /// `GitStore::open_with_arms`, so nothing folds the table on open: "the
6772 /// commit graph did not survive a clean shutdown: 0 of 551 commits — the
6773 /// exploded table is a warm cache, not a durable table", left 0 right 551.
6774 /// That is the recorded bug's own number, reproduced. Restored.
6775 ///
6776 /// Seen RED a second time by deleting `self.exploded.flush()?` from
6777 /// `Absorber::absorb_one`, which is the **durability** edit: the rows then
6778 /// live only in the un-flushed buffer, which answers perfectly inside the
6779 /// child and is gone when the process ends. It fires assertion (2) rather
6780 /// than (1) — "the reopen re-queued the pack, so the graph above is a
6781 /// recovery artefact rather than the table's" — and that is worth writing
6782 /// down, because assertion (1) *passed*: the 64 MiB flush threshold had
6783 /// tripped once mid-pack, leaving 1798 of 2687 rows on disk which happened
6784 /// to include all 551 commits. A guard with only assertion (1) would have
6785 /// called that edit green. Restored.
6786 /// **An annotated tag reaches the commit it points at.**
6787 ///
6788 /// # The bug this is the guard for
6789 ///
6790 /// Reachability bitmaps are built over the commit graph, and no tree points
6791 /// at a tag, so a tag is in nobody's closure. `reachable`'s `want` loop had
6792 /// one arm for "not a commit": insert the object itself, and stop. So a
6793 /// `want` that was an annotated tag answered with **one object** — the tag —
6794 /// and the commit it names, plus that commit's whole history, was left out.
6795 ///
6796 /// It stayed invisible because a tag is nearly always pushed alongside the
6797 /// branch that carries the same commit, and then the branch's closure
6798 /// covers it. It surfaces the moment a tag is the ONLY thing naming a
6799 /// commit — which is what gunnar's replication does, one ref at a time. Six
6800 /// repositories on the live fleet could not replicate on 2026-08-31, one of
6801 /// them with 25 tags hanging off a single commit no branch carried, all
6802 /// refused by the far end as `missing necessary objects … (peeling a tag)`.
6803 ///
6804 /// So the fixture asserts the premise it needs: the tag is the only `want`.
6805 ///
6806 /// # 🔴 Seen red
6807 ///
6808 /// Against the arm as it was (`union.insert(o)` and nothing more):
6809 /// `the tag's closure is 1 object, so it carries the tag and nothing it
6810 /// points at`. With the peel: the commit, its trees and its blobs.
6811 #[test]
6812 fn an_annotated_tag_reaches_the_commit_it_points_at() {
6813 let dir = tmpdir("tag-peel");
6814 let store = GitStore::open(&dir, "rickard").unwrap();
6815 let (pack, rows) = corpus_pack();
6816 store.put(&pack, &[]).unwrap();
6817 store.wait_indexed();
6818
6819 // Any commit will do: the tag is the only `want`, so nothing else can
6820 // pull this commit's closure in behind our backs.
6821 let commit = rows
6822 .iter()
6823 .find(|r| r.kind == GitObjectKind::Commit)
6824 .expect("the corpus has commits");
6825 let commit_hex = hex::encode(&commit.oid);
6826 let commit_closure = store.reachable(&[&commit.oid], &[]).unwrap();
6827 assert!(
6828 commit_closure.len() > 1,
6829 "the fixture's commit closes over nothing, so this test could not tell a peel from a no-op"
6830 );
6831
6832 let (tag_pack, tag_oid) = crate::store::tests::one_tag_pack(&commit_hex, "v1.0");
6833 store.put(&tag_pack, &[]).unwrap();
6834 store.wait_indexed();
6835
6836 let closure = store.reachable(&[&tag_oid], &[]).unwrap();
6837 assert!(
6838 closure.contains(&tag_oid),
6839 "the tag itself is not in its own closure"
6840 );
6841 assert!(
6842 closure.contains(&commit.oid),
6843 "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",
6844 closure.len(),
6845 &commit_hex[..12]
6846 );
6847 // Not merely the commit: everything the commit reaches, or a clone from
6848 // this answer is short by a tree.
6849 for oid in &commit_closure {
6850 assert!(
6851 closure.contains(oid),
6852 "the tag's closure is missing {} which the commit it points at reaches",
6853 hex::encode(oid)
6854 );
6855 }
6856 }
6857
6858 #[test]
6859 fn a_clean_shutdown_and_reopen_keeps_the_graph_and_reachable() {
6860 let dir = tmpdir("clean-shutdown-graph");
6861 let (pack, rows) = corpus_pack();
6862 std::fs::write(dir.join("fixture.pack"), &pack).unwrap();
6863
6864 let status = std::process::Command::new(std::env::current_exe().unwrap())
6865 .args([
6866 "--exact",
6867 "--ignored",
6868 "--nocapture",
6869 "git_ops::tests::the_child_that_pushes_and_shuts_down_cleanly",
6870 ])
6871 .env(CLEAN_DIR, &dir)
6872 .status()
6873 .expect("spawn the child that shuts down cleanly");
6874 assert!(
6875 status.success(),
6876 "the child did not exit cleanly — it {status:?}, so this is a crash guard and not a \
6877 clean-shutdown one"
6878 );
6879 assert!(
6880 dir.join("clean").exists(),
6881 "the child never reached its clean shutdown"
6882 );
6883
6884 let tip_hex = std::fs::read_to_string(dir.join("tip")).unwrap();
6885 let closure_before: usize = std::fs::read_to_string(dir.join("closure"))
6886 .unwrap()
6887 .parse()
6888 .unwrap();
6889 let commits_before: usize = std::fs::read_to_string(dir.join("commits"))
6890 .unwrap()
6891 .parse()
6892 .unwrap();
6893 assert_eq!(
6894 commits_before,
6895 commits_in(&rows),
6896 "the child's own graph was already wrong, so the reopen proves nothing"
6897 );
6898
6899 // ── the reopen, and (1): the graph before anything is waited on ──────
6900 let store = GitStore::open(&dir, "rickard").unwrap();
6901 assert_eq!(
6902 store.commit_count(),
6903 commits_in(&rows),
6904 "the commit graph did not survive a clean shutdown: {} of {} commits — the exploded \
6905 table is a warm cache, not a durable table",
6906 store.commit_count(),
6907 commits_in(&rows)
6908 );
6909 let tip_raw = hex::decode(&tip_hex).unwrap();
6910 let closure = store.reachable(&[&tip_raw], &[]).unwrap();
6911 assert_eq!(
6912 closure.len(),
6913 closure_before,
6914 "reachable() answers differently after a clean reopen: {} objects against the {} the \
6915 same tip closed over before the shutdown",
6916 closure.len(),
6917 closure_before
6918 );
6919 assert!(closure.contains(&tip_raw));
6920
6921 // ── (2): and none of it came from a re-absorb ────────────────────────
6922 store.wait_indexed();
6923 assert!(
6924 !store.indexer().is_indexed(0),
6925 "the reopen re-queued the pack, so the graph above is a recovery artefact rather \
6926 than the table's"
6927 );
6928 assert_eq!(store.unindexed_packs(), 0);
6929 assert_eq!(
6930 store.absorb_pending().unwrap(),
6931 0,
6932 "the reopen left index work"
6933 );
6934 assert_eq!(store.object_count(), rows.len());
6935 assert_eq!(
6936 store.exploded_stats().written,
6937 0,
6938 "this process wrote exploded rows, so the table was rebuilt rather than read"
6939 );
6940
6941 eprintln!(
6942 "load {}; clean shutdown → reopen: {} rows, {} commits, tip closure {} objects \
6943 (rebuilt nothing)",
6944 loadavg(),
6945 store.object_count(),
6946 store.commit_count(),
6947 closure.len(),
6948 );
6949 }
6950
6951 /// **A reopen READS `objects.graph`; it does not recompute it — and the
6952 /// read graph equals the recomputed one.** This is S-065's fix, and the only
6953 /// thing that can say which path a fold took is a counter, so the assertion
6954 /// is on [`GitStore::graph_source_stats`], not on the (identical) graph
6955 /// bytes — the identity-value trap, since both paths yield the same graph.
6956 ///
6957 /// One test exercises **both** arms so the counter cannot be blindly stuck:
6958 ///
6959 /// 1. A first store builds the graph, persists it, and shuts down cleanly.
6960 /// 2. `objects.graph` is deleted, so the reopen has no cache: it **recomputes**
6961 /// (`(read, recomputed) == (0, 1)`) and re-persists. Its graph and its
6962 /// `reachable()` are captured — this is the recomputed truth.
6963 /// 3. A second reopen finds the cache the step above wrote: it **reads**
6964 /// (`(1, 0)`), and its graph and `reachable()` must equal step 2's exactly
6965 /// — read == recomputed.
6966 ///
6967 /// # 🔴 Seen red
6968 ///
6969 /// With the persistence removed (open always folds), step 3 is `(0, 1)`:
6970 /// "the reopen recomputed the graph instead of reading objects.graph", left
6971 /// `(0, 1)` right `(1, 0)`. With the token check inverted so a stale cache is
6972 /// accepted, step 3's `reachable()` diverges from step 2's and the read==
6973 /// recomputed assertion fires. Restored.
6974 #[test]
6975 fn a_reopen_reads_the_persisted_graph_it_does_not_recompute() {
6976 let dir = tmpdir("persisted-graph");
6977 let (pack, rows) = corpus_pack();
6978 let tip_hex;
6979 {
6980 let store = GitStore::open(&dir, "rickard").unwrap();
6981 store.put_pack(&pack).unwrap();
6982 store.wait_indexed();
6983 tip_hex = store
6984 .graph_snapshot()
6985 .into_iter()
6986 .max_by_key(|c| c.generation)
6987 .expect("a graph")
6988 .oid;
6989 assert!(
6990 dir.join("objects.graph").exists(),
6991 "an absorb never persisted the graph, so a reopen has nothing to read"
6992 );
6993 }
6994 let tip_raw = hex::decode(&tip_hex).unwrap();
6995
6996 // ── (2) no cache → recompute, and capture the recomputed truth ───────
6997 std::fs::remove_file(dir.join("objects.graph")).unwrap();
6998 let (recomputed_graph, recomputed_closure) = {
6999 let store = GitStore::open(&dir, "rickard").unwrap();
7000 assert_eq!(
7001 store.graph_source_stats(),
7002 (0, 1),
7003 "with no objects.graph the reopen should have recomputed the graph"
7004 );
7005 assert_eq!(store.commit_count(), commits_in(&rows));
7006 let g = store.graph_snapshot();
7007 let c = store.reachable(&[&tip_raw], &[]).unwrap();
7008 assert!(!c.is_empty());
7009 (g, c)
7010 };
7011
7012 // ── (3) cache present → read, and it must equal the recomputed truth ──
7013 let store = GitStore::open(&dir, "rickard").unwrap();
7014 assert_eq!(
7015 store.graph_source_stats(),
7016 (1, 0),
7017 "the reopen recomputed the graph instead of reading objects.graph"
7018 );
7019 assert_eq!(
7020 store.graph_snapshot(),
7021 recomputed_graph,
7022 "the graph READ from objects.graph differs from the one refold RECOMPUTES"
7023 );
7024 assert_eq!(
7025 store.reachable(&[&tip_raw], &[]).unwrap(),
7026 recomputed_closure,
7027 "reachable() over the persisted graph differs from reachable() over the recomputed one"
7028 );
7029 assert_eq!(store.commit_count(), commits_in(&rows));
7030 }
7031
7032 /// **The token gates the read: a cache whose token does not match the
7033 /// object set is rejected and the graph is recomputed.** This is what makes
7034 /// `objects.graph` a memo rather than a second truth — and a push, a gc, or
7035 /// a torn write all reach the store through exactly this door.
7036 ///
7037 /// The object set is left unchanged and the token in the cache is corrupted
7038 /// directly, so the ONE thing under test is the token comparison and not
7039 /// some incidental object-count difference.
7040 ///
7041 /// # 🔴 Seen red
7042 ///
7043 /// With the token comparison dropped (`Some((_, g)) => g`), the corrupted
7044 /// cache is read and the reopen is `(1, 0)`: "a cache with the wrong token
7045 /// was trusted". Restored.
7046 #[test]
7047 fn a_graph_whose_token_does_not_match_is_rejected() {
7048 let dir = tmpdir("wrong-token-graph");
7049 let (pack, _rows) = corpus_pack();
7050 {
7051 let store = GitStore::open(&dir, "rickard").unwrap();
7052 store.put_pack(&pack).unwrap();
7053 store.wait_indexed();
7054 }
7055 // Flip a byte inside the 32-byte token (offset 5..37) without touching
7056 // the graph body, so only the token is wrong.
7057 let path = dir.join("objects.graph");
7058 let mut bytes = std::fs::read(&path).unwrap();
7059 assert!(bytes.len() > 37 && &bytes[..4] == b"GRPH");
7060 bytes[6] ^= 0xff;
7061 std::fs::write(&path, &bytes).unwrap();
7062
7063 let store = GitStore::open(&dir, "rickard").unwrap();
7064 assert_eq!(
7065 store.graph_source_stats(),
7066 (0, 1),
7067 "a cache with the wrong token was trusted instead of being recomputed"
7068 );
7069 }
7070
7071 /// **The ordinal space comes off the persisted index, not off a scan — and
7072 /// no `objects.ordinal` is ever written.** This replaces S-082's memo test:
7073 /// the memo spared a cold open the redb scan that rebuilt the sorted oid
7074 /// list, and since 2026-09-04 that list IS the persisted index's oid column,
7075 /// so there is nothing to spare and no file to validate. What is asserted:
7076 ///
7077 /// 1. A first store absorbs the corpus; its ordinal space equals the tail in
7078 /// oid order and every oid is accounted to one of the two sources. Then
7079 /// it folds explicitly, so redb is empty and the index holds everything.
7080 /// 2. The reopen is warm: its space comes **entirely off the index** —
7081 /// `(folded, tail) == (n, 0)` — and equals step 1's space, closure and
7082 /// counts exactly. `objects.ordinal` does not exist after either session.
7083 ///
7084 /// # 🔴 Seen red
7085 ///
7086 /// With `ObjectReadStack::oids_raw` returning only the redb tail on a
7087 /// persisting stack, step 2's space is empty (redb holds nothing after the
7088 /// fold) and `reachable()` over it is empty: "the ordinal space after the
7089 /// warm reopen is not the tail in oid order". Restored.
7090 #[test]
7091 fn the_ordinal_space_comes_off_the_index_not_a_scan() {
7092 let dir = tmpdir("ordinal-off-the-index");
7093 let (pack, rows) = corpus_pack();
7094 let tip_hex;
7095 let (first_oids, first_closure, first_objects, first_commits);
7096 {
7097 let store = GitStore::open(&dir, "rickard").unwrap();
7098 store.put_pack(&pack).unwrap();
7099 store.wait_indexed();
7100 tip_hex = store
7101 .graph_snapshot()
7102 .into_iter()
7103 .max_by_key(|c| c.generation)
7104 .expect("a graph")
7105 .oid;
7106 let tip_raw = hex::decode(&tip_hex).unwrap();
7107 let n = store.object_count() as u64;
7108 let (folded, tail) = store.ordinal_source_stats();
7109 assert_eq!(folded + tail, n, "every oid is accounted to the index or the tail");
7110 let oids = store.ordinal_oids();
7111 let scanned: Vec<String> =
7112 store.index().oids_in_order().unwrap().iter().map(hex::encode).collect();
7113 assert_eq!(oids, scanned, "the ordinal space is not the repository in oid order");
7114 assert!(
7115 !dir.join("objects.ordinal").exists(),
7116 "no memo may be written: the index's oid column is the ordinal space"
7117 );
7118 // Fold, so the reopen has everything on the index and nothing in redb.
7119 store.index().rebuild().unwrap();
7120 first_closure = store.reachable(&[&tip_raw], &[]).unwrap();
7121 assert!(!first_closure.is_empty());
7122 first_oids = oids;
7123 first_objects = store.object_count();
7124 first_commits = store.commit_count();
7125 }
7126 let tip_raw = hex::decode(&tip_hex).unwrap();
7127
7128 let store = GitStore::open(&dir, "rickard").unwrap();
7129 let n = store.object_count() as u64;
7130 assert_eq!(
7131 store.ordinal_source_stats(),
7132 (n, 0),
7133 "after a fold the warm reopen's ordinal space must come entirely off the index"
7134 );
7135 assert_eq!(
7136 store.ordinal_oids(),
7137 first_oids,
7138 "the ordinal space after the warm reopen is not the tail in oid order"
7139 );
7140 assert_eq!(
7141 store.reachable(&[&tip_raw], &[]).unwrap(),
7142 first_closure,
7143 "reachable() over the index's ordinal space differs from the first session's"
7144 );
7145 assert_eq!(store.object_count(), first_objects);
7146 assert_eq!(store.commit_count(), first_commits);
7147 assert_eq!(store.commit_count(), commits_in(&rows));
7148 assert!(!dir.join("objects.ordinal").exists(), "still no memo after the reopen");
7149 }
7150
7151 /// **A stale `objects.ordinal` left by an older binary is removed at open.**
7152 /// The file is dead weight now (279 MB on the kernel mirror) and its
7153 /// contents are never read, so a store that carries one loses it the first
7154 /// time this build opens it — and keeps working.
7155 ///
7156 /// # 🔴 Seen red
7157 ///
7158 /// With the `remove_file` in `open_with_arms` dropped, the file survives the
7159 /// open: "a stale objects.ordinal must be removed at open". Restored.
7160 #[test]
7161 fn a_stale_objects_ordinal_from_an_older_binary_is_removed_at_open() {
7162 let dir = tmpdir("stale-ordinal");
7163 let (pack, _rows) = corpus_pack();
7164 {
7165 let store = GitStore::open(&dir, "rickard").unwrap();
7166 store.put_pack(&pack).unwrap();
7167 store.wait_indexed();
7168 }
7169 // The old memo's shape: magic, version, oid_len, epoch, count, oids.
7170 let path = dir.join("objects.ordinal");
7171 let mut bytes = b"ORDL".to_vec();
7172 bytes.push(1);
7173 bytes.push(20);
7174 bytes.extend_from_slice(&7u64.to_le_bytes());
7175 bytes.extend_from_slice(&1u64.to_le_bytes());
7176 bytes.extend_from_slice(&[0xabu8; 20]);
7177 std::fs::write(&path, &bytes).unwrap();
7178 assert!(path.exists(), "precondition");
7179
7180 let store = GitStore::open(&dir, "rickard").unwrap();
7181 assert!(!path.exists(), "a stale objects.ordinal must be removed at open");
7182 assert!(store.object_count() > 0, "and the store still answers");
7183 }
7184
7185 /// **A content read is served from the table, not re-derived** — and the only
7186 /// thing that can say so is a counter.
7187 ///
7188 /// Both paths return the **identical** bytes: the table hands back what the
7189 /// resolver produced, and the fallback re-resolves the same pack and produces
7190 /// it again. A byte comparison therefore proves nothing at all about which
7191 /// one ran — this is the identity-value trap in its exact form, and it fired
7192 /// on this codebase once already. So the assertion is on
7193 /// [`ExplodedStats::served`] against [`ExplodedStats::rederived`], both of
7194 /// which are bumped where the work happens.
7195 ///
7196 /// The fallback is not asserted away: it is exercised in the same test, on a
7197 /// store whose table has been dropped, and the counters move the other way.
7198 /// A guard that only saw the fast path could not tell a working counter from
7199 /// one that is never incremented.
7200 ///
7201 /// Seen RED by deleting the `if let Some(hit) = self.exploded.content(oid)?`
7202 /// early return in `Absorber::resolved`, so every read re-derives: "512 content
7203 /// reads re-resolved a whole pack instead of hitting the table — served 0,
7204 /// rederived 512". The bytes were still correct on every one of them, which
7205 /// is the point. Restored.
7206 ///
7207 /// Seen RED a second time by bumping `served` unconditionally at the top of
7208 /// `ExplodedTable::content`, before the `pending`/redb probe — the counter
7209 /// then reports a hit whether or not the table had the object. The first half
7210 /// fires on the double count, "512 content reads re-resolved a whole pack
7211 /// instead of hitting the table — served 1024, rederived 0", and
7212 /// `exploded::tests::a_row_round_trips_and_the_kind_index_finds_it` fires with
7213 /// it ("a content read was not counted", left 5 right 2). Restored — a
7214 /// counter that cannot be wrong about a miss is the only kind worth
7215 /// asserting on.
7216 #[test]
7217 fn a_content_read_is_served_from_the_table_and_not_re_derived() {
7218 const READS: usize = 512;
7219 let dir = tmpdir("exploded-served");
7220 let (pack, rows) = corpus_pack();
7221 let oids: Vec<Vec<u8>> = rows
7222 .iter()
7223 .map(|r| r.oid.clone())
7224 .collect::<HashSet<Vec<u8>>>()
7225 .into_iter()
7226 .take(READS)
7227 .collect();
7228
7229 {
7230 let store = GitStore::open(&dir, "rickard").unwrap();
7231 store.put_pack(&pack).unwrap();
7232 store.wait_indexed();
7233
7234 // ── FIRST read: the lazy path, timed ────────────────────────
7235 let before = store.exploded_stats();
7236 let t = Instant::now();
7237 let mut first: Vec<(GitObjectKind, Vec<u8>)> = Vec::with_capacity(oids.len());
7238 for oid in &oids {
7239 first.push(
7240 store
7241 .content(oid)
7242 .unwrap()
7243 .expect("the store lost an object"),
7244 );
7245 }
7246 let cold = t.elapsed();
7247 let after = store.exploded_stats();
7248 let rederived = after.rederived - before.rederived;
7249 assert_eq!(
7250 rederived + (after.served - before.served),
7251 oids.len() as u64,
7252 "{} reads = served {} + rederived {} must account for every one",
7253 oids.len(),
7254 after.served - before.served,
7255 rederived
7256 );
7257 assert!(
7258 rederived > 0,
7259 "not one read re-derived: either every object is eager or the lazy \
7260 path never ran, and this test then proves nothing about either"
7261 );
7262
7263 // The write-back is off the read path, so it has to be waited for
7264 // HERE and only here — a reader never does.
7265 store.explode_cache_maintain().unwrap();
7266 // **How many of those reads the cache DECLINED.** A shallow, small
7267 // object is cheaper to inflate again than to keep, so it is read
7268 // directly and nothing is written
7269 // (`exploded_cache::worth_caching`). Those objects are re-derived
7270 // on every read for ever, on purpose, and the second pass below
7271 // has to expect exactly that many — not zero, which is what this
7272 // test asserted before the admission policy existed and would now
7273 // be asserting that the policy is off.
7274 let declined = store.explode_cache_use().direct_reads;
7275 assert!(
7276 declined > 0,
7277 "not one of {} reads was declined as cheap; either every object in \
7278 this corpus is expensive or the admission policy is not running",
7279 oids.len()
7280 );
7281
7282 // ── SECOND read: the design note's "= today" column ──────────
7283 let mid = store.exploded_stats();
7284 let t = Instant::now();
7285 for (i, oid) in oids.iter().enumerate() {
7286 let again = store.content(oid).unwrap().expect("the store lost an object");
7287 assert_eq!(again, first[i], "the second read of an object disagreed with the first");
7288 }
7289 let warm = t.elapsed();
7290 let end = store.exploded_stats();
7291 assert_eq!(
7292 end.rederived - mid.rederived,
7293 declined,
7294 "the second pass re-derived {} objects and the cache declined {declined} — \
7295 every EXPENSIVE object must come out of the table the second time, and \
7296 every cheap one must be read directly again",
7297 end.rederived - mid.rederived
7298 );
7299 assert_eq!(
7300 end.served - mid.served,
7301 oids.len() as u64 - declined,
7302 "the expensive objects did not come out of the table on the second pass"
7303 );
7304 eprintln!(
7305 "load {}; {} content reads: FIRST {:.1} ms ({:.1} µs/read, {} re-derived from \
7306 the pack, {declined} of them too cheap to cache) → SECOND {:.1} ms \
7307 ({:.1} µs/read) = {:.1}x",
7308 loadavg(),
7309 oids.len(),
7310 cold.as_secs_f64() * 1e3,
7311 cold.as_secs_f64() * 1e6 / oids.len() as f64,
7312 rederived,
7313 warm.as_secs_f64() * 1e3,
7314 warm.as_secs_f64() * 1e6 / oids.len() as f64,
7315 cold.as_secs_f64() / warm.as_secs_f64().max(f64::MIN_POSITIVE),
7316 );
7317 }
7318
7319 // ── and the fallback, so the counter is not a constant ───────────────
7320 //
7321 // The table is dropped and the *drain* is what would rebuild it, so the
7322 // reads have to happen before it gets there. `absorb_pending` is called
7323 // by hand first so the fallback under test is the content path's and not
7324 // the index's.
7325 std::fs::remove_file(dir.join("objects.exploded")).unwrap();
7326 // **And the lazy pool with it.** Dropping only the eager table leaves
7327 // the write-backs of the pass above sitting in `objects.exploded.cN`,
7328 // and those would answer — the fallback would then be measured on a
7329 // store that still has a cache, which is the exact shape of a test that
7330 // cannot fail. One `drop_explode_cache` is the sanctioned door, and it
7331 // is what `repo gc` calls.
7332 for e in std::fs::read_dir(&dir).unwrap().flatten() {
7333 if e.file_name().to_string_lossy().starts_with("objects.exploded.c") {
7334 std::fs::remove_file(e.path()).unwrap();
7335 }
7336 }
7337 let store = GitStore::open(&dir, "rickard").unwrap();
7338 let gate = store.hold_absorb_gate();
7339 let before = store.exploded_stats();
7340 let mut rederived = 0u64;
7341 for oid in oids.iter().take(8) {
7342 // Straight at the absorber: the store's `content` would absorb the
7343 // re-queued pack first and rebuild the very table this is testing
7344 // the absence of.
7345 assert!(
7346 store.absorber.resolved(oid).unwrap().is_some(),
7347 "the verbatim truth could not re-derive {}",
7348 hex::encode(oid)
7349 );
7350 rederived += 1;
7351 }
7352 let after = store.exploded_stats();
7353 // The drain starts the instant the store opens; on a loaded box it can
7354 // rebuild the small corpus's table before the gate above is taken, and
7355 // then the reads are legitimately served from it. Seen 2026-08-30 under
7356 // a parallel suite. That run proves nothing about the fallback and says
7357 // so, rather than failing on a race the test does not control.
7358 let drain_won = store.unindexed_packs() == 0;
7359 drop(gate);
7360 if drain_won {
7361 eprintln!("the drain rebuilt the table before the gate was held; the fallback was not observed this run");
7362 return;
7363 }
7364 assert_eq!(
7365 after.served - before.served,
7366 0,
7367 "a dropped table still reported {} reads served",
7368 after.served - before.served
7369 );
7370 assert_eq!(
7371 after.rederived - before.rederived,
7372 rederived,
7373 "the fallback ran but was not counted"
7374 );
7375 }
7376
7377 /// ★ **THE READ-AFTER-ACK GAP (lane U → lane Q): every read path answers
7378 /// with the indexer PAUSED, and none of them waits for it.**
7379 ///
7380 /// MEASURED on the rig 2026-08-30 (lane U, both ISOs, timing-dependent):
7381 /// right after an ACKED push a `clone` came back
7382 /// `remote error: object … is not in this repository (peeling a tag)` and a
7383 /// `push --mirror` on top got `[remote rejected] … missing necessary
7384 /// objects`. Both of those are gunnar's `Graph::load` — `GitServe::read` —
7385 /// answering `None` for an object whose bytes were fsynced before the client
7386 /// was told the push had landed. §13.12 says an index miss falls back to the
7387 /// journal; the tag-peel and connectivity-check paths were not observed to.
7388 ///
7389 /// So this holds the absorb gate — the indexer paused, exactly the state
7390 /// lane P's watchdog puts it in — and requires **`has`, `header` and `read`
7391 /// to answer for every object of a just-acked pack**, byte-correct, and to
7392 /// do it inside a deadline. The deadline is not decoration: "nobody waits"
7393 /// (the design note's rule) is violated by a read that blocks on the gate
7394 /// just as badly as by one that answers absent, and a blocking read looks
7395 /// exactly like a correct one to an assertion that has no clock.
7396 ///
7397 /// Seen RED by returning `Ok(None)` from `Absorber::resolved`'s pending arm:
7398 /// 1352 of 1352 objects report "the store said absent for an object it had
7399 /// already acked".
7400 #[test]
7401 fn every_read_path_answers_a_just_acked_push_with_the_indexer_paused() {
7402 use crate::serve::GitServe as _;
7403 let dir = tmpdir("read-after-ack");
7404 let store = Arc::new(GitStore::open(&dir, "rickard").unwrap());
7405 let (pack, rows) = corpus_pack();
7406
7407 // The ack: `put_pack` returning is what the client is told.
7408 store.put_pack(&pack).unwrap();
7409
7410 // The indexer, paused. A watchdog pause (lane P) pauses the indexer and
7411 // nothing else, so this is the shape the appliance is really in.
7412 let gate = store.hold_absorb_gate();
7413 assert!(
7414 store.unindexed_packs() > 0,
7415 "the drain absorbed the pack before the gate was taken, so this run \
7416 read an INDEXED store and proves nothing about the window"
7417 );
7418
7419 let distinct: Vec<Vec<u8>> = rows
7420 .iter()
7421 .map(|r| r.oid.clone())
7422 .collect::<HashSet<Vec<u8>>>()
7423 .into_iter()
7424 .collect();
7425 let hash = store.hash_kind();
7426 let n = distinct.len();
7427
7428 // On another thread, so the deadline measures the reads and not this
7429 // test's own patience.
7430 let (tx, rx) = std::sync::mpsc::channel::<Result<(usize, usize, usize)>>();
7431 let reader = {
7432 let store = store.clone();
7433 let distinct = distinct.clone();
7434 std::thread::spawn(move || {
7435 let mut had = 0usize;
7436 let mut headed = 0usize;
7437 let mut readback = 0usize;
7438 let r = (|| -> Result<()> {
7439 for oid in &distinct {
7440 if store.has(oid)? {
7441 had += 1;
7442 }
7443 if crate::serve::GitServe::header(&*store, oid)?.is_some() {
7444 headed += 1;
7445 }
7446 // The tag-peel / connectivity-check path, byte for byte.
7447 let Some((kind, payload)) = crate::serve::GitServe::read(&*store, oid)? else {
7448 continue;
7449 };
7450 let k = match kind {
7451 crate::index_layout::ObjType::Commit => GitObjectKind::Commit,
7452 crate::index_layout::ObjType::Tree => GitObjectKind::Tree,
7453 crate::index_layout::ObjType::Blob => GitObjectKind::Blob,
7454 crate::index_layout::ObjType::Tag => GitObjectKind::Tag,
7455 // A read resolves the chain, so a delta type coming
7456 // back out of it is the store contradicting itself.
7457 other => anyhow::bail!("`read` answered with a {other:?}"),
7458 };
7459 let rehashed = hash.oid_of(&crate::object::canonical(k, &payload));
7460 if &rehashed == oid {
7461 readback += 1;
7462 }
7463 }
7464 Ok(())
7465 })();
7466 let _ = tx.send(r.map(|_| (had, headed, readback)));
7467 })
7468 };
7469
7470 // The deadline. Generous — a cold pending scan of the corpus is
7471 // milliseconds — and it is against a WALL clock while the gate is held,
7472 // so a read that parks on the absorber trips it.
7473 let out = rx
7474 .recv_timeout(Duration::from_secs(20))
7475 .expect(
7476 "a read path blocked on the paused indexer for 20 s: 'nobody waits' is the \
7477 design note's rule and a read that waits for the drain is a bug by definition",
7478 );
7479 let (had, headed, readback) = out.expect("a read path errored inside the index window");
7480 drop(gate);
7481 let _ = reader.join();
7482
7483 assert_eq!(
7484 (had, headed, readback),
7485 (n, n, n),
7486 "the store answered for {had}/{n} `has`, {headed}/{n} `header` and {readback}/{n} \
7487 byte-correct `read`s of a pack it had already ACKED — an object the client was \
7488 told had landed must never read as absent"
7489 );
7490 }
7491
7492 /// ★ **An account at its explode budget serves EVERY object, correctly —
7493 /// only slower.**
7494 ///
7495 /// The quota is a cache size, not a storage limit: over budget the server
7496 /// EVICTS, it does not refuse. So a repository squeezed to a budget far
7497 /// under what it holds must still answer every read byte-for-byte, and the
7498 /// only thing that may change is where the bytes came from — the table, or
7499 /// the pack via [`crate::resolve::materialize_at`].
7500 ///
7501 /// This is requirement (c) of the quota: *"an account at its budget must
7502 /// still serve every read correctly, only slower"*. The `rederived`
7503 /// counter is what proves the eviction really happened, because identical
7504 /// bytes come back either way and a byte comparison cannot tell a working
7505 /// cache from a dead one — the identity-value trap in its exact form.
7506 ///
7507 /// Seen RED by having `write_back` return the payload's bytes from a
7508 /// half-written segment (`out.truncate(1)`): "the store returned the wrong
7509 /// bytes for an object under a squeezed budget".
7510 #[test]
7511 fn an_account_at_its_budget_serves_every_object_only_slower() {
7512 let dir = tmpdir("explode-budget");
7513 let store = GitStore::open(&dir, "rickard").unwrap();
7514 let (pack, rows) = corpus_pack();
7515 store.put_pack(&pack).unwrap();
7516 store.wait_indexed();
7517
7518 let oids: Vec<Vec<u8>> = rows
7519 .iter()
7520 .map(|r| r.oid.clone())
7521 .collect::<HashSet<Vec<u8>>>()
7522 .into_iter()
7523 .collect();
7524
7525 // Every object once, unsqueezed: the truth to compare against.
7526 let mut truth: Vec<(GitObjectKind, Vec<u8>)> = Vec::with_capacity(oids.len());
7527 for oid in &oids {
7528 truth.push(store.content(oid).unwrap().expect("the store lost an object"));
7529 }
7530 store.explode_cache_maintain().unwrap();
7531
7532 // Now the plan changes: a budget far under what this repository holds.
7533 store.set_explode_budget(64 << 10);
7534 let before = store.exploded_stats();
7535 for _ in 0..3 {
7536 for (i, oid) in oids.iter().enumerate() {
7537 let got = store
7538 .content(oid)
7539 .unwrap()
7540 .expect("an object went absent when the budget was lowered");
7541 assert_eq!(
7542 got, truth[i],
7543 "the store returned the wrong bytes for an object under a squeezed budget"
7544 );
7545 }
7546 store.explode_cache_maintain().unwrap();
7547 }
7548 let after = store.exploded_stats();
7549 let band = store.explode_cache_use();
7550
7551 assert!(
7552 band.evicted_bytes > 0 || band.lazy_bytes <= 64 << 10,
7553 "the budget was lowered to 64 KiB and the lazy pool holds {} B with nothing \\
7554 evicted — the quota is not enforced, so this run proves nothing about reading \\
7555 under one: {band:?}",
7556 band.lazy_bytes
7557 );
7558 assert!(
7559 after.rederived > before.rederived,
7560 "not one read re-derived from the pack under a 64 KiB budget, so the cache was \\
7561 never squeezed and 'only slower' was never tested"
7562 );
7563 eprintln!(
7564 "load {}; {} objects read 3x under a 64 KiB budget: {} re-derived, {} served, \
7565{} evicted bytes over {} segments; every byte identical",
7566 loadavg(),
7567 oids.len(),
7568 after.rederived - before.rederived,
7569 after.served - before.served,
7570 band.evicted_bytes,
7571 band.evicted_segments,
7572 );
7573 }
7574
7575 /// ★ **A THIN push on top of a just-acked one is accepted with the indexer
7576 /// PAUSED** — lane U's second rig symptom.
7577 ///
7578 /// MEASURED on the rig 2026-08-30 (lane U): right after an acked push,
7579 /// `git push --mirror` of the next pack came back
7580 /// `[remote rejected] … missing necessary objects`, on a varying set of
7581 /// repositories run to run — a race, not a repository. A thin pack names
7582 /// its delta bases by oid and expects the server to have them; the server
7583 /// **did** have them, in a pack it had acked and not yet indexed, and
7584 /// `scan_pending`'s re-entrancy guard answered `None` for every base looked
7585 /// up from inside a scan rather than only for the ones that would have
7586 /// started a second scan.
7587 ///
7588 /// Seen RED by restoring that guard (`if nested { return Ok((out, None)) }`):
7589 /// *"reading the thin pack's object errored inside the index window:
7590 /// resolving a pending pack read-only for lookups — this pack is thin: it
7591 /// deltas against 42088154… which is not in it, and no base source could
7592 /// produce that object's content"*. The **push** is accepted either way —
7593 /// `external_bases_exist` finds the base with a top-level `lookup_one`
7594 /// before it ever resolves — so the assertion that discriminates is the
7595 /// READ with the gate still held, and an earlier draft of this test that
7596 /// dropped the gate first passed under both arms.
7597 #[test]
7598 fn a_thin_push_on_top_of_an_unindexed_one_is_accepted() {
7599 let dir = tmpdir("thin-on-unindexed");
7600 let store = GitStore::open(&dir, "rickard").unwrap();
7601 let (pack, rows) = corpus_pack();
7602 store.put_pack(&pack).unwrap();
7603
7604 // The indexer, paused — so the base can only be found in the pack that
7605 // was acked a moment ago.
7606 let gate = store.hold_absorb_gate();
7607 assert!(
7608 store.unindexed_packs() > 0,
7609 "the drain absorbed the first pack before the gate was taken; this run \
7610 pushed onto an INDEXED store and proves nothing"
7611 );
7612
7613 // A base with a payload, and its real size: a `REF_DELTA` declares the
7614 // source size and the resolver checks it, so a made-up number would fail
7615 // for a reason that has nothing to do with the window under test.
7616 let base = rows
7617 .iter()
7618 .find(|r| r.kind == GitObjectKind::Blob && r.uncompressed_size > 0)
7619 .expect("the corpus has a blob");
7620 let thin = crate::pack_fixture::thin_ref_delta_pack(
7621 &base.oid,
7622 base.uncompressed_size,
7623 b"lane Q was here\n",
7624 );
7625
7626 let tx = store.put_pack(&thin).unwrap_or_else(|e| {
7627 panic!("the thin push was refused: {e:#}");
7628 });
7629 assert!(tx.pack_id.is_some(), "the thin pack was not stored");
7630
7631 // **And its object reads back with the gate STILL HELD.** This is the
7632 // nested case and the one the guard was too wide for: the read scans
7633 // pack B, B's entry is a ref-delta, its base is in pack A, and
7634 // resolving it is a second `scan_pending` from inside the first. A
7635 // lookup into A's already-built scan terminates; only a *build* would
7636 // not.
7637 let hash = store.hash_kind();
7638 let oid = hash.oid_of(&crate::object::canonical(
7639 GitObjectKind::Blob,
7640 b"lane Q was here\n",
7641 ));
7642 let got = store.content(&oid);
7643 drop(gate);
7644 let (kind, payload) = got
7645 .expect("reading the thin pack's object errored inside the index window")
7646 .expect("the thin pack's own object is absent after its push was acked");
7647 assert_eq!(kind, GitObjectKind::Blob);
7648 assert_eq!(payload, b"lane Q was here\n");
7649 }
7650
7651 /// **Dropping the table and reopening still answers correctly — fall back,
7652 /// then rebuild.**
7653 ///
7654 /// §14 calls the resolved table *droppable*: derived, verifiable against the
7655 /// verbatim truth, and deletable at any time without consulting a client.
7656 /// This is that sentence as an operation — `rm objects.exploded` between two
7657 /// opens — and §13.12's rule applied to it unchanged: **absent means fall
7658 /// back, never means wrong.**
7659 ///
7660 /// Three things are asserted, in this order:
7661 ///
7662 /// 1. the file really is gone and the reopened store really does start with
7663 /// an empty table — otherwise the rest is testing a warm one;
7664 /// 2. **`reachable()` is already correct**, asked before any drain has been
7665 /// waited on: the fall-back absorb runs inline and rebuilds what the
7666 /// answer needs;
7667 /// 3. the table is **rebuilt** — one row per object again, the graph back,
7668 /// and the rows written by *this* process, which is what says a rebuild
7669 /// happened rather than a survival.
7670 ///
7671 /// Seen RED by `let exploded_is_whole = self.exploded.rows()? >= self.objects.len()
7672 /// as u64;` → `= true;` in `Absorber::adopt_journal`, so a dropped table is
7673 /// never noticed and nothing is re-queued. Assertion (2) fires first:
7674 /// **"reachable() after dropping the table: 1 objects, against 2683
7675 /// before"** — one object, the tip alone, which is the recorded bug's exact
7676 /// symptom ("`reachable` on a commit returns that commit alone rather than
7677 /// its closure") arrived at from the other direction. Restored.
7678 ///
7679 /// Seen RED a second time by `>=` → `>` in the same expression, which makes a
7680 /// *whole* table look short. This guard stays green — the rebuild is
7681 /// correct — and two others fire instead:
7682 /// [`a_clean_shutdown_and_reopen_keeps_the_graph_and_reachable`]'s "the
7683 /// reopen re-queued the pack, so the graph above is a recovery artefact
7684 /// rather than the table's", and the pre-existing
7685 /// [`a_clean_reopen_requeues_nothing_and_the_next_push_gets_a_fresh_ordinal`]'s
7686 /// "a fully absorbed pack was re-queued on open — the diff is not exact".
7687 /// That pair is the point: one guard catches the table not being rebuilt when
7688 /// it should be, the others catch it being rebuilt when it should not.
7689 /// Restored.
7690 #[test]
7691 fn dropping_the_exploded_table_and_reopening_still_answers() {
7692 let dir = tmpdir("exploded-dropped");
7693 let (pack, rows) = corpus_pack();
7694 let tip_hex;
7695 let closure_before;
7696 {
7697 let store = GitStore::open(&dir, "rickard").unwrap();
7698 store.put_pack(&pack).unwrap();
7699 store.wait_indexed();
7700 let tip = store
7701 .graph_snapshot()
7702 .into_iter()
7703 .max_by_key(|c| c.generation)
7704 .expect("a graph");
7705 let tip_raw = hex::decode(&tip.oid).unwrap();
7706 closure_before = store.reachable(&[&tip_raw], &[]).unwrap().len();
7707 tip_hex = tip.oid;
7708 }
7709
7710 // (1) — the drop.
7711 let table = dir.join("objects.exploded");
7712 assert!(table.exists(), "the store never built an exploded table");
7713 std::fs::remove_file(&table).unwrap();
7714 {
7715 let fresh = crate::exploded_arrow::ExplodedArchive::open(&table).unwrap();
7716 assert_eq!(fresh.rows().unwrap(), 0, "the drop did not drop anything");
7717 }
7718 // Opening an absent table does not create one — so this only has
7719 // anything to remove if the assertion above was inspecting a live file.
7720 let _ = std::fs::remove_file(&table);
7721
7722 // (2) — a correct answer with the table gone.
7723 let store = GitStore::open(&dir, "rickard").unwrap();
7724 let tip_raw = hex::decode(&tip_hex).unwrap();
7725 let closure = store.reachable(&[&tip_raw], &[]).unwrap();
7726 assert_eq!(
7727 closure.len(),
7728 closure_before,
7729 "reachable() after dropping the table: {} objects, against {} before",
7730 closure.len(),
7731 closure_before
7732 );
7733 assert!(store.has(&rows[0].oid).unwrap());
7734
7735 // (3) — and it is back. The GRAPH is what comes back at push time; a
7736 // blob comes back when somebody reads it, which is the whole of lazy
7737 // explode, so the count to compare against is the graph's and not the
7738 // pack's.
7739 store.wait_indexed();
7740 let stats = store.exploded_stats();
7741 let distinct: HashSet<Vec<u8>> = rows.iter().map(|r| r.oid.clone()).collect();
7742 // Commits, and NOT `of_kind(Tree)`: under the shipped policy a tree is
7743 // lazy, so the trees this test's own reads cached are in the lazy pool
7744 // and `of_kind` unions both.
7745 let graph_rows = store.exploded_of_kind(GitObjectKind::Commit).unwrap().len();
7746 // The EAGER pool, not `stats.rows`: step (2) above read objects through
7747 // the store, and every one of those reads wrote its object back into the
7748 // lazy pool, which `stats.rows` also counts. Comparing the sum against
7749 // the graph would be comparing "what the push wrote" against "what the
7750 // push wrote plus what this test happened to read".
7751 let eager = store.exploded_eager_rows().unwrap();
7752 assert_eq!(
7753 eager, graph_rows as u64,
7754 "the dropped table came back holding {eager} rows against a {}-object graph",
7755 graph_rows
7756 );
7757 assert!(
7758 eager > 0 && eager < distinct.len() as u64,
7759 "the rebuilt pool holds {eager} of {} objects — neither 0 nor all of them is \
7760 the lane Q shape",
7761 distinct.len()
7762 );
7763 assert!(
7764 stats.written > 0,
7765 "the table came back without this process writing a row — it was never dropped"
7766 );
7767 assert_eq!(
7768 store.commit_count(),
7769 commits_in(&rows),
7770 "the graph did not come back with the table: {} of {}",
7771 store.commit_count(),
7772 commits_in(&rows)
7773 );
7774 assert_eq!(
7775 store.object_count(),
7776 rows.len(),
7777 "the rebuild lost index rows"
7778 );
7779 eprintln!(
7780 "load {}; dropped and rebuilt: {} rows written, {} commits, tip closure {} objects",
7781 loadavg(),
7782 stats.written,
7783 store.commit_count(),
7784 closure.len(),
7785 );
7786 }
7787
7788 // ─────────────────────────────────────────────────────────────────────
7789 // lane ENGINE-OPT scaffold: a graded linear-history generator, the O(C)
7790 // negotiation timing test, and the cross-push memo-invalidation guard.
7791 // ─────────────────────────────────────────────────────────────────────
7792
7793 /// A linear commit history of `n` commits as a clone pack, optionally
7794 /// chained onto `start_parent` (an oid already in the store). Each commit
7795 /// carries its own one-entry tree and one unique blob, so the object count
7796 /// is `3n` and the graph is a single parent chain `n` deep — the shape that
7797 /// makes a negotiation's cost O(commits), which is what this lane memoizes.
7798 fn linear_history_pack_from(
7799 n: usize,
7800 start_parent: Option<&[u8]>,
7801 ) -> (Vec<u8>, Vec<Vec<u8>>) {
7802 use crate::object::canonical;
7803 use crate::pack_fixture::{build_pack, Item};
7804 let hash = GitHashKind::Sha1;
7805 let mut items: Vec<Item> = Vec::with_capacity(n * 3);
7806 let mut commits: Vec<Vec<u8>> = Vec::with_capacity(n);
7807 let mut prev: Option<Vec<u8>> = start_parent.map(|p| p.to_vec());
7808 for c in 0..n {
7809 let body = format!("blob for commit {c} of {n}, parent {prev:?}\n").into_bytes();
7810 let blob_oid = hash.oid_of(&canonical(GitObjectKind::Blob, &body));
7811 items.push(Item::Whole(GitObjectKind::Blob, body));
7812
7813 let mut tpayload = Vec::new();
7814 tpayload.extend_from_slice(b"100644 f.bin\0");
7815 tpayload.extend_from_slice(&blob_oid);
7816 let tree_oid = hash.oid_of(&canonical(GitObjectKind::Tree, &tpayload));
7817 items.push(Item::Whole(GitObjectKind::Tree, tpayload));
7818
7819 let mut cbody = format!("tree {}\n", hex::encode(&tree_oid));
7820 if let Some(parent) = &prev {
7821 cbody.push_str(&format!("parent {}\n", hex::encode(parent)));
7822 }
7823 cbody.push_str(
7824 "author A U Thor <a@example.com> 1700000000 +0000\n\
7825 committer A U Thor <a@example.com> 1700000000 +0000\n\n\
7826 commit\n",
7827 );
7828 let cbody = cbody.into_bytes();
7829 let commit_oid = hash.oid_of(&canonical(GitObjectKind::Commit, &cbody));
7830 items.push(Item::Whole(GitObjectKind::Commit, cbody));
7831 prev = Some(commit_oid.clone());
7832 commits.push(commit_oid);
7833 }
7834 (build_pack(&items), commits)
7835 }
7836
7837 /// The O(commits) negotiation cost, graded. `reachable` rebuilt the
7838 /// `by_commit` and graph maps on every call — O(C) regardless of how few
7839 /// objects the request touched — so the `root` query (closure ~1) isolates
7840 /// that map-build cost from the walk, and the `tip` query (full closure)
7841 /// pays both. Median of nine, min–max, with the loadavg of its own run.
7842 #[test]
7843 #[ignore = "perf measurement; run with --release --ignored --nocapture"]
7844 fn bench_reachable_negotiation() {
7845 for &n in &[48usize, 2_000, 20_000] {
7846 let dir = tmpdir(&format!("bench-reach-{n}"));
7847 let (pack, commits) = linear_history_pack_from(n, None);
7848 let store = GitStore::open(&dir, "rickard").unwrap();
7849 store.put_pack(&pack).unwrap();
7850 store.wait_indexed();
7851 let root = commits.first().unwrap().clone();
7852 let tip = commits.last().unwrap().clone();
7853 // warm the reach bitmaps once so the loop times the negotiation, not
7854 // the one-time bitmap build.
7855 let _ = store.reachable(&[&tip], &[]).unwrap();
7856
7857 let reps = 9usize;
7858 let (mut root_t, mut tip_t) = (Vec::new(), Vec::new());
7859 for _ in 0..reps {
7860 let t = Instant::now();
7861 let _ = store.reachable(&[&root], &[]).unwrap();
7862 root_t.push(t.elapsed());
7863 let t = Instant::now();
7864 let _ = store.reachable(&[&tip], &[]).unwrap();
7865 tip_t.push(t.elapsed());
7866 }
7867 root_t.sort();
7868 tip_t.sort();
7869 let tip_closure = store.reachable(&[&tip], &[]).unwrap().len();
7870 eprintln!(
7871 "load {}; n={n}: reachable(root, closure~1) med {:?} [{:?}..{:?}] | \
7872 reachable(tip, closure {}) med {:?} [{:?}..{:?}]",
7873 loadavg(),
7874 root_t[reps / 2],
7875 root_t[0],
7876 root_t[reps - 1],
7877 tip_closure,
7878 tip_t[reps / 2],
7879 tip_t[0],
7880 tip_t[reps - 1],
7881 );
7882 }
7883 }
7884
7885 /// **A negotiation answered from the memoized reach view is the answer a
7886 /// cold fold gives — including across a push that changes the fold.**
7887 ///
7888 /// Lane ENGINE-OPT memoizes the `by_commit` and graph lookup maps that
7889 /// `reachable` used to rebuild on every call, in `Derived`, folded by the
7890 /// same call that folds `commit_raw`. The bug this discipline exists to
7891 /// catch is a memo that SURVIVES a fold it should not: after a push adds
7892 /// commits, the warm store must answer with the new closure, byte-identical
7893 /// (order included — both spaces sort the same oids) to a fresh store
7894 /// opened cold on the same bytes.
7895 ///
7896 /// Seen RED by skipping the memo rebuild in `refold` (leaving `graph_index`
7897 /// from before the push): the post-push tip is then not a known commit,
7898 /// `reachable` contributes it alone, and the warm answer is a handful of
7899 /// objects where the cold fold gives the whole chain — the assertion names
7900 /// both lengths.
7901 #[test]
7902 fn reachable_memo_matches_a_cold_fold_across_a_push() {
7903 let dir = tmpdir("reach-memo-across-push");
7904 let (pack_a, commits_a) = linear_history_pack_from(40, None);
7905 let store = GitStore::open(&dir, "rickard").unwrap();
7906 store.put_pack(&pack_a).unwrap();
7907 store.wait_indexed();
7908 let tip_a = commits_a.last().unwrap().clone();
7909 let warm_a = store.reachable(&[&tip_a], &[]).unwrap();
7910 assert!(
7911 warm_a.len() > 10,
7912 "A's closure is too small to tell a stale memo from a coincidence: {}",
7913 warm_a.len()
7914 );
7915
7916 // Push B: a second linear history chained ONTO A's tip, so B's tip
7917 // closes over A too. This is a fold that genuinely grows the graph.
7918 let (pack_b, commits_b) = linear_history_pack_from(40, Some(&tip_a));
7919 store.put_pack(&pack_b).unwrap();
7920 store.wait_indexed();
7921 let tip_b = commits_b.last().unwrap().clone();
7922 let warm_b_full = store.reachable(&[&tip_b], &[]).unwrap();
7923 let warm_b_minus_a = store.reachable(&[&tip_b], &[&tip_a]).unwrap();
7924 assert!(
7925 warm_b_full.len() > warm_a.len(),
7926 "the push did not grow the closure ({} !> {}), so the memo could be \
7927 stale and this test would not know",
7928 warm_b_full.len(),
7929 warm_a.len()
7930 );
7931
7932 // The cold oracle: a fresh handle on the same bytes, folded from disk.
7933 drop(store);
7934 let cold = GitStore::open(&dir, "rickard").unwrap();
7935 cold.wait_indexed();
7936 let cold_b_full = cold.reachable(&[&tip_b], &[]).unwrap();
7937 let cold_b_minus_a = cold.reachable(&[&tip_b], &[&tip_a]).unwrap();
7938
7939 assert_eq!(
7940 warm_b_full, cold_b_full,
7941 "warm memo != cold fold for want=tip_b (a memo that survived the push)"
7942 );
7943 assert_eq!(
7944 warm_b_minus_a, cold_b_minus_a,
7945 "warm memo != cold fold for want=tip_b have=tip_a"
7946 );
7947 // tip_a is still answered identically after the push (its closure did
7948 // not change; only the store grew around it).
7949 let cold_a = cold.reachable(&[&tip_a], &[]).unwrap();
7950 assert_eq!(
7951 warm_a, cold_a,
7952 "tip_a's closure changed across the push"
7953 );
7954 eprintln!(
7955 "load {}; A closure {}, B closure {}, B−A {}",
7956 loadavg(),
7957 warm_a.len(),
7958 warm_b_full.len(),
7959 warm_b_minus_a.len(),
7960 );
7961 }
7962
7963 /// **The clone-after-push lever, witnessed.** A push folds; the fold used
7964 /// to drop the reachability table and the next reader rebuilt it by
7965 /// walking every commit's trees — 0.2 ms per object, 10 s on a 51k-object
7966 /// repository, measured 2026-09-05. Now the fold CARRIES the table
7967 /// (renumbered) when the graph grew by no more than the carry rule allows.
7968 /// RED before the carry: `reach_rebuilt` went 1 → 2 across the push.
7969 #[test]
7970 fn a_push_carries_the_reach_table_across_the_fold() {
7971 let dir = tmpdir("reach-carry-across-push");
7972 let (pack_a, commits_a) = linear_history_pack_from(40, None);
7973 let store = GitStore::open(&dir, "rickard").unwrap();
7974 store.put_pack(&pack_a).unwrap();
7975 store.wait_indexed();
7976 let tip_a = commits_a.last().unwrap().clone();
7977 let warm_a = store.reachable(&[&tip_a], &[]).unwrap();
7978 assert_eq!(store.reach_source_stats(), (0, 1), "the first read builds the table once");
7979
7980 // A push of 30 commits onto A's tip: within the carry rule (64 or a
7981 // tenth of 40, whichever is larger), so the fold carries.
7982 let (pack_b, commits_b) = linear_history_pack_from(30, Some(&tip_a));
7983 store.put_pack(&pack_b).unwrap();
7984 store.wait_indexed();
7985 let tip_b = commits_b.last().unwrap().clone();
7986 let warm_b = store.reachable(&[&tip_b], &[]).unwrap();
7987 let warm_b_minus_a = store.reachable(&[&tip_b], &[&tip_a]).unwrap();
7988 assert_eq!(
7989 store.reach_source_stats(),
7990 (1, 1),
7991 "the fold carried the table; no read rebuilt it"
7992 );
7993 assert!(warm_b.len() > warm_a.len(), "the push grew the closure");
7994 assert_eq!(warm_b_minus_a.len(), warm_b.len() - warm_a.len(), "tip_b − tip_a is exactly the pushed objects");
7995 assert_eq!(store.reachable(&[&tip_a], &[]).unwrap(), warm_a, "tip_a's closure survived the renumbering");
7996
7997 // The cold oracle: the same bytes folded from disk with a fresh table.
7998 drop(store);
7999 let cold = GitStore::open(&dir, "rickard").unwrap();
8000 cold.wait_indexed();
8001 assert_eq!(cold.reachable(&[&tip_b], &[]).unwrap(), warm_b, "carried != rebuilt for want=tip_b");
8002 assert_eq!(
8003 cold.reachable(&[&tip_b], &[&tip_a]).unwrap(),
8004 warm_b_minus_a,
8005 "carried != rebuilt for want=tip_b have=tip_a"
8006 );
8007 assert_eq!(cold.reach_source_stats(), (0, 1), "the cold handle built once and carried nothing");
8008 }
8009
8010 /// The carry rule's other arm: a push larger than the allowance drops the
8011 /// table and the next read rebuilds it whole — bounded staleness, never a
8012 /// walk that grows without limit.
8013 #[test]
8014 fn a_push_past_the_carry_allowance_rebuilds_the_reach_table() {
8015 let dir = tmpdir("reach-carry-rebuild");
8016 let (pack_a, commits_a) = linear_history_pack_from(20, None);
8017 let store = GitStore::open(&dir, "rickard").unwrap();
8018 store.put_pack(&pack_a).unwrap();
8019 store.wait_indexed();
8020 let tip_a = commits_a.last().unwrap().clone();
8021 let _ = store.reachable(&[&tip_a], &[]).unwrap();
8022 assert_eq!(store.reach_source_stats(), (0, 1));
8023 // 100 commits > max(64, 20/10): over the allowance.
8024 let (pack_b, commits_b) = linear_history_pack_from(100, Some(&tip_a));
8025 store.put_pack(&pack_b).unwrap();
8026 store.wait_indexed();
8027 let tip_b = commits_b.last().unwrap().clone();
8028 let warm_b = store.reachable(&[&tip_b], &[]).unwrap();
8029 assert_eq!(store.reach_source_stats(), (0, 2), "over the allowance: not carried, rebuilt once more");
8030 drop(store);
8031 let cold = GitStore::open(&dir, "rickard").unwrap();
8032 cold.wait_indexed();
8033 assert_eq!(cold.reachable(&[&tip_b], &[]).unwrap(), warm_b, "rebuilt warm != cold fold");
8034 }
8035}