use std::future::Future;
use tokio_util::{sync::CancellationToken, task::TaskTracker};
#[derive(Debug, Clone, Default)]
pub struct FallbackCoordinator {
signal: CancellationToken,
tasks: TaskTracker,
}
impl FallbackCoordinator {
pub fn new() -> Self {
Self::default()
}
pub fn token(&self) -> CancellationToken {
self.signal.clone()
}
pub fn track<F: Future>(&self, fut: F) -> impl Future<Output = F::Output> + use<F> {
self.tasks.track_future(fut)
}
pub async fn trigger_fallback_and_wait(&self) {
tracing::debug!(
pending = self.tasks.len(),
"FallbackCoordinator: triggering fallback"
);
self.signal.cancel();
self.tasks.close();
loop {
self.tasks.wait().await;
if self.tasks.is_empty() {
break;
}
tracing::debug!(
pending = self.tasks.len(),
"FallbackCoordinator: work tracked after the tracker emptied, waiting again"
);
}
tracing::debug!("FallbackCoordinator: all components finished fallback cleanup");
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::FutureExt as _;
use std::pin::pin;
#[tokio::test]
async fn waits_for_work_tracked_after_an_earlier_one_finished() {
let coordinator = FallbackCoordinator::new();
let first = coordinator.track(std::future::pending::<()>());
let mut wait = pin!(coordinator.trigger_fallback_and_wait());
assert!(wait.as_mut().now_or_never().is_none());
drop(first);
let second = coordinator.track(std::future::pending::<()>());
assert!(
wait.as_mut().now_or_never().is_none(),
"fallback completed while a component was still pending"
);
drop(second);
assert!(wait.as_mut().now_or_never().is_some());
}
#[tokio::test]
async fn tracked_future_counts_from_creation_until_dropped() {
let coordinator = FallbackCoordinator::new();
let never_polled = coordinator.track(std::future::pending::<()>());
let mut wait = pin!(coordinator.trigger_fallback_and_wait());
assert!(wait.as_mut().now_or_never().is_none());
drop(never_polled);
assert!(wait.as_mut().now_or_never().is_some());
}
#[tokio::test]
async fn panicking_tracked_task_is_released() {
let coordinator = FallbackCoordinator::new();
let task = tokio::spawn(coordinator.track(async { panic!("task failed") }));
assert!(task.await.unwrap_err().is_panic());
coordinator.trigger_fallback_and_wait().await;
}
}