use std::sync::Arc;
use parking_lot::RwLock;
use tokio_util::sync::CancellationToken;
#[derive(Clone, Debug)]
pub struct ShutdownToken {
inner: CancellationToken,
reason: Arc<RwLock<Option<ShutdownReason>>>,
}
impl ShutdownToken {
pub fn new() -> Self {
Self {
inner: CancellationToken::new(),
reason: Arc::new(RwLock::new(None)),
}
}
pub fn is_cancelled(&self) -> bool {
self.inner.is_cancelled()
}
pub async fn cancelled(&self) {
self.inner.cancelled().await
}
pub fn cancel(&self, reason: ShutdownReason) {
{
let mut w = self.reason.write();
if w.is_none() {
*w = Some(reason);
}
}
self.inner.cancel();
}
pub fn reason(&self) -> Option<ShutdownReason> {
self.reason.read().clone()
}
pub fn child_token(&self) -> ShutdownToken {
Self {
inner: self.inner.child_token(),
reason: self.reason.clone(),
}
}
}
impl Default for ShutdownToken {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub enum ShutdownReason {
UserRequested,
RpcRequested,
ReloadRequested,
RequestedByRun,
Fatal(String),
}
#[derive(Clone, Debug)]
pub enum ExitReason {
RequestedShutdown(ShutdownReason),
RunCompleted,
RunError(Arc<anyhow::Error>),
}
#[derive(Clone, Debug)]
pub struct ExitStatus {
pub reason: ExitReason,
}
impl ExitStatus {
pub fn is_graceful(&self) -> bool {
!matches!(
self.reason,
ExitReason::RunError(_) | ExitReason::RequestedShutdown(ShutdownReason::Fatal(_))
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fresh_token_is_uncancelled() {
let t = ShutdownToken::new();
assert!(!t.is_cancelled());
assert!(t.reason().is_none());
}
#[test]
fn cancel_sets_state() {
let t = ShutdownToken::new();
t.cancel(ShutdownReason::UserRequested);
assert!(t.is_cancelled());
assert!(matches!(t.reason(), Some(ShutdownReason::UserRequested)));
}
#[test]
fn cancel_reason_is_first_wins() {
let t = ShutdownToken::new();
t.cancel(ShutdownReason::UserRequested);
t.cancel(ShutdownReason::Fatal("too late".to_string()));
assert!(matches!(t.reason(), Some(ShutdownReason::UserRequested)));
}
#[test]
fn clone_shares_state() {
let a = ShutdownToken::new();
let b = a.clone();
assert!(!a.is_cancelled() && !b.is_cancelled());
b.cancel(ShutdownReason::RpcRequested);
assert!(a.is_cancelled() && b.is_cancelled());
assert!(matches!(a.reason(), Some(ShutdownReason::RpcRequested)));
}
#[test]
fn exit_status_is_graceful_classifies_correctly() {
let graceful = ExitStatus {
reason: ExitReason::RunCompleted,
};
assert!(graceful.is_graceful());
let graceful = ExitStatus {
reason: ExitReason::RequestedShutdown(ShutdownReason::UserRequested),
};
assert!(graceful.is_graceful());
let fatal = ExitStatus {
reason: ExitReason::RequestedShutdown(ShutdownReason::Fatal("x".into())),
};
assert!(!fatal.is_graceful());
let run_err = ExitStatus {
reason: ExitReason::RunError(Arc::new(anyhow::anyhow!("boom"))),
};
assert!(!run_err.is_graceful());
}
}