use std::fmt;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
struct GateState {
name: String,
ready: AtomicBool,
blocking: Arc<AtomicUsize>,
}
#[derive(Clone, Default)]
pub struct Readiness {
gates: Arc<Mutex<Vec<Arc<GateState>>>>,
blocking: Arc<AtomicUsize>,
}
impl Readiness {
pub fn new() -> Self {
Self::default()
}
pub fn gate(&self, name: impl Into<String>) -> ReadinessGate {
let state = Arc::new(GateState {
name: name.into(),
ready: AtomicBool::new(false),
blocking: Arc::clone(&self.blocking),
});
self.blocking.fetch_add(1, Ordering::AcqRel);
self.lock().push(Arc::clone(&state));
ReadinessGate { state }
}
pub fn is_ready(&self) -> bool {
self.blocking.load(Ordering::Acquire) == 0
}
pub fn blocked_on(&self) -> Vec<String> {
self.lock()
.iter()
.filter(|gate| !gate.ready.load(Ordering::Acquire))
.map(|gate| gate.name.clone())
.collect()
}
fn lock(&self) -> MutexGuard<'_, Vec<Arc<GateState>>> {
self.gates
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
impl fmt::Debug for Readiness {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = f.debug_map();
for gate in self.lock().iter() {
debug.entry(&gate.name, &gate.ready.load(Ordering::Acquire));
}
debug.finish()
}
}
#[derive(Clone)]
pub struct ReadinessGate {
state: Arc<GateState>,
}
impl ReadinessGate {
pub fn name(&self) -> &str {
&self.state.name
}
pub fn set_ready(&self, ready: bool) {
if self.state.ready.swap(ready, Ordering::AcqRel) == ready {
return;
}
match ready {
true => self.state.blocking.fetch_sub(1, Ordering::AcqRel),
false => self.state.blocking.fetch_add(1, Ordering::AcqRel),
};
}
pub fn is_ready(&self) -> bool {
self.state.ready.load(Ordering::Acquire)
}
}
impl fmt::Debug for ReadinessGate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ReadinessGate")
.field("name", &self.state.name)
.field("ready", &self.is_ready())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::Readiness;
#[test]
fn empty_registry_is_ready() {
let readiness = Readiness::new();
assert!(readiness.is_ready());
assert!(readiness.blocked_on().is_empty());
}
#[test]
fn a_new_gate_starts_not_ready() {
let readiness = Readiness::new();
let gate = readiness.gate("nats");
assert!(!gate.is_ready());
assert!(!readiness.is_ready());
assert_eq!(readiness.blocked_on(), vec!["nats".to_string()]);
}
#[test]
fn readiness_needs_every_gate() {
let readiness = Readiness::new();
let nats = readiness.gate("nats");
let db = readiness.gate("db");
nats.set_ready(true);
assert!(!readiness.is_ready());
assert_eq!(readiness.blocked_on(), vec!["db".to_string()]);
db.set_ready(true);
assert!(readiness.is_ready());
assert!(readiness.blocked_on().is_empty());
}
#[test]
fn a_gate_can_flip_back() {
let readiness = Readiness::new();
let gate = readiness.gate("nats");
gate.set_ready(true);
assert!(readiness.is_ready());
gate.set_ready(false);
assert!(!readiness.is_ready());
assert_eq!(readiness.blocked_on(), vec!["nats".to_string()]);
}
#[test]
fn repeating_a_gate_value_leaves_readiness_where_it_was() {
let readiness = Readiness::new();
let gate = readiness.gate("nats");
gate.set_ready(true);
gate.set_ready(true);
assert!(readiness.is_ready());
gate.set_ready(false);
assert!(!readiness.is_ready());
}
#[test]
fn concurrent_flips_leave_readiness_consistent() {
let readiness = Readiness::new();
let gate = readiness.gate("nats");
gate.set_ready(true);
let flippers: Vec<_> = (0..8)
.map(|_| {
let gate = gate.clone();
std::thread::spawn(move || {
for _ in 0..1_000 {
gate.set_ready(false);
gate.set_ready(true);
}
})
})
.collect();
for flipper in flippers {
flipper.join().expect("flipper thread");
}
assert!(readiness.is_ready());
assert!(readiness.blocked_on().is_empty());
}
#[test]
fn clones_share_one_registry() {
let readiness = Readiness::new();
let gate = readiness.gate("nats");
let observer = readiness.clone();
assert!(!observer.is_ready());
gate.set_ready(true);
assert!(observer.is_ready());
}
#[test]
fn a_gate_outlives_its_handles() {
let readiness = Readiness::new();
drop(readiness.gate("nats"));
assert!(!readiness.is_ready());
assert_eq!(readiness.blocked_on(), vec!["nats".to_string()]);
}
}