use std::sync::Arc;
use super::hook::{DrainError, DrainHook, ShutdownHooks};
use super::ready::ReadinessChecks;
use super::state::{LifecycleState, StateCell};
#[derive(Clone)]
pub struct Lifecycle {
inner: Arc<Inner>,
}
struct Inner {
state: StateCell,
readiness: ReadinessChecks,
hooks: ShutdownHooks,
}
impl Lifecycle {
#[must_use]
pub fn new() -> Self {
Lifecycle {
inner: Arc::new(Inner {
state: StateCell::new(),
readiness: ReadinessChecks::default(),
hooks: ShutdownHooks::default(),
}),
}
}
#[must_use]
pub fn state(&self) -> LifecycleState {
self.inner.state.load()
}
#[must_use]
pub fn live(&self) -> bool {
self.state().is_live()
}
#[must_use]
pub fn ready(&self) -> bool {
self.state() == LifecycleState::Ready && self.inner.readiness.all_pass()
}
#[must_use]
pub fn register_readiness<F>(self, check: F) -> Self
where
F: Fn() -> bool + Send + Sync + 'static,
{
self.inner.readiness.register(std::sync::Arc::new(check));
self
}
#[must_use]
pub fn register_drain_hook<H>(self, hook: H) -> Self
where
H: DrainHook + 'static,
{
self.inner.hooks.register(std::sync::Arc::new(hook));
self
}
pub fn mark_ready(&self) {
let _ = self
.inner
.state
.compare_exchange(LifecycleState::Starting, LifecycleState::Ready);
}
pub fn begin_drain(&self) {
loop {
let current = self.inner.state.load();
match current {
LifecycleState::Stopped | LifecycleState::Draining => return,
LifecycleState::Starting | LifecycleState::Ready => {
if self
.inner
.state
.compare_exchange(current, LifecycleState::Draining)
.is_ok()
{
return;
}
}
}
}
}
pub fn mark_stopped(&self) {
self.inner.state.store(LifecycleState::Stopped);
}
#[cfg(feature = "macros")]
pub async fn run_drain_hooks(&self) -> Vec<DrainError> {
self.inner.hooks.run().await
}
pub async fn run_drain_hooks_sequential(&self) -> Vec<DrainError> {
self.inner.hooks.run_sequential().await
}
}
impl Default for Lifecycle {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for Lifecycle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Lifecycle")
.field("state", &self.state())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::Lifecycle;
use super::LifecycleState;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[test]
fn new_lifecycle_starts_in_starting_not_ready() {
let lifecycle = Lifecycle::new();
assert_eq!(lifecycle.state(), LifecycleState::Starting);
assert!(lifecycle.live());
assert!(!lifecycle.ready());
}
#[test]
fn mark_ready_transitions_to_ready() {
let lifecycle = Lifecycle::new();
lifecycle.mark_ready();
assert_eq!(lifecycle.state(), LifecycleState::Ready);
assert!(lifecycle.live());
assert!(lifecycle.ready());
}
#[test]
fn readiness_check_gates_ready() {
let flag = Arc::new(AtomicBool::new(false));
let flag_for_check = flag.clone();
let lifecycle =
Lifecycle::new().register_readiness(move || flag_for_check.load(Ordering::SeqCst));
lifecycle.mark_ready();
assert!(!lifecycle.ready());
flag.store(true, Ordering::SeqCst);
assert!(lifecycle.ready());
}
#[test]
fn begin_drain_makes_not_ready_immediately() {
let lifecycle = Lifecycle::new();
lifecycle.mark_ready();
assert!(lifecycle.ready());
lifecycle.begin_drain();
assert_eq!(lifecycle.state(), LifecycleState::Draining);
assert!(lifecycle.live());
assert!(!lifecycle.ready());
}
#[test]
fn begin_drain_during_startup_is_safe() {
let lifecycle = Lifecycle::new();
lifecycle.begin_drain();
assert_eq!(lifecycle.state(), LifecycleState::Draining);
lifecycle.mark_ready();
assert_eq!(lifecycle.state(), LifecycleState::Draining);
assert!(!lifecycle.ready());
}
#[test]
fn mark_stopped_is_terminal_and_idempotent() {
let lifecycle = Lifecycle::new();
lifecycle.mark_ready();
lifecycle.begin_drain();
lifecycle.mark_stopped();
assert_eq!(lifecycle.state(), LifecycleState::Stopped);
assert!(!lifecycle.live());
assert!(!lifecycle.ready());
lifecycle.mark_stopped();
assert_eq!(lifecycle.state(), LifecycleState::Stopped);
}
#[tokio::test]
#[cfg(feature = "macros")]
async fn drain_hooks_run_and_report_errors() {
use std::sync::atomic::AtomicUsize;
use std::time::Duration;
use super::DrainHook;
struct CountingHook {
name: &'static str,
counter: Arc<AtomicUsize>,
fail: bool,
}
impl DrainHook for CountingHook {
fn name(&self) -> &'static str {
self.name
}
fn drain(
&self,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), String>> + Send + '_>>
{
let counter = self.counter.clone();
let fail = self.fail;
Box::pin(async move {
tokio::time::sleep(Duration::from_millis(5)).await;
counter.fetch_add(1, Ordering::SeqCst);
if fail {
Err("simulated drain failure".to_owned())
} else {
Ok(())
}
})
}
}
let counter = Arc::new(AtomicUsize::new(0));
let lifecycle = Lifecycle::new()
.register_drain_hook(CountingHook {
name: "ok-hook",
counter: counter.clone(),
fail: false,
})
.register_drain_hook(CountingHook {
name: "fail-hook",
counter: counter.clone(),
fail: true,
});
let errors = lifecycle.run_drain_hooks().await;
assert_eq!(counter.load(Ordering::SeqCst), 2);
assert_eq!(errors.len(), 1);
assert!(matches!(
errors[0],
super::DrainError::Hook {
name: "fail-hook",
..
}
));
}
#[test]
fn clone_shares_state() {
let lifecycle = Lifecycle::new();
let clone = lifecycle.clone();
lifecycle.mark_ready();
assert_eq!(clone.state(), LifecycleState::Ready);
assert!(clone.ready());
}
}