use std::sync::Arc;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
#[derive(Clone)]
pub struct AdmissionGate {
limit: Arc<Semaphore>,
}
impl AdmissionGate {
#[must_use]
pub fn new(max_concurrent_turns: usize) -> Self {
Self {
limit: Arc::new(Semaphore::new(max_concurrent_turns)),
}
}
#[must_use]
pub fn try_admit(&self) -> Option<AdmissionPermit> {
Arc::clone(&self.limit)
.try_acquire_owned()
.ok()
.map(AdmissionPermit)
}
}
#[must_use = "dropping this immediately releases the admission slot"]
pub struct AdmissionPermit(
#[allow(dead_code, reason = "held only for its Drop")] OwnedSemaphorePermit,
);
#[cfg(test)]
mod tests {
use super::AdmissionGate;
#[test]
fn admits_up_to_the_limit_then_sheds() {
let gate = AdmissionGate::new(2);
let first = gate.try_admit();
assert!(first.is_some());
let second = gate.try_admit();
assert!(second.is_some());
assert!(gate.try_admit().is_none());
drop(first);
drop(second);
}
#[test]
fn dropping_a_permit_frees_the_slot() {
let gate = AdmissionGate::new(1);
let permit = gate.try_admit();
assert!(permit.is_some());
assert!(gate.try_admit().is_none(), "already at capacity");
drop(permit);
assert!(
gate.try_admit().is_some(),
"freed slot must be admittable again"
);
}
#[test]
fn clones_share_the_same_underlying_limit() {
let gate = AdmissionGate::new(1);
let clone = gate.clone();
let _held = gate.try_admit().expect("first admit");
assert!(clone.try_admit().is_none());
}
}