use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use gunnar_ssh::governor::OpClass;
use gunnar_ssh::gunnar_core::ANONYMOUS_PRINCIPAL_ID;
use gunnar_ssh::{Admission, Refusal};
fn gate(limit: usize, keyed_wait: Option<Duration>) -> Arc<Admission> {
Arc::new(
Admission::new(Some(limit), Some(8))
.with_anonymous_ceiling(Some(5))
.with_wait(Duration::from_millis(50))
.with_keyed_wait(keyed_wait),
)
}
async fn burst(
gate: &Arc<Admission>,
principal: &'static str,
n: usize,
hold: Duration,
) -> (usize, Vec<Refusal>) {
let admitted = Arc::new(AtomicUsize::new(0));
let mut tasks = tokio::task::JoinSet::new();
for _ in 0..n {
let gate = Arc::clone(gate);
let admitted = Arc::clone(&admitted);
tasks.spawn(async move {
match gate
.admit_within(principal, "nordisk", OpClass::Fetch)
.await
{
Ok((guard, _waited)) => {
admitted.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(hold).await;
drop(guard);
None
}
Err(refusal) => Some(refusal),
}
});
}
let mut refusals = Vec::new();
while let Some(done) = tasks.join_next().await {
if let Some(r) = done.expect("a transfer task") {
refusals.push(r);
}
}
(admitted.load(Ordering::SeqCst), refusals)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 8)]
async fn forty_keyed_transfers_are_all_served_and_forty_strangers_get_their_ceiling() {
let keyed = gate(8, Some(Duration::from_secs(20)));
let (admitted, refusals) = burst(&keyed, "admin", 40, Duration::from_millis(150)).await;
assert!(
refusals.is_empty(),
"a keyed CI burst was refused although it may queue for 20 s and every slot frees in \
150 ms: {} refused, first {:?}",
refusals.len(),
refusals.first()
);
assert_eq!(admitted, 40);
assert_eq!(keyed.stats().refused(), 0);
let open = gate(64, Some(Duration::from_secs(20)));
let started = std::time::Instant::now();
let (admitted, refusals) = burst(
&open,
ANONYMOUS_PRINCIPAL_ID,
40,
Duration::from_millis(1500),
)
.await;
assert_eq!(
admitted, 5,
"a stranger's ceiling is 5 however idle the box is"
);
assert_eq!(refusals.len(), 35);
for r in &refusals {
assert!(
matches!(r, Refusal::AnonymousAtCapacity { limit: 5 }),
"{r:?}"
);
assert!(
r.client_message().contains("try again"),
"transient words: {}",
r.client_message()
);
}
assert_eq!(
open.stats().refused_anonymous,
35,
"counted under its own reason"
);
assert!(
started.elapsed() < Duration::from_secs(5),
"{:?}",
started.elapsed()
);
let old = gate(8, None);
let (admitted, refusals) = burst(&old, "admin", 40, Duration::from_millis(150)).await;
assert!(
!refusals.is_empty() && admitted < 40,
"the control arm (2026-10-25 behaviour: the 50 ms wait for keys too) must refuse some of \
the burst, or this test proves nothing: admitted {admitted}"
);
}