use alloc::sync::Arc;
use core::fmt;
use core::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
use core::time::Duration;
use tokio::sync::{Notify, watch};
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DrainReason {
Signal = 1,
Unready = 2,
Operator = 3,
Fatal = 4,
}
impl DrainReason {
#[must_use]
pub const fn as_label(self) -> &'static str {
match self {
Self::Signal => "signal",
Self::Unready => "unready",
Self::Operator => "operator",
Self::Fatal => "fatal",
}
}
const fn from_u8(value: u8) -> Option<Self> {
match value {
1 => Some(Self::Signal),
2 => Some(Self::Unready),
3 => Some(Self::Operator),
4 => Some(Self::Fatal),
_ => None,
}
}
}
impl fmt::Display for DrainReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_label())
}
}
#[derive(Clone, Debug)]
pub struct Shutdown {
token: CancellationToken,
reason: Arc<AtomicU8>,
}
impl Shutdown {
#[must_use]
pub fn new() -> Self {
Self {
token: CancellationToken::new(),
reason: Arc::new(AtomicU8::new(0)),
}
}
#[must_use]
pub fn child(&self) -> Self {
Self {
token: self.token.child_token(),
reason: Arc::clone(&self.reason),
}
}
pub fn trigger(&self, reason: DrainReason) {
let _ = self
.reason
.compare_exchange(0, reason as u8, Ordering::AcqRel, Ordering::Acquire);
self.token.cancel();
}
#[must_use]
pub fn is_triggered(&self) -> bool {
self.token.is_cancelled()
}
#[must_use]
pub fn reason(&self) -> Option<DrainReason> {
if self.token.is_cancelled() {
DrainReason::from_u8(self.reason.load(Ordering::Acquire))
} else {
None
}
}
pub async fn triggered(&self) {
self.token.cancelled().await;
}
#[must_use]
pub fn from_signals() -> Self {
let shutdown = Self::new();
let handle = shutdown.clone();
tokio::spawn(async move {
if wait_for_signal().await {
handle.trigger(DrainReason::Signal);
}
});
shutdown
}
}
impl Default for Shutdown {
fn default() -> Self {
Self::new()
}
}
#[cfg(unix)]
async fn wait_for_signal() -> bool {
use tokio::signal::unix::{SignalKind, signal};
let mut terminate = signal(SignalKind::terminate()).expect("install SIGTERM handler");
let mut interrupt = signal(SignalKind::interrupt()).expect("install SIGINT handler");
tokio::select! {
_ = terminate.recv() => {}
_ = interrupt.recv() => {}
}
true
}
#[cfg(not(unix))]
async fn wait_for_signal() -> bool {
tokio::signal::ctrl_c().await.is_ok()
}
#[derive(Clone, Default)]
pub struct Drain {
inner: Arc<DrainInner>,
}
#[derive(Default)]
struct DrainInner {
count: AtomicUsize,
idle: Notify,
}
impl Drain {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use = "dropping the guard immediately ends the work it should track"]
pub fn begin(&self) -> InFlight {
self.inner.count.fetch_add(1, Ordering::AcqRel);
InFlight {
inner: Arc::clone(&self.inner),
}
}
#[must_use]
pub fn outstanding(&self) -> usize {
self.inner.count.load(Ordering::Acquire)
}
pub async fn quiesce(&self, grace: Duration) -> QuiesceOutcome {
let deadline = Instant::now() + grace;
loop {
if self.outstanding() == 0 {
return QuiesceOutcome::Drained;
}
let idle = self.inner.idle.notified();
tokio::pin!(idle);
idle.as_mut().enable();
if self.outstanding() == 0 {
return QuiesceOutcome::Drained;
}
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return QuiesceOutcome::TimedOut {
remaining: self.outstanding(),
};
}
if tokio::time::timeout(remaining, idle).await.is_err() {
return QuiesceOutcome::TimedOut {
remaining: self.outstanding(),
};
}
}
}
}
#[must_use = "the guard must live for the duration of the work it tracks"]
pub struct InFlight {
inner: Arc<DrainInner>,
}
impl Drop for InFlight {
fn drop(&mut self) {
if self.inner.count.fetch_sub(1, Ordering::AcqRel) == 1 {
self.inner.idle.notify_waiters();
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum QuiesceOutcome {
Drained,
TimedOut { remaining: usize },
}
pub struct Readiness;
impl Readiness {
#[allow(clippy::new_ret_no_self)]
#[must_use]
pub fn new() -> (ReadinessSetter, ReadinessWatcher) {
let (tx, rx) = watch::channel(ReadyState::Starting);
(ReadinessSetter { tx }, ReadinessWatcher { rx })
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ReadyState {
Starting,
Ready,
Draining,
Failed,
}
impl fmt::Display for ReadyState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_label())
}
}
impl ReadyState {
#[must_use]
pub const fn as_label(self) -> &'static str {
match self {
Self::Starting => "starting",
Self::Ready => "ready",
Self::Draining => "draining",
Self::Failed => "failed",
}
}
}
#[derive(Clone)]
pub struct ReadinessSetter {
tx: watch::Sender<ReadyState>,
}
impl ReadinessSetter {
pub fn set(&self, state: ReadyState) {
self.tx.send_replace(state);
}
}
#[derive(Clone)]
pub struct ReadinessWatcher {
rx: watch::Receiver<ReadyState>,
}
impl ReadinessWatcher {
#[must_use]
pub fn current(&self) -> ReadyState {
*self.rx.borrow()
}
pub async fn changed(&mut self) -> ReadyState {
let _ = self.rx.changed().await;
*self.rx.borrow()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn drain_reason_labels_are_stable() {
assert_eq!(DrainReason::Signal.to_string(), "signal");
assert_eq!(DrainReason::Unready.to_string(), "unready");
assert_eq!(DrainReason::Operator.to_string(), "operator");
assert_eq!(DrainReason::Fatal.to_string(), "fatal");
}
#[tokio::test]
async fn trigger_is_idempotent_and_keeps_first_reason() {
let shutdown = Shutdown::new();
assert!(!shutdown.is_triggered());
assert_eq!(shutdown.reason(), None);
shutdown.trigger(DrainReason::Operator);
shutdown.trigger(DrainReason::Fatal);
assert!(shutdown.is_triggered());
assert_eq!(shutdown.reason(), Some(DrainReason::Operator));
shutdown.triggered().await;
}
#[tokio::test]
async fn child_sees_parent_trigger() {
let parent = Shutdown::new();
let child = parent.child();
assert!(!child.is_triggered());
assert_eq!(child.reason(), None);
parent.trigger(DrainReason::Signal);
assert!(child.is_triggered());
assert_eq!(child.reason(), Some(DrainReason::Signal));
child.triggered().await;
}
#[tokio::test]
async fn child_trigger_does_not_cancel_parent() {
let parent = Shutdown::new();
let child = parent.child();
child.trigger(DrainReason::Unready);
assert!(child.is_triggered());
assert!(!parent.is_triggered());
assert_eq!(parent.reason(), None);
}
#[tokio::test]
async fn quiesce_returns_immediately_when_idle() {
let drain = Drain::new();
assert_eq!(drain.outstanding(), 0);
assert_eq!(
drain.quiesce(Duration::from_secs(5)).await,
QuiesceOutcome::Drained
);
}
#[tokio::test]
async fn quiesce_drains_when_guards_drop() {
let drain = Drain::new();
let first = drain.begin();
let second = drain.begin();
assert_eq!(drain.outstanding(), 2);
let worker = drain.clone();
let task = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(10)).await;
drop(first);
tokio::time::sleep(Duration::from_millis(10)).await;
drop(second);
drop(worker);
});
assert_eq!(
drain.quiesce(Duration::from_secs(5)).await,
QuiesceOutcome::Drained
);
assert_eq!(drain.outstanding(), 0);
task.await.expect("worker task joins");
}
#[tokio::test]
async fn quiesce_times_out_with_outstanding_work() {
let drain = Drain::new();
let _guard = drain.begin();
let _other = drain.begin();
assert_eq!(
drain.quiesce(Duration::from_millis(20)).await,
QuiesceOutcome::TimedOut { remaining: 2 }
);
assert_eq!(drain.outstanding(), 2);
}
#[tokio::test]
async fn readiness_watcher_observes_transitions() {
let (setter, mut watcher) = Readiness::new();
assert_eq!(watcher.current(), ReadyState::Starting);
setter.set(ReadyState::Ready);
assert_eq!(watcher.changed().await, ReadyState::Ready);
assert_eq!(watcher.current(), ReadyState::Ready);
setter.set(ReadyState::Draining);
assert_eq!(watcher.changed().await, ReadyState::Draining);
}
}