use std::future::Future;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::Duration;
use tokio::task::{AbortHandle, JoinSet};
struct AbortOnDrop(AbortHandle);
impl Drop for AbortOnDrop {
fn drop(&mut self) {
self.0.abort();
}
}
#[derive(Default)]
struct State {
set: JoinSet<()>,
closed: bool,
}
#[derive(Clone)]
pub(crate) struct TaskTracker {
state: Arc<Mutex<State>>,
panics: Arc<AtomicU64>,
owner: &'static str,
}
impl TaskTracker {
pub(crate) fn new(owner: &'static str) -> Self {
Self {
state: Arc::new(Mutex::new(State::default())),
panics: Arc::new(AtomicU64::new(0)),
owner,
}
}
fn lock(&self) -> MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(|e| e.into_inner())
}
pub(crate) fn spawn<F>(&self, task: &'static str, future: F) -> bool
where
F: Future<Output = ()> + Send + 'static,
{
let owner = self.owner;
let panics = self.panics.clone();
let mut state = self.lock();
if state.closed {
tracing::warn!("{owner}: not starting {task}: shutting down");
return false;
}
while state.set.try_join_next().is_some() {}
let handle = tokio::spawn(future);
let guard = AbortOnDrop(handle.abort_handle());
state.set.spawn(async move {
let _guard = guard;
if let Err(err) = handle.await {
if err.is_panic() {
panics.fetch_add(1, Ordering::Relaxed);
tracing::error!("{owner}: {task} task panicked: {err}");
} else {
tracing::debug!("{owner}: {task} task cancelled");
}
}
});
true
}
pub(crate) fn panic_count(&self) -> u64 {
self.panics.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.lock().set.len()
}
pub(crate) fn close(&self) {
self.lock().closed = true;
}
pub(crate) async fn abort_all(&self) {
let mut set = {
let mut state = self.lock();
state.set.abort_all();
std::mem::take(&mut state.set)
};
while set.join_next().await.is_some() {}
}
pub(crate) async fn drain(&self, grace: Duration) -> usize {
let mut set = {
let mut state = self.lock();
state.closed = true;
std::mem::take(&mut state.set)
};
let finished =
tokio::time::timeout(grace, async { while set.join_next().await.is_some() {} })
.await
.is_ok();
if finished {
return 0;
}
let remaining = set.len();
tracing::warn!(
"{}: aborting {remaining} task(s) still running after {grace:?}",
self.owner
);
set.abort_all();
while set.join_next().await.is_some() {}
remaining
}
}
pub(crate) struct ListenerHandle {
pub(crate) subscription_id: uuid::Uuid,
_stop: tokio::sync::oneshot::Sender<()>,
}
impl ListenerHandle {
pub(crate) fn new(subscription_id: uuid::Uuid) -> (Self, tokio::sync::oneshot::Receiver<()>) {
let (stop, stopped) = tokio::sync::oneshot::channel();
(
Self {
subscription_id,
_stop: stop,
},
stopped,
)
}
}
pub(crate) fn clamp_permits(configured: usize, name: &str) -> usize {
let permits = configured.clamp(1, tokio::sync::Semaphore::MAX_PERMITS);
if permits != configured {
tracing::warn!(
configured,
effective = permits,
"{name} is out of range, clamping"
);
}
permits
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn dropping_a_listener_handle_stops_the_listener() {
let (handle, mut stopped) = ListenerHandle::new(uuid::Uuid::new_v4());
assert!(stopped.try_recv().is_err());
drop(handle);
assert!(stopped.await.is_err());
}
#[test]
fn clamp_permits_bounds_the_value() {
assert_eq!(clamp_permits(0, "x"), 1);
assert_eq!(clamp_permits(5, "x"), 5);
assert_eq!(
clamp_permits(usize::MAX, "x"),
tokio::sync::Semaphore::MAX_PERMITS
);
}
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[tokio::test]
async fn drain_waits_for_running_tasks() {
let tracker = TaskTracker::new("test");
let done = Arc::new(AtomicBool::new(false));
let flag = done.clone();
assert!(tracker.spawn("slow", async move {
tokio::time::sleep(Duration::from_millis(100)).await;
flag.store(true, Ordering::SeqCst);
}));
assert_eq!(tracker.drain(Duration::from_secs(5)).await, 0);
assert!(done.load(Ordering::SeqCst), "drain must wait for the task");
}
#[tokio::test]
async fn drain_aborts_tasks_that_outlive_the_grace_period() {
let tracker = TaskTracker::new("test");
let finished = Arc::new(AtomicBool::new(false));
let flag = finished.clone();
tracker.spawn("stuck", async move {
tokio::time::sleep(Duration::from_secs(60)).await;
flag.store(true, Ordering::SeqCst);
});
assert_eq!(tracker.drain(Duration::from_millis(50)).await, 1);
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(!finished.load(Ordering::SeqCst));
}
#[tokio::test]
async fn closed_tracker_rejects_new_tasks() {
let tracker = TaskTracker::new("test");
tracker.close();
let ran = Arc::new(AtomicUsize::new(0));
let counter = ran.clone();
assert!(!tracker.spawn("late", async move {
counter.fetch_add(1, Ordering::SeqCst);
}));
tokio::time::sleep(Duration::from_millis(20)).await;
assert_eq!(ran.load(Ordering::SeqCst), 0);
}
#[tokio::test]
async fn abort_all_stops_tasks() {
let tracker = TaskTracker::new("test");
let finished = Arc::new(AtomicBool::new(false));
let flag = finished.clone();
tracker.spawn("loop", async move {
tokio::time::sleep(Duration::from_secs(60)).await;
flag.store(true, Ordering::SeqCst);
});
tracker.abort_all().await;
assert_eq!(tracker.len(), 0);
assert!(!finished.load(Ordering::SeqCst));
}
#[tokio::test]
async fn finished_tasks_are_reaped_on_spawn() {
let tracker = TaskTracker::new("test");
for _ in 0..20 {
tracker.spawn("quick", async {});
tokio::time::sleep(Duration::from_millis(5)).await;
}
assert!(tracker.len() < 20, "finished tasks should be reaped");
}
#[tokio::test]
async fn panicking_task_does_not_break_the_tracker() {
let tracker = TaskTracker::new("test");
tracker.spawn("boom", async {
panic!("expected test panic");
});
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(tracker.panic_count(), 1);
let ok = Arc::new(AtomicBool::new(false));
let flag = ok.clone();
tracker.spawn("after", async move {
flag.store(true, Ordering::SeqCst);
});
assert_eq!(tracker.drain(Duration::from_secs(5)).await, 0);
assert!(ok.load(Ordering::SeqCst));
}
}