use std::future::Future;
use std::path::{Path, PathBuf};
use std::time::Duration;
use chrono::{DateTime, Utc};
use tokio::task::JoinHandle;
use tracing::{debug, error, info};
use super::publish::{self, SupervisorMetricsStatus};
pub(super) async fn bounded<F: Future>(limit: Duration, what: &str, fut: F) -> Option<F::Output> {
match tokio::time::timeout(limit, fut).await {
Ok(out) => Some(out),
Err(_) => {
error!(
limit_secs = limit.as_secs(),
"supervisor: {what} did not finish within its bound; abandoning it so \
the sweep loop continues (#8335)"
);
None
}
}
}
const BLOCKED_GRACE: Duration = Duration::from_millis(500);
pub(super) struct DetachedSweep<T> {
what: &'static str,
orphan: Option<JoinHandle<Option<T>>>,
}
impl<T: Send + 'static> DetachedSweep<T> {
pub(super) fn new(what: &'static str) -> Self {
Self { what, orphan: None }
}
pub(super) async fn run<F, Fut>(&mut self, limit: Duration, make: F) -> Option<T>
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = T> + 'static,
{
let what = self.what;
if let Some(orphan) = &self.orphan {
if !orphan.is_finished() {
error!(
"supervisor: the {what} abandoned earlier is still blocked; skipping \
this one so two never run at once (#8335)"
);
return None;
}
self.orphan = None;
info!("supervisor: the abandoned {what} has finished");
}
let rt = tokio::runtime::Handle::current();
let mut task = tokio::task::spawn_blocking(move || {
rt.block_on(async move { tokio::time::timeout(limit, make()).await.ok() })
});
let limit_secs = limit.as_secs();
match tokio::time::timeout(limit + BLOCKED_GRACE, &mut task).await {
Ok(Ok(Some(out))) => Some(out),
Ok(Ok(None)) => {
error!(
limit_secs,
"supervisor: {what} did not finish within its bound; abandoned it at an \
await point (#8335)"
);
None
}
Ok(Err(e)) => {
error!("supervisor: {what} did not complete: {e} (#8335)");
None
}
Err(_) => {
error!(
limit_secs,
"supervisor: {what} is blocked past its bound; abandoning it so the \
sweep loop continues. Its thread stays blocked until the call returns, \
and the child process is not killed (#8335)"
);
self.orphan = Some(task);
None
}
}
}
}
pub(super) fn check_heartbeat(path: &Path, now: DateTime<Utc>, was_stale: bool) -> bool {
match publish::read_status_at(path, now) {
SupervisorMetricsStatus::Stale {
snapshot,
age_secs,
stale_after_secs,
} => {
if !was_stale {
error!(
path = %path.display(),
written_at = %snapshot.written_at,
age_secs,
stale_after_secs,
consecutive_sweeps_abandoned =
snapshot.fleet.run_stats.consecutive_sweeps_abandoned,
"supervisor: heartbeat is stale; no fleet sweep has completed and \
published within its window (#8335)"
);
}
true
}
SupervisorMetricsStatus::Current { age_secs, .. } => {
if was_stale {
info!(age_secs, "supervisor: heartbeat recovered");
}
false
}
SupervisorMetricsStatus::Unavailable { .. } => was_stale,
}
}
pub(super) struct HeartbeatWatch {
period: Duration,
last_check: Option<DateTime<Utc>>,
stale: bool,
}
impl HeartbeatWatch {
pub(super) fn new(period: Duration) -> Self {
Self {
period,
last_check: None,
stale: false,
}
}
pub(super) fn observe(&mut self, path: &Path, now: DateTime<Utc>) -> bool {
let gap = self.last_check.map(|prev| now - prev);
self.last_check = Some(now);
let max_gap = chrono::Duration::from_std(self.period.saturating_mul(2))
.unwrap_or(chrono::Duration::MAX);
if let Some(gap) = gap.filter(|g| *g > max_gap) {
debug!(
gap_secs = gap.num_seconds(),
"supervisor: wall clock jumped since the last heartbeat check (system \
sleep?); skipping this verdict"
);
return self.stale;
}
self.stale = check_heartbeat(path, now, self.stale);
self.stale
}
}
pub(super) fn spawn_heartbeat_watchdog(path: PathBuf, period: Duration) -> JoinHandle<()> {
tokio::spawn(async move {
let mut timer = tokio::time::interval(period);
let mut watch = HeartbeatWatch::new(period);
loop {
timer.tick().await;
watch.observe(&path, Utc::now());
}
})
}
pub(super) struct LoopExitGuard {
watchdog: Option<JoinHandle<()>>,
armed: bool,
}
impl LoopExitGuard {
pub(super) fn new(watchdog: Option<JoinHandle<()>>) -> Self {
Self {
watchdog,
armed: true,
}
}
pub(super) fn disarm(&mut self) {
self.armed = false;
}
}
impl Drop for LoopExitGuard {
fn drop(&mut self) {
if let Some(handle) = self.watchdog.take() {
handle.abort();
}
if self.armed {
error!(
panicking = std::thread::panicking(),
"supervisor: sweep loop exited without a shutdown signal; fleet sweeps \
and heartbeat publishing have STOPPED (#8335)"
);
}
}
}