use std::time::Duration;
use tokio_util::sync::CancellationToken;
#[derive(Debug, Clone, Default)]
pub struct Shutdown {
token: CancellationToken,
}
impl Shutdown {
#[must_use]
pub fn new() -> Self {
Self {
token: CancellationToken::new(),
}
}
#[must_use]
pub fn watcher(&self) -> Watcher {
Watcher {
token: self.token.clone(),
}
}
pub fn stop(&self) {
if !self.token.is_cancelled() {
log_requested();
}
self.token.cancel();
}
#[must_use]
pub fn is_stopping(&self) -> bool {
self.token.is_cancelled()
}
pub fn listen_for_signals(&self) {
let shutdown = self.clone();
tokio::spawn(async move {
wait_for_signal().await;
shutdown.stop();
});
}
}
#[derive(Debug, Clone)]
pub struct Watcher {
token: CancellationToken,
}
impl Watcher {
#[must_use]
pub fn is_stopping(&self) -> bool {
self.token.is_cancelled()
}
#[must_use]
pub fn is_running(&self) -> bool {
!self.token.is_cancelled()
}
pub async fn wait(&self) {
self.token.cancelled().await;
}
pub async fn sleep(&self, duration: Duration) -> bool {
tokio::select! {
() = tokio::time::sleep(duration) => true,
() = self.token.cancelled() => false,
}
}
}
#[cfg(unix)]
async fn wait_for_signal() {
use tokio::signal::unix::{SignalKind, signal};
let mut terminate = match signal(SignalKind::terminate()) {
Ok(stream) => stream,
Err(error) => {
log_no_handler("SIGTERM", &error);
return;
}
};
let mut interrupt = match signal(SignalKind::interrupt()) {
Ok(stream) => stream,
Err(error) => {
log_no_handler("SIGINT", &error);
return;
}
};
tokio::select! {
_ = terminate.recv() => log_signal("SIGTERM"),
_ = interrupt.recv() => log_signal("SIGINT"),
}
}
#[cfg(not(unix))]
async fn wait_for_signal() {
if let Err(error) = tokio::signal::ctrl_c().await {
log_no_handler("Ctrl-C", &error);
}
}
#[cfg(feature = "tracing")]
fn log_requested() {
tracing::info!("shutdown requested");
}
#[cfg(not(feature = "tracing"))]
fn log_requested() {}
#[cfg(feature = "tracing")]
fn log_signal(name: &str) {
tracing::info!("{name} received");
}
#[cfg(not(feature = "tracing"))]
fn log_signal(_name: &str) {}
#[cfg(feature = "tracing")]
fn log_no_handler(name: &str, error: &std::io::Error) {
tracing::error!(%error, "could not listen for {name}");
}
#[cfg(not(feature = "tracing"))]
fn log_no_handler(_name: &str, _error: &std::io::Error) {}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_new_shutdown_is_not_stopping() {
let shutdown = Shutdown::new();
assert!(!shutdown.is_stopping());
assert!(shutdown.watcher().is_running());
}
#[tokio::test]
async fn stopping_is_visible_to_every_watcher() {
let shutdown = Shutdown::new();
let first = shutdown.watcher();
let second = shutdown.watcher();
shutdown.stop();
assert!(first.is_stopping());
assert!(second.is_stopping());
assert!(!first.is_running());
}
#[tokio::test]
async fn stopping_twice_is_harmless() {
let shutdown = Shutdown::new();
shutdown.stop();
shutdown.stop();
assert!(shutdown.is_stopping());
}
#[tokio::test]
async fn dropping_a_shutdown_does_not_stop_anything() {
let watcher = {
let shutdown = Shutdown::new();
shutdown.watcher()
};
assert!(watcher.is_running());
}
#[tokio::test(start_paused = true)]
async fn a_sleep_runs_to_completion_when_nothing_stops_it() {
let shutdown = Shutdown::new();
let watcher = shutdown.watcher();
assert!(watcher.sleep(Duration::from_secs(900)).await);
}
#[tokio::test(start_paused = true)]
async fn a_sleep_is_cut_short_by_a_stop() {
let shutdown = Shutdown::new();
let watcher = shutdown.watcher();
let sleeping = tokio::spawn(async move { watcher.sleep(Duration::from_secs(900)).await });
tokio::task::yield_now().await;
shutdown.stop();
assert!(
!sleeping.await.expect("the sleeping task"),
"the sleep should report having been cut short"
);
}
#[tokio::test]
async fn waiting_returns_at_once_when_already_stopping() {
let shutdown = Shutdown::new();
let watcher = shutdown.watcher();
shutdown.stop();
watcher.wait().await;
}
#[tokio::test(start_paused = true)]
async fn waiting_resolves_when_the_stop_arrives() {
let shutdown = Shutdown::new();
let watcher = shutdown.watcher();
let waiting = tokio::spawn(async move { watcher.wait().await });
tokio::task::yield_now().await;
shutdown.stop();
waiting.await.expect("the waiting task");
}
#[tokio::test]
async fn installing_signal_handlers_does_not_stop_anything_by_itself() {
let shutdown = Shutdown::new();
shutdown.listen_for_signals();
tokio::task::yield_now().await;
assert!(!shutdown.is_stopping());
}
}