#![cfg(feature = "admit-share")]
use std::sync::Arc;
use edda_admit::{Admit, Class, Settings, WorkKind};
use gunnar_core::{ControlStore, FlowLedger, FlowStore, RedbControlStore, Transfer};
use gunnar_ssh::governor::OpClass;
use gunnar_ssh::{Admission, Refusal};
fn ledger(heavy_bytes: u64) -> Arc<FlowLedger> {
let store = Arc::new(RedbControlStore::in_memory().unwrap()) as Arc<dyn ControlStore>;
let flow = Arc::new(FlowLedger::new(FlowStore::new(store).unwrap()));
let now = gunnar_core::now_ms();
for (ns, bytes) in [("heavy", heavy_bytes), ("light", 10)] {
flow.record(
ns,
now,
&Transfer {
bytes_out: bytes,
fetched: true,
..Transfer::default()
},
);
}
flow
}
fn gate(policy: Arc<dyn Admit>, flow: Arc<FlowLedger>) -> Arc<Admission> {
Arc::new(
Admission::new(Some(8), Some(1))
.with_anonymous_ceiling(Some(2))
.with_flow(flow)
.with_policy(policy),
)
}
fn budget(bytes: u64, flow: &Arc<FlowLedger>) -> Arc<dyn Admit> {
Settings {
limit: Some(bytes),
..Settings::default()
}
.build_seeded(Some(Admission::flow_seed(Arc::clone(flow))))
}
#[tokio::test]
async fn an_over_budget_namespace_gets_the_anonymous_ceiling() {
let flow = ledger(5000);
let gate = gate(budget(1000, &flow), flow);
let heavy: Vec<_> = (0..2)
.map(|_| {
gate.try_admit("p-heavy", "heavy", OpClass::Clone)
.expect("inside the ceiling")
})
.collect();
let refused = gate
.try_admit("p-heavy", "heavy", OpClass::Clone)
.expect_err("a third transfer is past the reduced share");
assert_eq!(
refused,
Refusal::ReducedShare {
ceiling: 2,
busy: false
}
);
assert_eq!(refused.reason(), "admission-reduced-share");
let words = refused.client_message();
assert!(words.contains("over its transfer budget"), "{words}");
assert!(
!words.contains("concurrent git transfers") && !words.contains("per-principal limit"),
"installtest tells refusals apart by those substrings: {words}"
);
let light: Vec<_> = (0..3)
.map(|_| {
gate.try_admit("p-light", "light", OpClass::Clone)
.expect("Full share")
})
.collect();
assert_eq!(gate.stats().refused_reduced, 1);
assert_eq!(
gate.stats().refused_anonymous,
0,
"not counted as a stranger"
);
let started = std::time::Instant::now();
let parked = gate.admit_within("p-heavy", "heavy", OpClass::Clone).await;
assert!(matches!(parked, Err(Refusal::ReducedShare { .. })));
assert!(
started.elapsed() < gate.wait(),
"the reduced share was parked"
);
drop((heavy, light));
}
#[tokio::test]
async fn under_load_the_over_budget_namespace_gives_way() {
let flow = ledger(5000);
let gate = gate(budget(1000, &flow), flow);
let load: Vec<_> = (0..6)
.map(|i| {
gate.try_admit(&format!("p-light-{i}"), "light", OpClass::Fetch)
.expect("the box has room")
})
.collect();
assert_eq!(
gate.try_admit("p-heavy", "heavy", OpClass::Clone)
.expect_err("reserved"),
Refusal::ReducedShare {
ceiling: 2,
busy: true
}
);
let keyed = gate.try_admit("p-light-x", "light", OpClass::Fetch);
assert!(keyed.is_ok(), "{keyed:?}");
drop(load);
}
#[test]
fn pass_through_is_the_gate_as_it_was() {
let flow = ledger(u64::MAX / 4);
for policy in [
Settings::pass_through().build(),
Arc::new(edda_admit::PassThrough::new()) as Arc<dyn Admit>,
] {
let with = gate(policy, Arc::clone(&flow));
let without = Arc::new(
Admission::new(Some(8), Some(1))
.with_anonymous_ceiling(Some(2))
.with_flow(Arc::clone(&flow)),
);
let a: Vec<_> = (0..8)
.map(|_| with.try_admit("p-heavy", "heavy", OpClass::Clone))
.collect();
let b: Vec<_> = (0..8)
.map(|_| without.try_admit("p-heavy", "heavy", OpClass::Clone))
.collect();
assert_eq!(
a.iter()
.map(|r| r.as_ref().err().copied())
.collect::<Vec<_>>(),
b.iter()
.map(|r| r.as_ref().err().copied())
.collect::<Vec<_>>(),
);
assert!(
a.iter().all(Result::is_ok),
"an idle box is the heavy namespace's to fill"
);
assert_eq!(with.stats().refused_reduced, 0);
}
}
#[test]
fn the_plan_text_names_the_budget_the_usage_and_the_class() {
let flow = ledger(63_000_000_000);
let gate = gate(budget(100_000_000_000, &flow), flow);
let policy = gate.admit_policy();
let usage = gate.admit_usage(&*policy, "heavy");
assert_eq!(
usage,
policy.usage(&edda_admit::Consumer::Namespace("heavy".into()))
);
assert_eq!(usage, edda_admit::Usage::new(63_000_000_000, 30));
let plan = policy.plan(
&edda_admit::Work::new(WorkKind::Clone, Some(1_200_000_000)),
&edda_admit::Consumer::Namespace("heavy".into()),
&usage,
);
assert_eq!(plan.class, Class::Full);
assert_eq!(plan.why, "100 GB/30d used 63 GB; this clone ~1.2 GB; Full");
assert_eq!(policy.policy(), "usage-share");
}
#[test]
fn the_record_feeds_the_estimate() {
let flow = ledger(10);
let gate = gate(budget(1_000_000, &flow), flow);
let policy = gate.admit_policy();
let light = edda_admit::Consumer::Namespace("light".into());
assert_eq!(policy.usage(&light).bytes, 10, "seeded from the ledger");
let guard = gate.try_admit("p-light", "light", OpClass::Clone).unwrap();
guard.reserve_more(7_300);
drop(guard);
let push = gate.try_admit("p-light", "light", OpClass::Push).unwrap();
push.reserve_more(400);
drop(push);
assert_eq!(
policy.usage(&light).bytes,
10 + 7_300 + 400,
"the table is the cache"
);
let rows = policy.estimates();
assert_eq!(rows.len(), 2);
assert_eq!((rows[0].kind, rows[0].samples), (WorkKind::Clone, 1));
assert_eq!((rows[0].mean.net_in, rows[0].mean.net_out), (0, 7_300));
assert_eq!(
(rows[1].kind, rows[1].mean.net_in, rows[1].mean.net_out),
(WorkKind::Push, 400, 0)
);
let plan = gate.admit_policy().plan(
&edda_admit::Work::new(WorkKind::Clone, None),
&edda_admit::Consumer::Namespace("light".into()),
&edda_admit::Usage::none(),
);
assert_eq!(plan.estimate.usage_bytes(), 7_300);
assert!(plan.why.contains("this clone ~7.3 KB"), "{}", plan.why);
}
#[test]
fn anonymous_is_not_planned() {
let flow = ledger(5000);
let gate = gate(budget(1, &flow), flow);
let a: Vec<_> = (0..2)
.map(|_| {
gate.try_admit(gunnar_core::ANONYMOUS_PRINCIPAL_ID, "heavy", OpClass::Clone)
.unwrap()
})
.collect();
assert_eq!(
gate.try_admit(gunnar_core::ANONYMOUS_PRINCIPAL_ID, "heavy", OpClass::Clone)
.unwrap_err(),
Refusal::AnonymousAtCapacity { limit: 2 }
);
assert_eq!(
gate.stats().refused_reduced,
0,
"a stranger is not a reduced namespace"
);
drop(a);
assert!(gate.admit_policy().estimates().is_empty());
}