use std::sync::{
Arc,
atomic::{AtomicBool, AtomicU8, Ordering},
};
use tokio::sync::Notify;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShutdownKind {
Graceful,
Immediate,
Reload,
}
impl ShutdownKind {
const NONE: u8 = 0;
const GRACEFUL: u8 = 1;
const IMMEDIATE: u8 = 2;
const RELOAD: u8 = 3;
fn to_u8(self) -> u8 {
match self {
ShutdownKind::Graceful => Self::GRACEFUL,
ShutdownKind::Immediate => Self::IMMEDIATE,
ShutdownKind::Reload => Self::RELOAD,
}
}
fn from_u8(v: u8) -> Option<Self> {
match v {
Self::NONE => None,
Self::GRACEFUL => Some(ShutdownKind::Graceful),
Self::IMMEDIATE => Some(ShutdownKind::Immediate),
Self::RELOAD => Some(ShutdownKind::Reload),
_ => None,
}
}
pub fn causes_drain(self) -> bool {
matches!(self, ShutdownKind::Graceful | ShutdownKind::Immediate)
}
}
struct Inner {
draining: AtomicBool,
kind: AtomicU8,
drain_notify: Notify,
kind_notify: Notify,
}
#[derive(Clone)]
pub struct DrainController {
inner: Arc<Inner>,
}
impl Default for DrainController {
fn default() -> Self {
Self::new()
}
}
impl DrainController {
#[must_use]
pub fn new() -> Self {
Self {
inner: Arc::new(Inner {
draining: AtomicBool::new(false),
kind: AtomicU8::new(ShutdownKind::NONE),
drain_notify: Notify::new(),
kind_notify: Notify::new(),
}),
}
}
pub fn kind(&self) -> Option<ShutdownKind> {
ShutdownKind::from_u8(self.inner.kind.load(Ordering::Acquire))
}
pub fn is_draining(&self) -> bool {
self.inner.draining.load(Ordering::Acquire)
}
pub async fn wait_for_signal(&self) -> ShutdownKind {
loop {
if let Some(k) = self.kind() {
return k;
}
let notified = self.inner.kind_notify.notified();
if let Some(k) = self.kind() {
return k;
}
notified.await;
}
}
pub async fn wait_for_drain(&self) -> ShutdownKind {
loop {
if self.is_draining() {
return self.kind().unwrap_or(ShutdownKind::Graceful);
}
let notified = self.inner.drain_notify.notified();
if self.is_draining() {
return self.kind().unwrap_or(ShutdownKind::Graceful);
}
notified.await;
}
}
pub fn begin_drain(&self, kind: ShutdownKind) {
self.inner.kind.store(kind.to_u8(), Ordering::Release);
self.inner.kind_notify.notify_waiters();
if kind.causes_drain() {
self.inner.draining.store(true, Ordering::Release);
self.inner.drain_notify.notify_waiters();
}
}
pub fn clear_drain(&self) {
self.inner.draining.store(false, Ordering::Release);
self.inner.kind.store(ShutdownKind::NONE, Ordering::Release);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn controller_starts_inactive() {
let c = DrainController::new();
assert_eq!(c.kind(), None);
assert!(!c.is_draining());
}
#[test]
fn begin_drain_sets_state() {
let c = DrainController::new();
c.begin_drain(ShutdownKind::Graceful);
assert!(c.is_draining());
assert_eq!(c.kind(), Some(ShutdownKind::Graceful));
}
#[test]
fn reload_does_not_drain() {
let c = DrainController::new();
c.begin_drain(ShutdownKind::Reload);
assert!(!c.is_draining());
assert_eq!(c.kind(), Some(ShutdownKind::Reload));
c.clear_drain();
assert_eq!(c.kind(), None);
}
#[tokio::test]
async fn wait_for_drain_unblocks_after_begin() {
let c = DrainController::new();
let c2 = c.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
c2.begin_drain(ShutdownKind::Immediate);
});
let k = c.wait_for_drain().await;
assert_eq!(k, ShutdownKind::Immediate);
}
#[tokio::test]
async fn wait_for_signal_returns_reload() {
let c = DrainController::new();
let c2 = c.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
c2.begin_drain(ShutdownKind::Reload);
});
let k = c.wait_for_signal().await;
assert_eq!(k, ShutdownKind::Reload);
assert!(!c.is_draining());
}
}