use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[derive(Clone, Default)]
pub struct JobGate {
busy: Arc<AtomicBool>,
}
impl JobGate {
pub fn new() -> Self {
Self::default()
}
pub fn from_shared(busy: Arc<AtomicBool>) -> Self {
Self { busy }
}
pub fn shared(&self) -> Arc<AtomicBool> {
self.busy.clone()
}
pub fn is_busy(&self) -> bool {
self.busy.load(Ordering::SeqCst)
}
pub fn try_reserve(&self) -> Option<JobReservation> {
self.busy
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.ok()
.map(|_| JobReservation {
busy: self.busy.clone(),
})
}
}
pub struct JobReservation {
busy: Arc<AtomicBool>,
}
impl Drop for JobReservation {
fn drop(&mut self) {
self.busy.store(false, Ordering::SeqCst);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reserve_is_exclusive_until_dropped() {
let gate = JobGate::new();
assert!(!gate.is_busy());
let reservation = gate.try_reserve().expect("first reserve wins");
assert!(gate.is_busy());
assert!(
gate.try_reserve().is_none(),
"a second reserve must fail while the first is held"
);
drop(reservation);
assert!(!gate.is_busy(), "dropping the reservation frees the slot");
assert!(gate.try_reserve().is_some(), "the slot is claimable again");
}
#[test]
fn clones_share_one_slot() {
let gate = JobGate::new();
let other = gate.clone();
let _held = gate.try_reserve().unwrap();
assert!(other.is_busy());
assert!(other.try_reserve().is_none());
}
#[test]
fn from_shared_adopts_an_existing_flag() {
let flag = Arc::new(AtomicBool::new(false));
let gate = JobGate::from_shared(flag.clone());
let _held = gate.try_reserve().unwrap();
assert!(
flag.load(Ordering::SeqCst),
"reserving must set the adopted flag so existing readers see it"
);
}
#[test]
fn reservation_releases_on_panic_unwind() {
let gate = JobGate::new();
let gate_for_thread = gate.clone();
let _ = std::thread::spawn(move || {
let _held = gate_for_thread.try_reserve().unwrap();
panic!("job blew up");
})
.join();
assert!(
!gate.is_busy(),
"the slot must be free after a panicking holder unwinds"
);
}
}