use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex, Weak};
use std::time::Duration;
#[cfg(test)]
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
pub(crate) const TAKEOVER_CHECK_INTERVAL: Duration = Duration::from_secs(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Wake {
Stop,
Poke,
Timeout,
}
#[derive(Default)]
struct State {
stop: bool,
poked: bool,
}
pub(crate) struct Bell {
state: Mutex<State>,
cv: Condvar,
#[cfg(test)]
wakes: AtomicU64,
#[cfg(test)]
reads: AtomicU64,
#[cfg(test)]
passes: AtomicU64,
#[cfg(test)]
exited: AtomicBool,
}
impl Bell {
pub(crate) fn new() -> Self {
Self {
state: Mutex::new(State::default()),
cv: Condvar::new(),
#[cfg(test)]
wakes: AtomicU64::new(0),
#[cfg(test)]
reads: AtomicU64::new(0),
#[cfg(test)]
passes: AtomicU64::new(0),
#[cfg(test)]
exited: AtomicBool::new(false),
}
}
pub(crate) fn wait(&self, timeout: Duration) -> Wake {
let guard = self.state.lock().unwrap_or_else(|e| e.into_inner());
let (mut guard, _) = self
.cv
.wait_timeout_while(guard, timeout, |s| !s.stop && !s.poked)
.unwrap_or_else(|e| e.into_inner());
#[cfg(test)]
self.wakes.fetch_add(1, Ordering::SeqCst);
if guard.stop {
Wake::Stop
} else if guard.poked {
guard.poked = false;
Wake::Poke
} else {
Wake::Timeout
}
}
pub(crate) fn stop(&self) {
self.state.lock().unwrap_or_else(|e| e.into_inner()).stop = true;
self.cv.notify_all();
}
fn poke_now(&self) {
self.state.lock().unwrap_or_else(|e| e.into_inner()).poked = true;
self.cv.notify_all();
}
#[cfg(test)]
pub(crate) fn wakes(&self) -> u64 {
self.wakes.load(Ordering::SeqCst)
}
#[cfg(test)]
pub(crate) fn note_read(&self) {
self.reads.fetch_add(1, Ordering::SeqCst);
}
#[cfg(test)]
pub(crate) fn reads(&self) -> u64 {
self.reads.load(Ordering::SeqCst)
}
#[cfg(test)]
pub(crate) fn note_pass(&self) {
self.passes.fetch_add(1, Ordering::SeqCst);
}
#[cfg(test)]
pub(crate) fn passes(&self) -> u64 {
self.passes.load(Ordering::SeqCst)
}
#[cfg(test)]
pub(crate) fn mark_exited(&self) {
self.exited.store(true, Ordering::SeqCst);
}
#[cfg(test)]
pub(crate) fn exited(&self) -> bool {
self.exited.load(Ordering::SeqCst)
}
}
static WATCHERS: Mutex<Vec<(PathBuf, Weak<Bell>)>> = Mutex::new(Vec::new());
pub(crate) fn register(path: PathBuf, bell: &Arc<Bell>) {
let mut watchers = WATCHERS.lock().unwrap_or_else(|e| e.into_inner());
watchers.retain(|(_, w)| w.strong_count() > 0);
watchers.push((path, Arc::downgrade(bell)));
}
pub(crate) fn poke(path: &Path) {
let bells: Vec<Arc<Bell>> = {
let mut watchers = WATCHERS.lock().unwrap_or_else(|e| e.into_inner());
watchers.retain(|(_, w)| w.strong_count() > 0);
watchers
.iter()
.filter(|(p, _)| p.as_path() == path)
.filter_map(|(_, w)| w.upgrade())
.collect()
};
for bell in bells {
bell.poke_now();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_stop_ends_a_long_wait_at_once() {
let bell = Arc::new(Bell::new());
let b = bell.clone();
let (tx, rx) = std::sync::mpsc::channel();
let t = std::thread::spawn(move || {
let reason = b.wait(Duration::from_secs(600));
let _ = tx.send(reason);
});
bell.stop();
assert_eq!(
rx.recv_timeout(Duration::from_secs(10)),
Ok(Wake::Stop),
"stop must end a 10-minute wait"
);
t.join().unwrap();
assert_eq!(bell.wait(Duration::from_secs(600)), Wake::Stop);
}
#[test]
fn a_poke_is_reported_once_and_stop_wins_over_it() {
let bell = Bell::new();
bell.poke_now();
assert_eq!(bell.wait(Duration::from_secs(600)), Wake::Poke);
assert_eq!(bell.wait(Duration::from_millis(1)), Wake::Timeout);
bell.poke_now();
bell.stop();
assert_eq!(bell.wait(Duration::from_secs(600)), Wake::Stop);
}
#[test]
fn a_poke_reaches_only_the_bell_that_watches_that_path() {
let mine = Arc::new(Bell::new());
let other = Arc::new(Bell::new());
let path = PathBuf::from("/rk-takeover-watch-test/mine/takeover.json");
register(path.clone(), &mine);
register(
PathBuf::from("/rk-takeover-watch-test/other/takeover.json"),
&other,
);
{
let gone = Arc::new(Bell::new());
register(path.clone(), &gone);
}
poke(&path);
assert_eq!(mine.wait(Duration::from_secs(600)), Wake::Poke);
assert_eq!(
other.wait(Duration::from_millis(1)),
Wake::Timeout,
"a request for another service must not wake this monitor"
);
let watchers = WATCHERS.lock().unwrap_or_else(|e| e.into_inner());
assert!(watchers
.iter()
.filter(|(p, _)| *p == path)
.all(|(_, w)| w.strong_count() > 0));
}
}