use std::future::Future;
use std::sync::LazyLock;
use std::time::Duration;
use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker;
const DEFAULT_DRAIN_TIMEOUT: Duration = Duration::from_secs(20);
static POOLS: LazyLock<WorkerPools> = LazyLock::new(WorkerPools::default);
pub(crate) fn pools() -> &'static WorkerPools {
&POOLS
}
#[derive(Default)]
pub(crate) struct WorkerPools {
shutdown: CancellationToken,
tracker: TaskTracker,
}
impl WorkerPools {
pub(crate) fn token(&self) -> CancellationToken {
self.shutdown.clone()
}
pub(crate) fn spawn<F>(&self, worker: F)
where
F: Future<Output = ()> + Send + 'static,
{
self.tracker.spawn(worker);
}
pub(crate) fn begin_shutdown(&self) {
self.shutdown.cancel();
}
pub(crate) async fn drain(&self, timeout: Duration) -> bool {
self.begin_shutdown();
self.tracker.close();
tokio::time::timeout(timeout, self.tracker.wait())
.await
.is_ok()
}
pub(crate) fn running(&self) -> usize {
self.tracker.len()
}
}
pub(crate) fn drain_timeout() -> Duration {
parse_drain_timeout(std::env::var("HFS_WORKER_SHUTDOWN_TIMEOUT").ok().as_deref())
}
fn parse_drain_timeout(value: Option<&str>) -> Duration {
match value.map(str::trim) {
None | Some("") => DEFAULT_DRAIN_TIMEOUT,
Some(raw) => match raw.parse::<u64>() {
Ok(secs) => Duration::from_secs(secs),
Err(_) => {
tracing::warn!(
value = raw,
default_secs = DEFAULT_DRAIN_TIMEOUT.as_secs(),
"HFS_WORKER_SHUTDOWN_TIMEOUT is not a whole number of seconds; using the default"
);
DEFAULT_DRAIN_TIMEOUT
}
},
}
}
pub(crate) async fn idle(shutdown: &CancellationToken, period: Duration) -> bool {
tokio::select! {
biased;
_ = shutdown.cancelled() => true,
_ = tokio::time::sleep(period) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn drain_timeout_defaults_and_parses_seconds() {
assert_eq!(parse_drain_timeout(None), DEFAULT_DRAIN_TIMEOUT);
assert_eq!(parse_drain_timeout(Some(" ")), DEFAULT_DRAIN_TIMEOUT);
assert_eq!(parse_drain_timeout(Some("5")), Duration::from_secs(5));
assert_eq!(parse_drain_timeout(Some("0")), Duration::ZERO);
assert_eq!(parse_drain_timeout(Some("soon")), DEFAULT_DRAIN_TIMEOUT);
}
#[tokio::test]
async fn drain_stops_idle_workers_and_waits_for_them() {
let pools = WorkerPools::default();
for _ in 0..3 {
let shutdown = pools.token();
pools.spawn(async move { while !idle(&shutdown, Duration::from_secs(3600)).await {} });
}
assert_eq!(pools.running(), 3);
assert!(pools.drain(Duration::from_secs(5)).await);
assert_eq!(pools.running(), 0);
}
#[tokio::test]
async fn drain_gives_up_on_a_worker_that_outlives_the_deadline() {
let pools = WorkerPools::default();
pools.spawn(std::future::pending());
assert!(!pools.drain(Duration::from_millis(50)).await);
assert_eq!(pools.running(), 1);
}
}