Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

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));
}