use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use tokio_util::sync::{CancellationToken, WaitForCancellationFuture};
use tracing::{debug, error};
use super::app::{HealthReport, SubscriptionEnd};
#[derive(Clone, Debug, Default)]
pub(crate) struct Shutdown {
token: CancellationToken,
state: Arc<State>,
}
#[derive(Debug)]
struct State {
cancelled: AtomicBool,
stopping: AtomicBool,
failure: Mutex<Option<String>>,
health: HealthReport,
on_end: SubscriptionEnd,
}
impl Default for State {
fn default() -> Self {
Self {
cancelled: AtomicBool::new(false),
stopping: AtomicBool::new(false),
failure: Mutex::new(None),
health: HealthReport::new(),
on_end: SubscriptionEnd::default(),
}
}
}
impl Shutdown {
#[cfg(test)]
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn with_subscription_end(on_end: SubscriptionEnd) -> Self {
Self {
token: CancellationToken::new(),
state: Arc::new(State {
on_end,
..State::default()
}),
}
}
pub(crate) fn health(&self) -> &HealthReport {
&self.state.health
}
pub(crate) fn consuming(&self) -> Consuming {
self.state.health.subscription_started();
Consuming(Arc::clone(&self.state))
}
pub(crate) fn stream_ended(&self, subscription: &str) {
if self.is_cancelled() || self.state.stopping.load(Ordering::Relaxed) {
return;
}
match self.state.on_end {
SubscriptionEnd::Log => debug!(
target: "ruststream::dispatch",
subscription = %subscription,
"subscriber stream ended",
),
SubscriptionEnd::FailFast => {
self.signal(subscription, "the subscription's stream ended");
}
}
}
pub(crate) fn begin(&self) {
self.state.stopping.store(true, Ordering::Relaxed);
}
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())
}
}
#[derive(Debug)]
pub(crate) struct Consuming(Arc<State>);
impl Drop for Consuming {
fn drop(&mut self) {
self.0.health.subscription_ended();
}
}
#[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);
}
}