use std::{future::Future, time::Duration};
use anyhow::{Context, Result};
use tokio::{task::JoinHandle, time::Instant};
pub(super) enum DaemonHealthOutcome {
Stopped(Result<()>),
Unresponsive(anyhow::Error),
}
pub(super) async fn supervise_daemon(
daemon: impl Future<Output = Result<()>> + Send + 'static,
mut start_heartbeat: impl FnMut() -> JoinHandle<Result<()>>,
interval: Duration,
timeout: Duration,
) -> DaemonHealthOutcome {
let mut daemon = tokio::spawn(daemon);
let mut ticker = tokio::time::interval_at(Instant::now() + interval, interval);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut deadline = Instant::now() + timeout;
let mut in_flight: Option<JoinHandle<Result<()>>> = None;
let mut last_error = None;
loop {
tokio::select! {
result = &mut daemon => {
let result = result.context("Agent scheduler task failed").and_then(|result| result);
if let Some(handle) = in_flight.take()
&& tokio::time::timeout_at(deadline, handle).await.is_err()
{
return unresponsive(timeout, last_error);
}
return DaemonHealthOutcome::Stopped(result);
}
_ = tokio::time::sleep_until(deadline) => {
daemon.abort();
if let Some(handle) = in_flight.take() {
handle.abort();
}
return unresponsive(timeout, last_error);
}
_ = ticker.tick(), if in_flight.is_none() => {
in_flight = Some(start_heartbeat());
}
result = async { in_flight.as_mut().unwrap().await }, if in_flight.is_some() => {
in_flight = None;
match result.context("Daemon heartbeat task failed").and_then(|result| result) {
Ok(()) => {
deadline = Instant::now() + timeout;
last_error = None;
}
Err(error) => last_error = Some(error),
}
}
}
}
}
fn unresponsive(timeout: Duration, last_error: Option<anyhow::Error>) -> DaemonHealthOutcome {
let message = format!(
"Agent service could not refresh its registry heartbeat for {} seconds",
timeout.as_secs()
);
DaemonHealthOutcome::Unresponsive(match last_error {
Some(error) => error.context(message),
None => anyhow::anyhow!(message),
})
}
#[cfg(test)]
mod tests;