use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
pub(crate) type Cut = Box<dyn FnOnce() + Send>;
enum State {
Armed(Cut),
Disarmed,
Fired,
}
struct Shared {
state: Mutex<State>,
ended: Condvar,
}
impl Shared {
fn watch(&self, at: Instant) {
let mut state = self.state.lock().expect("deadline state poisoned");
loop {
if !matches!(*state, State::Armed(_)) {
return;
}
let Some(left) = at
.checked_duration_since(Instant::now())
.filter(|l| !l.is_zero())
else {
break;
};
state = self
.ended
.wait_timeout(state, left)
.expect("deadline state poisoned")
.0;
}
if let State::Armed(cut) = std::mem::replace(&mut *state, State::Fired) {
cut();
}
}
}
pub(crate) struct PhaseDeadline {
shared: Option<Arc<Shared>>,
at: Option<Instant>,
}
impl PhaseDeadline {
pub(crate) fn none() -> Self {
PhaseDeadline {
shared: None,
at: None,
}
}
pub(crate) fn arm(after: Duration, cut: Cut) -> Self {
let Some(at) = Instant::now().checked_add(after) else {
return Self::none();
};
let shared = Arc::new(Shared {
state: Mutex::new(State::Armed(cut)),
ended: Condvar::new(),
});
let watching = Arc::clone(&shared);
let spawned = std::thread::Builder::new()
.name("rpc-deadline".into())
.spawn(move || watching.watch(at));
match spawned {
Ok(_) => PhaseDeadline {
shared: Some(shared),
at: Some(at),
},
Err(e) => {
log::warn!(
"rsbinder RPC: cannot start a deadline thread ({e}); this phase is bounded \
per wait only"
);
Self::none()
}
}
}
pub(crate) fn arm_with(after: Option<Duration>, cut: impl FnOnce() -> Option<Cut>) -> Self {
match after.and_then(|after| cut().map(|cut| (after, cut))) {
Some((after, cut)) => Self::arm(after, cut),
None => Self::none(),
}
}
pub(crate) fn disarm(&mut self) -> bool {
let Some(shared) = self.shared.take() else {
return true;
};
let mut state = shared.state.lock().expect("deadline state poisoned");
if let State::Fired = *state {
return false;
}
*state = State::Disarmed;
drop(state);
shared.ended.notify_one();
true
}
pub(crate) fn stop(&mut self) -> Option<Option<Duration>> {
let left = self
.at
.take()
.map(|at| at.saturating_duration_since(Instant::now()));
self.disarm().then_some(left)
}
pub(crate) fn fired(&self) -> bool {
self.shared.as_ref().is_some_and(|shared| {
let state = shared.state.lock().expect("deadline state poisoned");
matches!(*state, State::Fired)
})
}
}
impl Drop for PhaseDeadline {
fn drop(&mut self) {
self.disarm();
}
}
impl std::fmt::Debug for PhaseDeadline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PhaseDeadline")
.field("armed", &self.shared.is_some())
.field("fired", &self.fired())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
fn counting() -> (Arc<AtomicUsize>, Cut) {
let cuts = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&cuts);
(
cuts,
Box::new(move || {
counter.fetch_add(1, Ordering::SeqCst);
}),
)
}
fn wait_for(f: impl Fn() -> bool) -> bool {
let until = Instant::now() + Duration::from_secs(5);
while Instant::now() < until {
if f() {
return true;
}
std::thread::sleep(Duration::from_millis(5));
}
f()
}
#[test]
fn a_deadline_that_passes_cuts_once() {
let (cuts, cut) = counting();
let mut d = PhaseDeadline::arm(Duration::from_millis(30), cut);
assert!(wait_for(|| d.fired()), "the deadline never fired");
assert_eq!(cuts.load(Ordering::SeqCst), 1);
assert!(!d.disarm(), "a phase the deadline cut did not end in time");
}
#[test]
fn a_phase_that_ends_in_time_is_never_cut() {
let (cuts, cut) = counting();
let mut d = PhaseDeadline::arm(Duration::from_millis(300), cut);
assert!(d.disarm());
std::thread::sleep(Duration::from_millis(600));
assert_eq!(cuts.load(Ordering::SeqCst), 0);
}
#[test]
fn dropping_the_deadline_disarms_it() {
let (cuts, cut) = counting();
drop(PhaseDeadline::arm(Duration::from_millis(300), cut));
std::thread::sleep(Duration::from_millis(600));
assert_eq!(cuts.load(Ordering::SeqCst), 0);
}
#[test]
fn stopping_disarms_and_keeps_what_was_left() {
let (_, cut) = counting();
let mut long = PhaseDeadline::arm(Duration::from_secs(10), cut);
std::thread::sleep(Duration::from_millis(100));
let left = long.stop().expect("stopped in time").expect("armed");
assert!(
left > Duration::ZERO && left <= Duration::from_millis(9_900),
"{left:?}"
);
let (cuts, cut) = counting();
let mut d = PhaseDeadline::arm(Duration::from_millis(300), cut);
assert!(d.stop().is_some());
std::thread::sleep(Duration::from_millis(600));
assert_eq!(cuts.load(Ordering::SeqCst), 0);
let (_, cut) = counting();
let mut cut_already = PhaseDeadline::arm(Duration::from_millis(1), cut);
assert!(wait_for(|| cut_already.fired()), "the deadline never fired");
assert_eq!(cut_already.stop(), None);
assert_eq!(PhaseDeadline::none().stop(), Some(None));
}
#[test]
fn a_deadline_past_what_an_instant_holds_is_none() {
let (cuts, cut) = counting();
let mut d = PhaseDeadline::arm(Duration::MAX, cut);
assert!(!d.fired());
assert!(d.disarm());
assert_eq!(cuts.load(Ordering::SeqCst), 0);
}
#[test]
fn no_cut_means_no_deadline() {
let mut d = PhaseDeadline::arm_with(Some(Duration::from_millis(1)), || None);
std::thread::sleep(Duration::from_millis(20));
assert!(!d.fired());
assert!(d.disarm());
}
}