use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Condvar, LazyLock, Mutex, Once};
use std::time::{Duration, Instant};
use crate::runtime::sync::PriorityInheritanceMutex;
use crate::runtime::task::{MandatoryThread, StackSizeClass, ThreadPriority};
pub const TASKWD_DELAY: Duration = Duration::from_secs(6);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CheckIn {
Every(Duration),
Unbounded,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TaskwdId(u64);
#[derive(Clone, Debug)]
pub struct TaskInfo {
pub id: TaskwdId,
pub name: String,
}
pub type TaskwdCallback = Arc<dyn Fn() + Send + Sync>;
pub trait TaskwdMonitor: Send + Sync {
fn insert(&self, task: &TaskInfo) {
let _ = task;
}
fn notify(&self, task: &TaskInfo, suspended: bool) {
let _ = (task, suspended);
}
fn remove(&self, task: &TaskInfo) {
let _ = task;
}
}
struct TaskEntry {
info: TaskInfo,
check_in: CheckIn,
beat: Arc<AtomicU64>,
seen_beat: u64,
last_move: Instant,
suspended: bool,
callback: Option<TaskwdCallback>,
}
struct MonitorEntry {
id: u64,
monitor: Arc<dyn TaskwdMonitor>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Ctl {
Run,
Exit,
}
struct Taskwd {
tasks: PriorityInheritanceMutex<Vec<TaskEntry>>,
monitors: PriorityInheritanceMutex<Vec<MonitorEntry>>,
ctl: Mutex<Ctl>,
wake: Condvar,
thread: Mutex<Option<std::thread::JoinHandle<()>>>,
started: Once,
next_id: AtomicU64,
period_ms: AtomicU64,
}
static TASKWD: LazyLock<Arc<Taskwd>> = LazyLock::new(Taskwd::new);
impl Taskwd {
fn new() -> Arc<Self> {
Arc::new(Taskwd {
tasks: PriorityInheritanceMutex::new(Vec::new()),
monitors: PriorityInheritanceMutex::new(Vec::new()),
ctl: Mutex::new(Ctl::Run),
wake: Condvar::new(),
thread: Mutex::new(None),
started: Once::new(),
next_id: AtomicU64::new(1),
period_ms: AtomicU64::new(TASKWD_DELAY.as_millis() as u64),
})
}
fn start(self: &Arc<Self>) {
let owner = self.clone();
self.started.call_once(|| {
*self.ctl.lock().unwrap_or_else(|e| e.into_inner()) = Ctl::Run;
let runner = owner.clone();
let handle = MandatoryThread::new("taskwd", ThreadPriority::Low, StackSizeClass::Small)
.spawn(move || runner.run());
*self.thread.lock().unwrap_or_else(|e| e.into_inner()) = Some(handle);
crate::runtime::exit::at_exit("taskwd", move || owner.shutdown());
});
}
fn run(&self) {
loop {
let ctl = *self.ctl.lock().unwrap_or_else(|e| e.into_inner());
if ctl == Ctl::Exit {
return;
}
self.scan(Instant::now());
let period = Duration::from_millis(self.period_ms.load(Ordering::Relaxed));
let guard = self.ctl.lock().unwrap_or_else(|e| e.into_inner());
let (guard, _) = self
.wake
.wait_timeout_while(guard, period, |c| *c != Ctl::Exit)
.unwrap_or_else(|e| e.into_inner());
if *guard == Ctl::Exit {
return;
}
}
}
fn shutdown(&self) {
*self.ctl.lock().unwrap_or_else(|e| e.into_inner()) = Ctl::Exit;
self.wake.notify_all();
let handle = self.thread.lock().unwrap_or_else(|e| e.into_inner()).take();
if let Some(handle) = handle {
let _ = handle.join();
}
}
fn scan(&self, now: Instant) {
let mut transitions: Vec<(TaskInfo, bool, Option<TaskwdCallback>)> = Vec::new();
{
let mut tasks = self.tasks.lock();
for task in tasks.iter_mut() {
let beat = task.beat.load(Ordering::Relaxed);
if beat != task.seen_beat {
task.seen_beat = beat;
task.last_move = now;
}
let suspended = match task.check_in {
CheckIn::Unbounded => false,
CheckIn::Every(deadline) => {
now.saturating_duration_since(task.last_move) >= deadline
}
};
if suspended != task.suspended {
task.suspended = suspended;
transitions.push((task.info.clone(), suspended, task.callback.clone()));
}
}
}
for (info, suspended, callback) in transitions {
for monitor in self.monitor_snapshot() {
monitor.notify(&info, suspended);
}
if suspended {
crate::runtime::log::errlog_printf(&format!(
"Thread {} ({}) suspended\n",
info.name, info.id.0
));
if let Some(callback) = callback {
callback();
}
}
}
}
fn monitor_snapshot(&self) -> Vec<Arc<dyn TaskwdMonitor>> {
self.monitors
.lock()
.iter()
.map(|m| m.monitor.clone())
.collect()
}
}
pub fn taskwd_init() {
TASKWD.start();
}
pub fn taskwd_insert(
name: impl Into<String>,
check_in: CheckIn,
callback: Option<TaskwdCallback>,
) -> TaskwdEntry {
taskwd_init();
TASKWD.insert(name, check_in, callback)
}
pub struct TaskwdEntry {
owner: Arc<Taskwd>,
info: TaskInfo,
beat: Arc<AtomicU64>,
}
impl TaskwdEntry {
pub fn check_in(&self) {
self.beat.fetch_add(1, Ordering::Relaxed);
}
pub fn info(&self) -> &TaskInfo {
&self.info
}
}
impl Drop for TaskwdEntry {
fn drop(&mut self) {
let removed = {
let mut tasks = self.owner.tasks.lock();
match tasks.iter().position(|t| t.info.id == self.info.id) {
Some(idx) => {
tasks.remove(idx);
true
}
None => false,
}
};
if removed {
for monitor in self.owner.monitor_snapshot() {
monitor.remove(&self.info);
}
}
}
}
pub fn taskwd_monitor_add(monitor: Arc<dyn TaskwdMonitor>) -> TaskwdMonitorEntry {
taskwd_init();
TASKWD.monitor_add(monitor)
}
pub struct TaskwdMonitorEntry {
owner: Arc<Taskwd>,
id: u64,
}
impl Drop for TaskwdMonitorEntry {
fn drop(&mut self) {
self.owner.monitors.lock().retain(|m| m.id != self.id);
}
}
pub fn taskwd_show(level: u32, out: &dyn Fn(&str)) {
TASKWD.show(level, out)
}
impl Taskwd {
fn insert(
self: &Arc<Self>,
name: impl Into<String>,
check_in: CheckIn,
callback: Option<TaskwdCallback>,
) -> TaskwdEntry {
let info = TaskInfo {
id: TaskwdId(self.next_id.fetch_add(1, Ordering::Relaxed)),
name: name.into(),
};
let beat = Arc::new(AtomicU64::new(0));
for monitor in self.monitor_snapshot() {
monitor.insert(&info);
}
self.tasks.lock().push(TaskEntry {
info: info.clone(),
check_in,
beat: beat.clone(),
seen_beat: 0,
last_move: Instant::now(),
suspended: false,
callback,
});
TaskwdEntry {
owner: self.clone(),
info,
beat,
}
}
fn monitor_add(self: &Arc<Self>, monitor: Arc<dyn TaskwdMonitor>) -> TaskwdMonitorEntry {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
self.monitors.lock().push(MonitorEntry { id, monitor });
TaskwdMonitorEntry {
owner: self.clone(),
id,
}
}
fn show(&self, level: u32, out: &dyn Fn(&str)) {
let monitors = self.monitors.lock().len();
let tasks = self.tasks.lock();
out(&format!(
"{} monitors, {} tasks registered",
monitors,
tasks.len()
));
if level == 0 {
return;
}
let width = tasks
.iter()
.map(|t| t.info.name.chars().count())
.max()
.unwrap_or(0)
.max(16);
out(&format!(
"{:width$} {:>9} {:>12} {:>12} {:>8}",
"TASK NAME", "STATE", "CHECK-IN", "LAST BEAT", "CALLBACK"
));
let now = Instant::now();
for task in tasks.iter() {
let check_in = match task.check_in {
CheckIn::Unbounded => "unbounded".to_string(),
CheckIn::Every(d) => format!("{:.1}s", d.as_secs_f64()),
};
out(&format!(
"{:width$} {:>9} {:>12} {:>11.1}s {:>8}",
task.info.name,
if task.suspended { "Suspended" } else { "Ok" },
check_in,
now.saturating_duration_since(task.last_move).as_secs_f64(),
if task.callback.is_some() { "yes" } else { "-" }
));
}
}
}
#[cfg(test)]
impl Taskwd {
fn set_period_for_test(&self, period: Duration) {
self.period_ms
.store(period.as_millis() as u64, Ordering::Relaxed);
self.wake.notify_all();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex as StdMutex;
#[derive(Default)]
struct Recorder {
log: StdMutex<Vec<String>>,
watched: StdMutex<Option<Arc<Taskwd>>>,
}
impl Recorder {
fn taken(&self) -> Vec<String> {
std::mem::take(&mut *self.log.lock().unwrap_or_else(|e| e.into_inner()))
}
fn push(&self, line: String) {
self.log
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(line);
}
}
impl TaskwdMonitor for Recorder {
fn insert(&self, task: &TaskInfo) {
let watched = self
.watched
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
let counts = std::cell::RefCell::new(Vec::new());
if let Some(watched) = watched {
watched.show(0, &|line: &str| counts.borrow_mut().push(line.to_string()));
}
self.push(format!(
"insert {} [{}]",
task.name,
counts.into_inner().join("")
));
}
fn notify(&self, task: &TaskInfo, suspended: bool) {
self.push(format!("notify {} suspended={suspended}", task.name));
}
fn remove(&self, task: &TaskInfo) {
self.push(format!("remove {}", task.name));
}
}
fn watchdog() -> Arc<Taskwd> {
Taskwd::new()
}
fn rows(twd: &Taskwd, level: u32) -> Vec<String> {
let out = std::cell::RefCell::new(Vec::new());
twd.show(level, &|line: &str| out.borrow_mut().push(line.to_string()));
out.into_inner()
}
fn state_of(twd: &Taskwd, name: &str) -> String {
rows(twd, 1)
.into_iter()
.find(|r| r.starts_with(name))
.unwrap_or_else(|| panic!("{name} is not in the report"))
}
#[test]
fn a_task_that_keeps_checking_in_is_not_reported() {
let twd = watchdog();
let task = twd.insert("keeps-up", CheckIn::Every(Duration::from_secs(1)), None);
let start = Instant::now();
task.check_in();
twd.scan(start + Duration::from_secs(2));
assert!(
state_of(&twd, "keeps-up").contains("Ok"),
"{}",
state_of(&twd, "keeps-up")
);
}
#[test]
fn a_task_that_stops_checking_in_is_reported_at_its_own_deadline() {
let twd = watchdog();
let recorder = Arc::new(Recorder::default());
let _monitor = twd.monitor_add(recorder.clone());
let fired = Arc::new(AtomicU64::new(0));
let counter = fired.clone();
let _task = twd.insert(
"wedged",
CheckIn::Every(Duration::from_secs(1)),
Some(Arc::new(move || {
counter.fetch_add(1, Ordering::Relaxed);
})),
);
let start = Instant::now();
let _ = recorder.taken();
twd.scan(start + Duration::from_millis(999));
assert_eq!(fired.load(Ordering::Relaxed), 0, "below its deadline");
assert!(recorder.taken().is_empty(), "nothing to notify below it");
twd.scan(start + Duration::from_secs(1));
assert_eq!(fired.load(Ordering::Relaxed), 1, "at its deadline");
assert_eq!(recorder.taken(), vec!["notify wedged suspended=true"]);
assert!(state_of(&twd, "wedged").contains("Suspended"));
twd.scan(start + Duration::from_secs(30));
assert_eq!(fired.load(Ordering::Relaxed), 1, "the transition, once");
assert!(recorder.taken().is_empty());
}
#[test]
fn a_task_that_checks_in_again_is_reported_recovered() {
let twd = watchdog();
let recorder = Arc::new(Recorder::default());
let _monitor = twd.monitor_add(recorder.clone());
let task = twd.insert("recovers", CheckIn::Every(Duration::from_secs(1)), None);
let start = Instant::now();
twd.scan(start + Duration::from_secs(1));
let _ = recorder.taken();
task.check_in();
twd.scan(start + Duration::from_secs(2));
assert_eq!(recorder.taken(), vec!["notify recovers suspended=false"]);
assert!(state_of(&twd, "recovers").contains("Ok"));
}
#[test]
fn an_unbounded_task_is_listed_and_never_reported() {
let twd = watchdog();
let recorder = Arc::new(Recorder::default());
let _monitor = twd.monitor_add(recorder.clone());
let _task = twd.insert("parked-in-accept", CheckIn::Unbounded, None);
let start = Instant::now();
let _ = recorder.taken();
twd.scan(start + Duration::from_secs(86_400));
assert!(
recorder.taken().is_empty(),
"an unbounded task is never late"
);
assert!(state_of(&twd, "parked-in-accept").contains("Ok"));
}
#[test]
fn dropping_the_handle_removes_the_task_and_tells_the_monitors() {
let twd = watchdog();
let recorder = Arc::new(Recorder::default());
let _monitor = twd.monitor_add(recorder.clone());
let before = rows(&twd, 0);
let task = twd.insert("short-lived", CheckIn::Unbounded, None);
assert_ne!(rows(&twd, 0), before, "in the count while it lives");
let _ = recorder.taken();
drop(task);
assert_eq!(rows(&twd, 0), before, "and out of it once dropped");
assert_eq!(recorder.taken(), vec!["remove short-lived"]);
}
#[test]
fn a_monitor_is_told_before_the_task_joins_the_list() {
let twd = watchdog();
let recorder = Arc::new(Recorder::default());
*recorder.watched.lock().unwrap() = Some(twd.clone());
let _monitor = twd.monitor_add(recorder.clone());
let _task = twd.insert("newcomer", CheckIn::Unbounded, None);
assert_eq!(
recorder.taken(),
vec!["insert newcomer [1 monitors, 0 tasks registered]"]
);
}
#[test]
fn a_name_longer_than_cs_column_is_not_truncated() {
let twd = watchdog();
let long = "CAS-client 192.168.0.44:34122";
let _short = twd.insert("cbLow", CheckIn::Unbounded, None);
let _task = twd.insert(long, CheckIn::Unbounded, None);
let detailed = rows(&twd, 1);
assert!(
detailed.iter().any(|r| r.starts_with(long)),
"the whole name must survive: {detailed:?}"
);
let head = detailed[1].find("STATE").unwrap() + "STATE".len();
for row in &detailed[2..] {
assert_eq!(
row.find("Ok").unwrap() + "Ok".len(),
head,
"STATE must end at one column for header and rows: {detailed:?}"
);
}
}
#[test]
fn short_names_keep_cs_sixteen_wide_layout() {
let twd = watchdog();
let _task = twd.insert("cbLow", CheckIn::Unbounded, None);
let detailed = rows(&twd, 1);
assert_eq!(
detailed[1].find("STATE"),
Some(16 + 1 + 4),
"{:?}",
detailed[1]
);
}
#[test]
fn the_report_is_counts_alone_until_a_level_is_asked_for() {
let twd = watchdog();
let _task = twd.insert("listed", CheckIn::Every(Duration::from_secs(2)), None);
assert_eq!(rows(&twd, 0), vec!["0 monitors, 1 tasks registered"]);
let detailed = rows(&twd, 1);
assert_eq!(detailed.len(), 3, "counts, header, one task: {detailed:?}");
assert!(detailed[1].contains("TASK NAME"));
assert!(detailed[2].contains("2.0s"), "its declared interval");
}
#[test]
fn the_watchdog_thread_reports_a_task_that_stops_checking_in() {
let (console_tx, console_rx) = std::sync::mpsc::channel::<String>();
let listener = crate::runtime::log::errlog_add_listener(move |m: &str| {
if m.contains("stalls") {
let _ = console_tx.send(m.to_string());
}
});
let (fired_tx, fired_rx) = std::sync::mpsc::channel::<()>();
let _task = taskwd_insert(
"stalls",
CheckIn::Every(Duration::from_millis(20)),
Some(Arc::new(move || {
let _ = fired_tx.send(());
})),
);
TASKWD.set_period_for_test(Duration::from_millis(10));
assert!(
fired_rx.recv_timeout(Duration::from_secs(10)).is_ok(),
"the watchdog thread must call a stalled task's callback"
);
let line = console_rx
.recv_timeout(Duration::from_secs(10))
.expect("and say so on the console");
assert!(
line.contains("suspended"),
"C's wording is what an operator greps for: {line}"
);
crate::runtime::log::errlog_remove_listener(listener);
}
}