use std::time::Instant;
use crate::error::ServerError;
use crate::worker::registry::{ConnectedWorkerRegistry, WorkerId};
use super::{HeartbeatTracker, TaskLiveness};
impl HeartbeatTracker {
pub fn expired_workers(
&self,
registry: &ConnectedWorkerRegistry,
now: Instant,
) -> Result<Vec<WorkerId>, ServerError> {
let candidates = {
let state = self.state()?;
state
.connections
.iter()
.filter(|(_, last_activity)| {
now.checked_duration_since(**last_activity)
.is_some_and(|elapsed| elapsed > self.heartbeat_window)
})
.map(|(worker_id, _)| *worker_id)
.collect::<Vec<_>>()
};
let mut workers = Vec::new();
for worker_id in candidates {
if is_still_connected(registry, worker_id) && self.is_provably_reachable(worker_id, now)
{
continue;
}
workers.push(worker_id);
}
workers.sort_unstable();
Ok(workers)
}
fn is_provably_reachable(&self, worker_id: WorkerId, now: Instant) -> bool {
match self.is_dispatch_reachable(worker_id, now) {
Ok(reachable) => reachable,
Err(error) => {
tracing::error!(
worker_id = worker_id.value(),
%error,
"worker reachability is unreadable; treating this worker's dispatch path as \
unproved, which is the reading the expiry sweep had before it could ask"
);
false
}
}
}
}
pub(super) fn is_still_connected(registry: &ConnectedWorkerRegistry, worker_id: WorkerId) -> bool {
match registry.worker_by_id(worker_id) {
Ok(worker) => worker.is_some_and(|worker| worker.is_connected()),
Err(error) => {
tracing::error!(
worker_id = worker_id.value(),
%error,
"connected-worker registry is unreadable; judging this worker's push leg closed, \
which is the reading the expiry sweep had before it could ask at all"
);
false
}
}
}
pub(super) fn is_expired(liveness: &TaskLiveness, now: Instant) -> bool {
now.checked_duration_since(liveness.last_heartbeat_at)
.is_some_and(|elapsed| elapsed > liveness.heartbeat_window)
}