use std::sync::mpsc;
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PhaseOutcome {
Completed,
Failed(String),
TimedOut,
Panicked,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PhaseReport {
pub name: String,
pub outcome: PhaseOutcome,
pub elapsed: Duration,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ShutdownReport {
pub phases: Vec<PhaseReport>,
}
impl ShutdownReport {
pub fn ok(&self) -> bool {
self.phases
.iter()
.all(|p| p.outcome == PhaseOutcome::Completed)
}
}
type PhaseFn = Box<dyn FnOnce() -> Result<(), String> + Send>;
type ExitFn = Arc<dyn Fn(i32) + Send + Sync>;
struct Phase {
name: String,
deadline: Duration,
run: PhaseFn,
}
struct Inner {
phases: Mutex<Option<Vec<Phase>>>,
watchdog: Mutex<Option<(Duration, i32)>>,
exit: Mutex<ExitFn>,
report: Mutex<Option<ShutdownReport>>,
done: Condvar,
}
#[derive(Clone)]
pub struct OrderedShutdown {
inner: Arc<Inner>,
}
impl Default for OrderedShutdown {
fn default() -> Self {
Self::new()
}
}
impl OrderedShutdown {
pub fn new() -> Self {
Self {
inner: Arc::new(Inner {
phases: Mutex::new(Some(Vec::new())),
watchdog: Mutex::new(None),
exit: Mutex::new(Arc::new(|code| std::process::exit(code))),
report: Mutex::new(None),
done: Condvar::new(),
}),
}
}
#[must_use]
pub fn phase(
self,
name: impl Into<String>,
deadline: Duration,
run: impl FnOnce() -> Result<(), String> + Send + 'static,
) -> Self {
if let Some(phases) = self
.inner
.phases
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_mut()
{
phases.push(Phase {
name: name.into(),
deadline: deadline.max(Duration::from_millis(1)),
run: Box::new(run),
});
}
self
}
#[must_use]
pub fn watchdog(self, hard_deadline: Duration, exit_code: i32) -> Self {
*self
.inner
.watchdog
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some((hard_deadline, exit_code));
self
}
#[must_use]
pub fn on_watchdog(self, exit: impl Fn(i32) + Send + Sync + 'static) -> Self {
*self.inner.exit.lock().unwrap_or_else(|e| e.into_inner()) = Arc::new(exit);
self
}
pub fn run(&self) -> ShutdownReport {
let phases = self
.inner
.phases
.lock()
.unwrap_or_else(|e| e.into_inner())
.take();
let Some(phases) = phases else {
let mut report = self.inner.report.lock().unwrap_or_else(|e| e.into_inner());
while report.is_none() {
report = self
.inner
.done
.wait(report)
.unwrap_or_else(|e| e.into_inner());
}
return report.clone().unwrap_or_default();
};
let finished = Arc::new((Mutex::new(false), Condvar::new()));
let watchdog = *self
.inner
.watchdog
.lock()
.unwrap_or_else(|e| e.into_inner());
if let Some((hard, code)) = watchdog {
let finished = finished.clone();
let exit = self
.inner
.exit
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
let _ = std::thread::Builder::new()
.name("rk-shutdown-watchdog".into())
.spawn(move || {
let (flag, cv) = &*finished;
let guard = flag.lock().unwrap_or_else(|e| e.into_inner());
let (guard, _) = cv
.wait_timeout_while(guard, hard, |done| !*done)
.unwrap_or_else(|e| e.into_inner());
if !*guard {
exit(code);
}
});
}
let mut report = ShutdownReport::default();
for phase in phases {
let started = Instant::now();
let (tx, rx) = mpsc::channel();
let run = phase.run;
let spawned = std::thread::Builder::new()
.name(format!("rk-shutdown-{}", phase.name))
.spawn(move || {
let outcome = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(run))
{
Ok(Ok(())) => PhaseOutcome::Completed,
Ok(Err(e)) => PhaseOutcome::Failed(e),
Err(_) => PhaseOutcome::Panicked,
};
let _ = tx.send(outcome);
});
let outcome = match spawned {
Err(e) => PhaseOutcome::Failed(format!("could not start the phase: {e}")),
Ok(_) => rx
.recv_timeout(phase.deadline)
.unwrap_or(PhaseOutcome::TimedOut),
};
report.phases.push(PhaseReport {
name: phase.name,
outcome,
elapsed: started.elapsed(),
});
}
{
let (flag, cv) = &*finished;
*flag.lock().unwrap_or_else(|e| e.into_inner()) = true;
cv.notify_all();
}
*self.inner.report.lock().unwrap_or_else(|e| e.into_inner()) = Some(report.clone());
self.inner.done.notify_all();
report
}
}