use std::sync::Mutex;
use std::time::Duration;
use tokio::sync::watch;
use crate::runtime::{Interval, TaskHandle};
pub const TASK_STOP_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskKind {
AutoFlush,
AutoCompact,
Sync,
}
pub struct StopSignal {
rx: watch::Receiver<bool>,
}
impl StopSignal {
pub fn is_stopping(&self) -> bool {
*self.rx.borrow()
}
pub async fn stopped(&mut self) {
while !*self.rx.borrow_and_update() {
if self.rx.changed().await.is_err() {
return;
}
}
}
pub async fn tick_or_stop(&mut self, ticker: &mut Interval) -> bool {
tokio::select! {
_ = ticker.tick() => !self.is_stopping(),
_ = self.stopped() => false,
}
}
}
struct Registered {
kind: TaskKind,
stop: watch::Sender<bool>,
handle: TaskHandle,
}
#[derive(Default)]
pub struct TaskRegistry {
tasks: Mutex<Vec<Registered>>,
}
impl TaskRegistry {
pub fn signal() -> (watch::Sender<bool>, StopSignal) {
let (tx, rx) = watch::channel(false);
(tx, StopSignal { rx })
}
pub fn track(&self, kind: TaskKind, stop: watch::Sender<bool>, handle: TaskHandle) {
self.lock().push(Registered { kind, stop, handle });
}
pub fn has(&self, kind: TaskKind) -> bool {
self.lock().iter().any(|task| task.kind == kind)
}
pub async fn stop(&self, kind: TaskKind) -> bool {
let taken: Vec<Registered> = {
let mut tasks = self.lock();
let (matching, rest) = tasks.drain(..).partition(|task| task.kind == kind);
*tasks = rest;
matching
};
let stopped = !taken.is_empty();
Self::wind_down(taken).await;
stopped
}
pub async fn shutdown(&self) {
let taken: Vec<Registered> = self.lock().drain(..).collect();
Self::wind_down(taken).await;
}
async fn wind_down(tasks: Vec<Registered>) {
for task in &tasks {
let _ = task.stop.send(true);
}
for mut task in tasks {
if !task.handle.join_within(TASK_STOP_TIMEOUT).await {
tracing::warn!(
kind = ?task.kind,
timeout_ms = TASK_STOP_TIMEOUT.as_millis(),
"background task ignored its stop signal; aborting"
);
task.handle.abort();
}
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, Vec<Registered>> {
self.tasks.lock().unwrap_or_else(|e| e.into_inner())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn stop_signal_resolves_when_set() {
let (tx, mut signal) = TaskRegistry::signal();
assert!(!signal.is_stopping());
tx.send(true).unwrap();
signal.stopped().await;
assert!(signal.is_stopping());
}
#[tokio::test]
async fn stop_signal_resolves_when_sender_dropped() {
let (tx, mut signal) = TaskRegistry::signal();
drop(tx);
signal.stopped().await;
}
#[tokio::test]
async fn shutdown_stops_a_registered_task() {
let registry = TaskRegistry::default();
let (tx, mut signal) = TaskRegistry::signal();
let handle = crate::runtime::spawn(async move {
signal.stopped().await;
});
registry.track(TaskKind::AutoFlush, tx, handle);
assert!(registry.has(TaskKind::AutoFlush));
registry.shutdown().await;
assert!(!registry.has(TaskKind::AutoFlush));
}
#[tokio::test]
async fn stop_takes_only_the_named_kind() {
let registry = TaskRegistry::default();
for kind in [TaskKind::AutoFlush, TaskKind::Sync] {
let (tx, mut signal) = TaskRegistry::signal();
let handle = crate::runtime::spawn(async move {
signal.stopped().await;
});
registry.track(kind, tx, handle);
}
assert!(registry.stop(TaskKind::Sync).await);
assert!(!registry.has(TaskKind::Sync));
assert!(registry.has(TaskKind::AutoFlush));
assert!(!registry.stop(TaskKind::Sync).await);
registry.shutdown().await;
}
#[tokio::test]
async fn shutdown_aborts_a_task_that_ignores_the_signal() {
let registry = TaskRegistry::default();
let (tx, _signal) = TaskRegistry::signal();
let handle = crate::runtime::spawn(async move {
std::future::pending::<()>().await;
});
registry.track(TaskKind::Sync, tx, handle);
tokio::time::timeout(TASK_STOP_TIMEOUT * 2, registry.shutdown())
.await
.expect("shutdown must not outlast the stop timeout");
}
}