crates/gunnar-wire/tests/ref_txn_cost.rs
//! **What a non-atomic push of many references costs the store** (GUN-PERF, 2026-10-31).
//!
//! The first push of git.git carries 1 006 reference commands. Each one was its own
//! one-edit `put_refs_cas`, and every such call is a durable transaction on the
//! engine (a journal row fsynced on znippy): 22 s of the push, 22 ms per reference.
//! A push whose every compare-and-swap holds is one transaction now; only a batch
//! that fails goes back to one call per reference, so a lost race still refuses that
//! reference alone and the others land, which is what a non-atomic push promises.
mod common;
use gix_hash::Kind;
use gunnar_wire::receive_pack::AllowAll;
use gunnar_wire::{ReceivePack, RefStatus};
use common::{empty_pack, push_bytes, validating_ingest, Fixture};
fn push(fixture: &Fixture, commands: &[(String, String, String)]) -> Vec<RefStatus> {
let receive = ReceivePack::new(&fixture.git, fixture.kind);
let borrowed: Vec<(&str, &str, &str)> = commands
.iter()
.map(|(o, n, r)| (o.as_str(), n.as_str(), r.as_str()))
.collect();
let input = push_bytes(
&borrowed,
"report-status object-format=sha1",
&empty_pack(Kind::Sha1),
);
let mut input = std::io::Cursor::new(input);
let mut out = Vec::new();
receive
.serve(
&mut input,
&mut out,
&validating_ingest(Kind::Sha1),
&AllowAll,
)
.expect("the session itself completes")
.expect("a push outcome")
.statuses
}
#[test]
fn a_non_atomic_push_of_many_references_is_one_reference_transaction() {
let fixture = Fixture::build(Kind::Sha1);
let zero = Kind::Sha1.null().to_string();
let targets = [fixture.one, fixture.two, fixture.three, fixture.merge];
let commands: Vec<(String, String, String)> = (0..64)
.map(|i| {
(
zero.clone(),
targets[i % targets.len()].to_string(),
format!("refs/tags/perf-{i:02}"),
)
})
.collect();
fixture.git.reset();
let statuses = push(&fixture, &commands);
assert!(statuses.iter().all(RefStatus::is_ok), "{statuses:?}");
assert_eq!(
fixture.git.ref_txn_count(),
1,
"64 reference creations took {} reference transactions",
fixture.git.ref_txn_count()
);
for (_, new, name) in &commands {
assert_eq!(
fixture.read_ref(name).map(|id| id.to_string()).as_deref(),
Some(new.as_str())
);
}
}
#[test]
fn a_lost_compare_and_swap_still_refuses_only_its_own_reference() {
let fixture = Fixture::build(Kind::Sha1);
let zero = Kind::Sha1.null().to_string();
let commands = vec![
(
zero.clone(),
fixture.two.to_string(),
"refs/tags/before".to_string(),
),
// `main` is at `four`; this command expects `two`: the race is lost.
(
fixture.two.to_string(),
fixture.merge.to_string(),
"refs/heads/main".to_string(),
),
(
zero,
fixture.three.to_string(),
"refs/tags/after".to_string(),
),
];
let statuses = push(&fixture, &commands);
assert!(statuses[0].is_ok(), "{statuses:?}");
assert!(!statuses[1].is_ok(), "{statuses:?}");
assert!(statuses[2].is_ok(), "{statuses:?}");
assert_eq!(fixture.read_ref("refs/heads/main"), Some(fixture.four));
assert_eq!(fixture.read_ref("refs/tags/before"), Some(fixture.two));
assert_eq!(fixture.read_ref("refs/tags/after"), Some(fixture.three));
}