crates/storage-git-gix/parked-gix-2/gunnar-store-gix/tests/multi_pack_lookup.rs
#![cfg(feature = "fs")]
//! `P-012`: object lookup across many packs, and the two things the fix for it
//! could quietly have broken.
//!
//! The change under test does three things: it pools `gix-odb` handles instead
//! of building one per call, it turns **off** `gix-odb`'s re-scan-the-directory
//! -on-every-miss refresh mode, and it writes a multi-pack index so a lookup
//! probes one index rather than `M`. The first is invisible to correctness. The
//! other two are not, and they fail in **opposite directions**:
//!
//! * turning the refresh off can make a present object report **absent**. A
//! pack that landed after the store last scanned is simply not in the
//! snapshot. That is the failure mode of the staleness watch, and
//! [`a_pack_written_out_of_band_is_found_without_an_explicit_refresh`] is what
//! would see it. It writes the pack the way a real push does: through
//! `index_pack` straight into `objects/pack`, telling the store nothing.
//! * a multi-pack index can make an absent object report **present**: it names
//! its `.idx` files, so one left in place across a repack answers for objects
//! whose pack has been unlinked. That is
//! [`a_repack_invalidates_the_multi_pack_index_it_made_stale`].
//!
//! Both directions are asserted, before and after a repack, because `P-012`'s
//! whole subject is a repository whose pack count moves.
//!
//! # Every one of these was seen RED
//!
//! Recorded here because a guard nobody has watched fail is not evidence
//! (LAW 2). The reds are in `.nornir/performance-design.md` `P-012`; each was
//! produced by breaking the implementation, not the test.
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use gix_hash::{Kind, ObjectId};
use gunnar_store::fs::FsStore;
use gunnar_store::midx::MULTI_PACK_INDEX_THRESHOLD;
use gunnar_store::{ObjectStore, RepackOptions};
mod common;
use common::{drop_keep_files, fold_into_pack, pack_dir, packs, read_everything};
/// The multi-pack index, by the name git gives it and `gix-odb` looks for.
fn midx_path(git_dir: &Path) -> PathBuf {
pack_dir(git_dir).join("multi-pack-index")
}
/// A repository whose objects live in `packs` packfiles and nowhere else.
///
/// It holds **no** open store: `FsStore` takes the Arrow log's exclusive lock
/// for the lifetime of the handle, so a second one on the same directory is
/// refused. Every test opens what it needs and drops it, which is also what
/// makes "reopen and look again" expressible at all. That is the only assertion
/// that reads what is on disk rather than what is in a snapshot.
struct Fixture {
_dir: tempfile::TempDir,
git_dir: PathBuf,
hash: Kind,
/// Every object that is really in there.
ids: Vec<ObjectId>,
}
impl Fixture {
/// `pack_count` packs of `per_pack` blobs each, written the way a push
/// writes them and with the loose copies removed.
fn new(hash: Kind, pack_count: usize, per_pack: usize) -> Fixture {
let dir = tempfile::tempdir().expect("a temporary directory");
let git_dir = dir.path().join("repo.git");
let mut ids = Vec::new();
{
let store = FsStore::open_or_init(&git_dir, hash).expect("open the repository");
for pack in 0..pack_count {
let objects = store.objects();
let mut batch = Vec::with_capacity(per_pack);
for blob in 0..per_pack {
let body = format!("pack {pack} blob {blob}\n").repeat(3);
batch.push(
objects
.write(gix_object::Kind::Blob, body.as_bytes())
.expect("write a blob"),
);
}
fold_into_pack(&git_dir, &objects, hash, &batch);
ids.extend(batch);
}
}
drop_keep_files(&git_dir);
assert_eq!(
packs(&git_dir).len(),
pack_count,
"the fixture did not end up with {pack_count} packs, so it cannot say anything \
about lookup across packs"
);
Fixture {
_dir: dir,
git_dir,
hash,
ids,
}
}
/// Open the repository from disk: a fresh store, a fresh snapshot and a
/// fresh handle pool. Only one may be alive at a time.
fn open(&self) -> FsStore {
FsStore::open_or_init(&self.git_dir, self.hash).expect("open the repository")
}
}
/// Oids that are definitely not in `present`, derived rather than random so the
/// set is the same on every run.
///
/// Derived by flipping the leading byte of a real id: same length, same hash
/// kind, same neighbourhood in the sorted index, so the miss walks as far into
/// the fanout and the binary search as a hit does, instead of being rejected by
/// the very first fanout bucket.
fn absent_ids(present: &[ObjectId], count: usize) -> Vec<ObjectId> {
let known: BTreeSet<ObjectId> = present.iter().copied().collect();
let mut out = Vec::with_capacity(count);
let mut i = 0usize;
while out.len() < count {
let source = present[i % present.len()];
let mut raw = source.as_slice().to_vec();
raw[0] ^= 0xA5;
raw[1] ^= (i / present.len()) as u8;
let candidate = ObjectId::from_bytes_or_panic(&raw);
if !known.contains(&candidate) {
out.push(candidate);
}
i += 1;
assert!(i < count * 8 + 64, "could not derive {count} absent ids");
}
out
}
/// Assert both directions at once: everything present is found and readable,
/// and nothing absent is claimed.
///
/// `where_` names the moment, because the same store is checked before a repack,
/// after a multi-pack index, and after a repack, and a failure has to say which.
fn assert_lookup_is_exact(
store: &FsStore,
present: &[ObjectId],
absent: &[ObjectId],
where_: &str,
) {
let objects = store.objects();
let mut buf = Vec::new();
for id in present {
assert!(
objects.contains(id),
"{where_}: {id} is in this repository and `contains` said it is not"
);
let found = objects.read(id, &mut buf).expect("read");
assert!(
found.is_some(),
"{where_}: {id} is in this repository and `read` returned nothing"
);
assert!(
objects.header(id).expect("header").is_some(),
"{where_}: {id} is in this repository and `header` returned nothing"
);
}
for id in absent {
assert!(
!objects.contains(id),
"{where_}: {id} is in no pack and `contains` claimed it is present"
);
assert!(
objects.read(id, &mut buf).expect("read").is_none(),
"{where_}: {id} is in no pack and `read` returned bytes for it"
);
assert!(
objects.header(id).expect("header").is_none(),
"{where_}: {id} is in no pack and `header` returned one"
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// the staleness watch: a miss must not be able to hide a pack
// ─────────────────────────────────────────────────────────────────────────────
/// A pack dropped into `objects/pack` behind the store's back is found by the
/// very next lookup, with nobody calling `refresh()`.
///
/// **This is the guard on turning `gix-odb`'s refresh mode off.** `gunnar-wire`
/// hands a pushed pack to a `PackIngest` the caller supplies and requires only
/// that "after `ingest` returns, the objects must be readable from the
/// `ObjectStore`". No notification, no handle, nothing this crate can hook. So
/// the store has to notice on its own. It does, by watching the mtime of
/// `objects/pack`.
///
/// **Seen red:** with the mtime watch disabled (`refresh_if_stale` returning
/// `false` unconditionally, which is what "just set `refresh_never` and ship it"
/// amounts to) this fails on the first `contains` of the second batch.
#[test]
fn a_pack_written_out_of_band_is_found_without_an_explicit_refresh() {
for hash in [Kind::Sha1, Kind::Sha256] {
let f = Fixture::new(hash, 2, 4);
let store = f.open();
let before = f.ids.clone();
assert_lookup_is_exact(&store, &before, &absent_ids(&before, 8), "before the push");
// A second "push", after the store has already scanned and cached.
let objects = store.objects();
let mut pushed = Vec::new();
for blob in 0..5 {
let body = format!("pushed later {blob}\n").repeat(4);
pushed.push(
objects
.write(gix_object::Kind::Blob, body.as_bytes())
.expect("write a blob"),
);
}
fold_into_pack(&f.git_dir, &objects, hash, &pushed);
drop_keep_files(&f.git_dir);
// No `refresh()` here, deliberately. This is the assertion.
let mut everything = before;
everything.extend(pushed.iter().copied());
assert_lookup_is_exact(
&store,
&everything,
&absent_ids(&everything, 8),
"after a pack was written out of band",
);
}
}
/// The same, on the read path rather than the existence path, and with the
/// store having been made to answer a **miss** for those very objects first.
///
/// A snapshot with the refresh turned off is a store that caches "not here",
/// and such a store is at its most dangerous immediately after it has answered
/// no. `contains` is asked before the pack exists, so the negative answer is
/// real, and then asked again after it does.
#[test]
fn a_miss_answered_before_the_pack_landed_is_answered_again_afterwards() {
let hash = Kind::Sha256;
let f = Fixture::new(hash, 2, 3);
let store = f.open();
let objects = store.objects();
let body = b"an object that is about to exist\n";
let expected = gunnar_store::object::compute_id(hash, gix_object::Kind::Blob, body)
.expect("the id of a blob");
assert!(
!objects.contains(&expected),
"the fixture already contains the object this test is about to add"
);
// Write it loose, pack it, and remove the loose copy, so the ONLY way to
// find it afterwards is through a pack index the store has not seen.
let written = objects
.write(gix_object::Kind::Blob, body)
.expect("write the blob");
assert_eq!(written, expected);
fold_into_pack(&f.git_dir, &objects, hash, &[written]);
drop_keep_files(&f.git_dir);
let mut buf = Vec::new();
assert!(
objects.contains(&written),
"the store went on answering `false` for an object whose pack had landed"
);
let data = objects
.read(&written, &mut buf)
.expect("read")
.expect("the object is in a pack that is on disk");
assert_eq!(
data.data, body,
"the bytes that came back are not the bytes"
);
}
// ─────────────────────────────────────────────────────────────────────────────
// the multi-pack index
// ─────────────────────────────────────────────────────────────────────────────
/// Writing the index spans every pack and every object, read back off the file
/// that landed.
///
/// **Seen red** (2026-08-03) by making `indices_in` drop one `.idx`, an index
/// written one pack short: *"the index does not span all five packs"*. The
/// counts asserted here are read back out of the file that landed, not the ones
/// the writer was handed, which is why a short index cannot report a full one.
#[test]
fn the_multi_pack_index_spans_every_pack_and_every_object() {
let hash = Kind::Sha256;
let f = Fixture::new(hash, 5, 6);
assert!(
!midx_path(&f.git_dir).is_file(),
"five packs is below the threshold of {MULTI_PACK_INDEX_THRESHOLD}, so opening the \
repository must not have written an index"
);
let store = f.open();
let outcome = store.write_multi_pack_index().expect("write the index");
assert!(outcome.written, "nothing was written");
assert!(!outcome.replaced, "there was no previous index to replace");
assert_eq!(outcome.packs, 5, "the index does not span all five packs");
assert_eq!(
outcome.objects as usize,
f.ids.len(),
"the index does not hold every object in the repository"
);
assert!(outcome.bytes > 0, "the index that landed is empty");
assert!(midx_path(&f.git_dir).is_file(), "no index file is on disk");
assert_lookup_is_exact(
&store,
&f.ids,
&absent_ids(&f.ids, 16),
"with a multi-pack index",
);
let again = store.write_multi_pack_index().expect("rewrite the index");
assert!(again.replaced, "the rewrite did not report replacing one");
assert_eq!(again.objects, outcome.objects);
drop(store);
// And from a store that has never seen the packs individually.
assert_lookup_is_exact(
&f.open(),
&f.ids,
&absent_ids(&f.ids, 16),
"reopened, with a multi-pack index",
);
}
/// `gix-odb` really does resolve objects **through** the multi-pack index, not
/// through the per-pack indices it names.
///
/// Writing the file proves nothing on its own: if `gix-odb` ignored it, every
/// assertion above would still pass and the `O(packs)` probe `P-012` is about
/// would still be there. So the per-pack `.idx` files are deleted, leaving the
/// `.pack` data files, which the multi-pack index locates by name, and every
/// object is asked for again. Nothing but the multi-pack index can answer.
///
/// **Seen red** (2026-08-03) with the same one-pack-short probe as above: the
/// objects of the pack the index did not name became unreadable the instant
/// their `.idx` was gone, which is the direct evidence that lookups are going
/// through the multi-pack index and not around it.
#[test]
fn objects_resolve_through_the_multi_pack_index_alone() {
let hash = Kind::Sha256;
let f = Fixture::new(hash, 4, 5);
let store = f.open();
let expected = read_everything(&store, &f.ids);
store.write_multi_pack_index().expect("write the index");
drop(store);
let mut removed = 0;
for entry in std::fs::read_dir(pack_dir(&f.git_dir)).expect("read the pack directory") {
let path = entry.expect("a directory entry").path();
if path.extension().and_then(|e| e.to_str()) == Some("idx") {
std::fs::remove_file(&path).expect("remove a per-pack index");
removed += 1;
}
}
assert_eq!(removed, 4, "the per-pack indices were not the ones removed");
let reopened = f.open();
assert_eq!(
read_everything(&reopened, &f.ids),
expected,
"with only the multi-pack index left, the objects did not come back byte for byte, \
so gix-odb was resolving them through the per-pack indices and the index bought nothing"
);
assert_lookup_is_exact(
&reopened,
&f.ids,
&absent_ids(&f.ids, 16),
"through the multi-pack index alone",
);
}
/// Enough packs, and opening the repository writes the index without being
/// asked, which is the only reason any of this reaches a running server.
#[test]
fn opening_a_repository_with_many_packs_writes_the_index() {
let hash = Kind::Sha256;
let f = Fixture::new(hash, MULTI_PACK_INDEX_THRESHOLD, 2);
// The fixture's store was opened when the repository was empty and dropped
// before the packs existed, so it is the *next* open that crosses the
// threshold.
assert!(
!midx_path(&f.git_dir).is_file(),
"the index appeared before any store had opened a repository with enough packs"
);
let reopened = f.open();
assert!(
midx_path(&f.git_dir).is_file(),
"opening a repository with {MULTI_PACK_INDEX_THRESHOLD} packs did not write a \
multi-pack index, so nothing in production ever gets one"
);
assert_lookup_is_exact(
&reopened,
&f.ids,
&absent_ids(&f.ids, 16),
"after the index was written at open",
);
}
/// Below the threshold, nothing is written and nothing is left behind.
#[test]
fn a_repository_with_few_packs_gets_no_index() {
let hash = Kind::Sha256;
let f = Fixture::new(hash, MULTI_PACK_INDEX_THRESHOLD - 1, 2);
let reopened = f.open();
assert!(
!midx_path(&f.git_dir).is_file(),
"an index was written {} packs below the threshold",
1
);
assert_lookup_is_exact(
&reopened,
&f.ids,
&absent_ids(&f.ids, 8),
"below the threshold",
);
}
// ─────────────────────────────────────────────────────────────────────────────
// the interaction with repack: where a stale index turns into a wrong answer
// ─────────────────────────────────────────────────────────────────────────────
/// A repack unlinks the packs a multi-pack index names, so the index must go
/// first, and every object must still be found afterwards.
///
/// **Seen red** (2026-08-03) with `repack()` made to leave the index entirely
/// alone, both the removal in the swap and the rewrite at the tail disabled:
/// *"object b724…5a42 is not in the store"* on the first read afterwards,
/// because `contains` still answered `true` out of the stale index and the pack
/// it points into had been unlinked.
///
/// Disabling **only** the removal inside the swap does *not* fail this test, and
/// that is worth stating rather than glossing: the rewrite at the tail removes
/// the index a moment later, so a single-threaded test cannot see the gap. The
/// removal in the swap is what closes that gap for a **concurrent** reader, and
/// nothing here proves it does. It stays because the ordering argument is sound,
/// not because this test earned it.
#[test]
fn a_repack_invalidates_the_multi_pack_index_it_made_stale() {
for hash in [Kind::Sha1, Kind::Sha256] {
let f = Fixture::new(hash, 6, 4);
let store = f.open();
store.write_multi_pack_index().expect("write the index");
assert!(midx_path(&f.git_dir).is_file());
let before = read_everything(&store, &f.ids);
let absent = absent_ids(&f.ids, 16);
let outcome = store.repack(&RepackOptions::new()).expect("repack");
assert!(
outcome.rewrote,
"the repack did nothing, so it proves nothing"
);
assert_eq!(
packs(&f.git_dir).len(),
1,
"the repack left more than one pack"
);
// Either it is gone, or it is a fresh one over the packs that remain.
// What must never be true is that it names an unlinked pack, and that
// shows up as a lookup that answers for an object nobody can read.
assert_eq!(
read_everything(&store, &f.ids),
before,
"objects did not survive the repack byte for byte"
);
assert_lookup_is_exact(&store, &f.ids, &absent, "after a repack");
drop(store);
assert_lookup_is_exact(&f.open(), &f.ids, &absent, "reopened after a repack");
}
}
/// The same, from a store that opens **after** the repack, so nothing is
/// carried in memory and the only evidence is what is on disk.
#[test]
fn a_repacked_repository_reopens_without_a_stale_index() {
let hash = Kind::Sha256;
let f = Fixture::new(hash, MULTI_PACK_INDEX_THRESHOLD, 3);
let reopened = f.open();
assert!(
midx_path(&f.git_dir).is_file(),
"the fixture never got the index this test is about"
);
let before = read_everything(&reopened, &f.ids);
reopened.repack(&RepackOptions::new()).expect("repack");
drop(reopened);
let after = f.open();
assert_eq!(
read_everything(&after, &f.ids),
before,
"a repository reopened after a repack did not return the same bytes"
);
assert_lookup_is_exact(
&after,
&f.ids,
&absent_ids(&f.ids, 16),
"reopened after a repack of an indexed repository",
);
assert!(
!midx_path(&f.git_dir).is_file(),
"one pack was left, so the multi-pack index should have been removed rather than \
rewritten over a single index"
);
}
// ─────────────────────────────────────────────────────────────────────────────
// the pool
// ─────────────────────────────────────────────────────────────────────────────
/// Many threads, one store, sharing a bounded pool of `gix-odb` handles.
///
/// `gix-odb`'s handle is `Send` and not `Sync` (its snapshot and inflate state
/// are `RefCell`s), which is exactly why this crate used to build a fresh one
/// per call. Pooling them is the fix, and a pool is the kind of thing that is
/// correct until two threads use it. Every thread asks for every object and for
/// objects that are not there, so a lease handed to two threads at once, or a
/// handle returned to the pool with another thread's borrow still live, shows up
/// as a wrong answer rather than as a race nobody sees.
#[test]
fn concurrent_lookups_share_the_handle_pool_without_lying() {
let hash = Kind::Sha256;
let f = Fixture::new(hash, 4, 8);
let absent = absent_ids(&f.ids, 8);
let store = f.open();
let ids = f.ids.clone();
std::thread::scope(|scope| {
for _ in 0..16 {
let store = store.clone();
let ids = &ids;
let absent = &absent;
scope.spawn(move || {
let objects = store.objects();
let mut buf = Vec::new();
for _ in 0..8 {
for id in ids {
assert!(objects.contains(id), "{id} went missing under concurrency");
assert!(objects.read(id, &mut buf).expect("read").is_some());
}
for id in absent {
assert!(!objects.contains(id), "{id} was invented under concurrency");
}
}
});
}
});
}
// ─────────────────────────────────────────────────────────────────────────────
// the third thing that can quietly break: a miss that IS the answer
// ─────────────────────────────────────────────────────────────────────────────
//
// `ObjectStore::contains_without_rescan` splits the call site the two tests
// above are about into the two it always was. `contains` keeps everything they
// assert. The new method deliberately gives some of it up, and these are the
// tests that say **exactly how much**, in which direction, and that the two
// really are different — which nothing else in the tree can see, because they
// are defined to return the same answer whenever the snapshot is current.
/// **`contains` still re-scans.** The pack-copy and want-validation path is
/// untouched by the split: a pack that landed behind the store's back is still
/// found, and found by the read path too.
///
/// This is [`a_pack_written_out_of_band_is_found_without_an_explicit_refresh`]
/// asked again after the split, and it is here rather than folded into that test
/// because it fails for a *different* reason: not "the watch was disabled" but
/// "the wrong caller was routed onto the cheap path". Routing `contains` itself
/// to `lookup_without_rescan` is a two-character edit that no other test in this
/// repository would notice.
///
/// **Seen RED** by making `FsObjects::contains` call `lookup_without_rescan`:
/// the first `contains` of the pushed batch answers `false`, for an object
/// sitting in a pack on disk. That is the failed clone the split exists not to
/// cause.
#[test]
fn contains_still_finds_a_pack_that_landed_after_the_snapshot() {
for hash in [Kind::Sha1, Kind::Sha256] {
let f = Fixture::new(hash, 2, 4);
let store = f.open();
let objects = store.objects();
// Warm the snapshot AND the handle pool, so what follows is genuinely a
// store that has already looked and already answered.
assert_lookup_is_exact(&store, &f.ids, &absent_ids(&f.ids, 4), "before the push");
let mut pushed = Vec::new();
for blob in 0..5 {
let body = format!("landed after the snapshot {blob}\n").repeat(4);
pushed.push(
objects
.write(gix_object::Kind::Blob, body.as_bytes())
.expect("write a blob"),
);
}
fold_into_pack(&f.git_dir, &objects, hash, &pushed);
drop_keep_files(&f.git_dir);
// The cheap path FIRST, because a `contains` here would refresh the pool
// and hand the cheap path a snapshot that already knows.
let cheap: Vec<bool> = pushed
.iter()
.map(|id| objects.contains_without_rescan(id))
.collect();
let mut buf = Vec::new();
for id in &pushed {
assert!(
objects.contains(id),
"{id} is in a pack on disk and `contains` — the path a want, a delta base and \
the closure walk all take — reported it absent. That is a refused fetch."
);
assert!(
objects.read(id, &mut buf).expect("read").is_some(),
"{id} is in a pack on disk and `read` returned nothing"
);
}
// …and the cheap path's answers, recorded before any of that: it is
// allowed to have said no, and it must never have said yes to something
// absent. Both directions are checked below; this one is the record that
// the two paths were asked in an order that could tell them apart.
assert!(
cheap.len() == pushed.len(),
"the cheap path was not asked about every pushed object"
);
}
}
/// **The two paths really are different, and the difference is the whole
/// change.** On a store whose snapshot predates a pack, `contains` answers
/// `true` and `contains_without_rescan` answers `false` for the very same id.
///
/// This is the only assertion in the tree that can fail if the split is a no-op.
/// Every other check — answers on a current snapshot, wire bytes, object counts,
/// `contains` call totals — is green whether `contains_without_rescan` is wired
/// to the cheap lookup or is still the trait default forwarding to `contains`.
/// So this is what makes "the path you think ran is the path that ran" provable
/// here at all.
///
/// **Seen RED** two ways, and it had to be both: with `contains_without_rescan`
/// left on the trait default it answers `true` and the `!` fails; with
/// `contains` also routed to the cheap lookup the second half fails. A test that
/// only asserted one of the two would pass on a store where *neither* path
/// refreshes, which is the shape that loses objects.
#[test]
fn the_cheap_path_answers_a_miss_where_the_refreshing_path_finds_the_pack() {
let hash = Kind::Sha256;
let f = Fixture::new(hash, 2, 4);
let store = f.open();
let objects = store.objects();
assert_lookup_is_exact(&store, &f.ids, &absent_ids(&f.ids, 4), "before the push");
let body = b"an object in a pack this store has not scanned\n";
let expected = gunnar_store::object::compute_id(hash, gix_object::Kind::Blob, body)
.expect("the id of a blob");
assert!(
!objects.contains(&expected),
"the fixture already holds the object this test is about to add"
);
let written = objects
.write(gix_object::Kind::Blob, body)
.expect("write the blob");
assert_eq!(written, expected);
fold_into_pack(&f.git_dir, &objects, hash, &[written]);
drop_keep_files(&f.git_dir);
assert!(
!objects.contains_without_rescan(&written),
"`contains_without_rescan` consulted the disk. It is not supposed to: this object's \
pack landed after the snapshot, so answering `true` here means the syscall this whole \
change removes is still being paid."
);
assert!(
objects.contains(&written),
"`contains` did not find a pack that landed behind the store's back. The refreshing \
path is the one a want takes, and a stale `false` there is a refused fetch."
);
// And once the refreshing path HAS scanned, the cheap one agrees again:
// the snapshot is shared, so this is a bounded staleness and not a cache of
// "no" that outlives the fact.
assert!(
objects.contains_without_rescan(&written),
"after a refresh the cheap path still says absent, so it is answering from a snapshot \
of its own rather than from the store's. That would be an unbounded stale `no`."
);
}
/// **The cheap path can only ever be wrong in one direction.** It never claims
/// an object the repository does not hold, at any pack count, before or after a
/// repack.
///
/// The safety of the whole change rests on this asymmetry: a stale `false` on a
/// `have` widens a pack, and a stale `true` would put a thin delta's base on the
/// wire pointing at something the client has not got — a failed fetch, not a
/// smaller one.
///
/// **Seen RED** by making `contains_without_rescan` return `true` for anything
/// whose 32-bit prefix matches an object in the index, which is what a
/// prefix-only membership structure would answer: 6 of the 8 derived absent ids
/// are reported present.
#[test]
fn the_cheap_path_never_invents_an_object() {
for hash in [Kind::Sha1, Kind::Sha256] {
for packs in [1usize, 3, 6] {
let f = Fixture::new(hash, packs, 5);
let store = f.open();
let objects = store.objects();
let absent = absent_ids(&f.ids, 16);
for id in &absent {
assert!(
!objects.contains_without_rescan(id),
"{hash:?}/{packs} packs: the cheap path claimed {id}, which is in no pack. \
A false positive here vouches for a thin delta base the client has not got."
);
}
for id in &f.ids {
assert!(
objects.contains_without_rescan(id),
"{hash:?}/{packs} packs: the cheap path missed {id} on a current snapshot. \
Staleness is the only thing it is allowed to lose to."
);
}
}
}
}
/// **The syscall is really gone**, counted rather than inferred.
///
/// `FsStore::rescan_probes` counts every `statx` on `objects/pack` a miss makes
/// to ask whether its snapshot is stale, and every full consolidation that
/// probe turned into. A miss down `contains` moves it; a miss down
/// `contains_without_rescan` must not, and neither must a hit down either.
///
/// This is the arithmetic the bench arm's µs are supposed to be *explained* by,
/// asserted where no box load can blur it.
///
/// # Two regimes, because the refreshing path has two and they differ by 4×
///
/// The watch is only usable as a memo once the mtime it recorded is older than
/// `RACY_WINDOW`; see `odb::RACY_WINDOW` for git's own "racily clean" rule
/// behind that. So:
///
/// * **settled** — the pack directory has been quiet for a second, which is
/// every long-lived server almost all of the time. A miss costs exactly one
/// `statx` and no consolidation.
/// * **hot** — something wrote to `objects/pack` within the last second. Then
/// `refresh_if_stale` cannot trust its memo, consolidates, reports that it
/// did, and the retry loop goes round again: **`LOOKUP_ATTEMPTS` = 4 full
/// directory consolidations for one miss.** MEASURED here, not inferred:
/// 128 probes and 128 consolidations for 32 misses. That is pre-existing and
/// is `contains`'s business, not this split's, but it is the reason the
/// settled figure is the one the bench arm may quote.
///
/// **Seen RED** with `contains_without_rescan` left on the trait default: the
/// settled `contains_without_rescan` count is 32 instead of 0, i.e. one probe
/// per miss — exactly the cost the arm measures.
#[test]
fn only_the_refreshing_path_asks_the_filesystem() {
let hash = Kind::Sha256;
let f = Fixture::new(hash, 3, 6);
let store = f.open();
let objects = store.objects();
let absent = absent_ids(&f.ids, 32);
let n = absent.len() as u64;
// ── hot: the fixture's packs were written moments ago ────────────────
let (h0, r0) = store.rescan_probes();
for id in &absent {
assert!(!objects.contains(id));
}
let (h1, r1) = store.rescan_probes();
assert!(
h1 - h0 > n,
"a miss on a directory written within the racy window is supposed to consolidate and \
retry, so 32 misses should cost MORE than 32 probes; they cost {}. Either the watch is \
being trusted when it must not be, or this test is not in the regime it says.",
h1 - h0
);
assert_eq!(
r1 - r0,
h1 - h0,
"every probe in the hot regime should have turned into a full consolidation"
);
// ── settled: let the recorded mtime age past the racy window ─────────
//
// One second of sleep, and it buys the only regime a server actually lives
// in. `odb`'s own unit tests pay the same second for the same reason.
std::thread::sleep(std::time::Duration::from_millis(1_100));
assert!(
!objects.contains(&absent[0]),
"one miss to settle the watch"
);
let (p0, _) = store.rescan_probes();
for id in &absent {
assert!(!objects.contains(id));
}
let (p1, s1) = store.rescan_probes();
for id in &absent {
assert!(!objects.contains_without_rescan(id));
}
let (p2, s2) = store.rescan_probes();
for id in &f.ids {
assert!(objects.contains_without_rescan(id));
assert!(objects.contains(id));
}
let (p3, s3) = store.rescan_probes();
assert_eq!(
p1 - p0,
n,
"on a settled watch the refreshing path is supposed to consult the filesystem exactly \
once per miss; it consulted it {} times for {n} misses, so this test is not measuring \
what it says",
p1 - p0
);
assert_eq!(
p2 - p1,
0,
"{n} misses down `contains_without_rescan` still cost {} staleness probes. The whole \
change is that number being zero.",
p2 - p1
);
assert_eq!(
p3 - p2,
0,
"a HIT consulted the filesystem, on either path. Nothing about this change should have \
put a syscall on the hit path."
);
assert_eq!(
(s2 - s1, s3 - s2),
(0, 0),
"the cheap path or a hit paid a full directory consolidation"
);
}