mod common;
use std::sync::{Arc, Barrier, Mutex};
use gix_hash::Kind;
use gunnar_wire::receive_pack::{AllowAll, Config};
use gunnar_wire::{PackCeiling, PackLedger, ReceivePack};
use common::{incompressible_pack, push_bytes, validating_ingest, Fixture, Tracked};
const CEILING: u64 = 64 * 1024;
const PACK_BYTES: usize = 40 * 1024;
const CLIENTS: usize = 8;
const REFUSAL: &str = "plan 10gb would pass its pack quota with this push: the transfer was \
stopped and nothing was stored; delete or buy more at \
https://gunnar.rs/billing/team";
#[derive(Debug)]
struct TestLedger {
admitted: Mutex<u64>,
}
#[derive(Debug)]
struct Holder {
ledger: Arc<TestLedger>,
at: u64,
}
impl PackLedger for Holder {
fn admit_upto(&self, want: u64, room: u64) -> u64 {
let mut admitted = self.ledger.admitted.lock().unwrap();
let since = admitted.saturating_sub(self.at);
let grant = want.min(room.saturating_sub(since));
*admitted += grant;
grant
}
}
fn door(ledger: &Arc<TestLedger>) -> PackCeiling {
let at = *ledger.admitted.lock().unwrap();
PackCeiling {
bytes: CEILING,
refusal: REFUSAL.to_owned(),
ledger: Some(Arc::new(Holder {
ledger: Arc::clone(ledger),
at,
})),
}
}
struct Pushed {
pack_taken: u64,
admitted: bool,
}
fn push(pack: &[u8], ceiling: PackCeiling) -> Pushed {
let fixture = Fixture::build(Kind::Sha1);
let zero = Kind::Sha1.null().to_string();
let input = push_bytes(
&[(&zero, &fixture.merge.to_string(), "refs/heads/fresh")],
"report-status object-format=sha1",
pack,
);
let commands = (input.len() - pack.len()) as u64;
let mut reader = Tracked::new(input);
let rp = ReceivePack::new(&fixture.git, fixture.kind).with_config(Config {
pack_ceiling: Some(ceiling),
..Config::default()
});
let mut out = Vec::new();
let outcome = rp
.serve(
&mut reader,
&mut out,
&validating_ingest(Kind::Sha1),
&AllowAll,
)
.expect("serve")
.expect("a push happened");
Pushed {
pack_taken: reader.taken - commands,
admitted: outcome.unpack.is_ok(),
}
}
#[test]
fn simultaneous_pushes_are_not_each_admitted_against_the_same_measurement() {
let ledger = Arc::new(TestLedger {
admitted: Mutex::new(0),
});
let pack = incompressible_pack(Kind::Sha1, 1, PACK_BYTES);
assert!(
2 * pack.len() as u64 > CEILING,
"two of these packs must not fit under the ceiling or this test proves nothing: \
{} bytes each, ceiling {CEILING}",
pack.len()
);
let measured = Barrier::new(CLIENTS);
let pushes: Vec<Pushed> = std::thread::scope(|scope| {
let handles: Vec<_> = (0..CLIENTS)
.map(|_| {
let ledger = &ledger;
let measured = &measured;
let pack = &pack;
scope.spawn(move || {
let ceiling = door(ledger);
measured.wait();
push(pack, ceiling)
})
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
let landed: u64 = pushes.iter().map(|pushed| pushed.pack_taken).sum();
let admitted = pushes.iter().filter(|pushed| pushed.admitted).count();
assert!(
landed <= CEILING + 1,
"{CLIENTS} simultaneous pushes put {landed} pack bytes into a namespace with room for \
{CEILING}: {admitted} of them were admitted, each against the same measurement of the \
same store"
);
assert!(
admitted <= 1,
"room for one push and {admitted} were let in"
);
}
#[test]
fn simultaneous_pushes_that_all_fit_are_all_admitted() {
let ledger = Arc::new(TestLedger {
admitted: Mutex::new(0),
});
let pack = incompressible_pack(Kind::Sha1, 1, PACK_BYTES);
let roomy = (CLIENTS as u64 + 1) * pack.len() as u64;
let measured = Barrier::new(CLIENTS);
let pushes: Vec<Pushed> = std::thread::scope(|scope| {
let handles: Vec<_> = (0..CLIENTS)
.map(|_| {
let ledger = &ledger;
let measured = &measured;
let pack = &pack;
scope.spawn(move || {
let at = *ledger.admitted.lock().unwrap();
let ceiling = PackCeiling {
bytes: roomy,
refusal: REFUSAL.to_owned(),
ledger: Some(Arc::new(Holder {
ledger: Arc::clone(ledger),
at,
})),
};
measured.wait();
push(pack, ceiling)
})
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
assert_eq!(
pushes.iter().filter(|pushed| pushed.admitted).count(),
CLIENTS,
"every one of these pushes fits under the ceiling; the ledger must not refuse a push \
because a sibling exists"
);
for pushed in &pushes {
assert_eq!(
pushed.pack_taken,
pack.len() as u64,
"a push that fits must be read WHOLE: a ledger that hands out short of the pack is \
a corrupt push, not a refused one"
);
}
}
#[test]
fn a_panic_mid_push_does_not_cost_the_next_push() {
let ledger = Arc::new(TestLedger {
admitted: Mutex::new(0),
});
let pack = incompressible_pack(Kind::Sha1, 1, PACK_BYTES);
let died = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let ceiling = door(&ledger);
let ledger = ceiling.ledger.as_ref().expect("a standing");
assert_eq!(ledger.admit_upto(CEILING, CEILING), CEILING);
panic!("the connection dropped");
}));
assert!(died.is_err(), "the test's own panic must have happened");
let next = push(&pack, door(&ledger));
assert!(
next.admitted,
"a push that died before it stored anything has locked the tenant out of room they \
still own"
);
assert_eq!(
next.pack_taken,
pack.len() as u64,
"the next push must be read whole, not held to what a dead one was charged"
);
}
#[test]
fn a_cut_push_charges_what_it_took_and_not_what_it_meant_to_send() {
let ledger = Arc::new(TestLedger {
admitted: Mutex::new(0),
});
let huge = incompressible_pack(Kind::Sha1, 8, 128 * 1024);
let cut = push(&huge, door(&ledger));
assert!(!cut.admitted, "a push far over the ceiling must be cut");
assert_eq!(
cut.pack_taken,
CEILING + 1,
"the cut push must cost the server the ceiling and the one byte that trips it, no more"
);
assert_eq!(
*ledger.admitted.lock().unwrap(),
CEILING + 1,
"the namespace is charged what arrived, not the ceiling it was granted and not the \
pack the client meant to send"
);
let small = incompressible_pack(Kind::Sha1, 1, PACK_BYTES);
let next = push(&small, door(&ledger));
assert!(
next.admitted,
"an aborted transfer has charged the tenant for ever; the next push is refused room \
nobody is using"
);
}