use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use tokio_util::sync::{CancellationToken, WaitForCancellationFuture};
use tracing::error;
#[derive(Clone, Debug, Default)]
pub(crate) struct Shutdown {
token: CancellationToken,
state: Arc<State>,
}
#[derive(Debug, Default)]
struct State {
cancelled: AtomicBool,
failure: Mutex<Option<String>>,
}
impl Shutdown {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn cancel(&self) {
self.state.cancelled.store(true, Ordering::Relaxed);
self.token.cancel();
}
pub(crate) fn signal(&self, subscription: &str, reason: &str) {
error!(
target: "ruststream::dispatch",
subscription = %subscription,
reason = %reason,
"fail-fast: a dispatch failure is tearing the service down",
);
if let Ok(mut slot) = self.state.failure.lock() {
slot.get_or_insert_with(|| format!("{subscription}: {reason}"));
}
self.cancel();
}
pub(crate) fn is_cancelled(&self) -> bool {
self.state.cancelled.load(Ordering::Relaxed)
}
pub(crate) fn cancelled(&self) -> WaitForCancellationFuture<'_> {
self.token.cancelled()
}
pub(crate) fn token(&self) -> &CancellationToken {
&self.token
}
pub(crate) fn taken_failure(&self) -> Option<String> {
self.state
.failure
.lock()
.ok()
.and_then(|mut slot| slot.take())
}
pub(crate) fn peek_failure(&self) -> Option<String> {
self.state.failure.lock().ok().and_then(|slot| slot.clone())
}
}
#[cfg(test)]
mod tests {
use super::Shutdown;
#[tokio::test]
async fn the_flag_and_the_wait_never_disagree() {
let shutdown = Shutdown::new();
assert!(!shutdown.is_cancelled());
let watcher = shutdown.clone();
shutdown.cancel();
assert!(watcher.is_cancelled(), "the flag answers every holder");
watcher.cancelled().await;
}
#[test]
fn the_first_failure_is_the_one_reported() {
let shutdown = Shutdown::new();
assert!(!shutdown.is_cancelled());
shutdown.signal("orders.inbound", "handler panicked");
assert!(shutdown.is_cancelled());
shutdown.signal("other", "second");
assert_eq!(
shutdown.peek_failure().as_deref(),
Some("orders.inbound: handler panicked")
);
assert_eq!(
shutdown.taken_failure().as_deref(),
Some("orders.inbound: handler panicked")
);
assert_eq!(shutdown.taken_failure(), None);
}
}