use std::fmt;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EventLifecycleState {
Serving,
Draining,
Stopped,
}
impl EventLifecycleState {
pub const fn allows_publication(self) -> bool {
if self.is_stopped() {
return false;
}
!self.is_shutting_down()
}
pub const fn is_shutting_down(self) -> bool {
matches!(self, Self::Draining | Self::Stopped)
}
pub const fn is_stopped(self) -> bool {
matches!(self, Self::Stopped)
}
}
impl fmt::Display for EventLifecycleState {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let state = match self {
Self::Serving => "serving",
Self::Draining => "draining",
Self::Stopped => "stopped",
};
formatter.write_str(state)
}
}
#[derive(Debug)]
pub struct EventLifecycle {
state: std::sync::atomic::AtomicU8,
}
impl EventLifecycle {
const SERVING: u8 = 0;
const DRAINING: u8 = 1;
const STOPPED: u8 = 2;
pub fn new() -> Self {
Self {
state: std::sync::atomic::AtomicU8::new(Self::SERVING),
}
}
pub fn state(&self) -> EventLifecycleState {
match self.state.load(std::sync::atomic::Ordering::Acquire) {
Self::SERVING => EventLifecycleState::Serving,
Self::DRAINING => EventLifecycleState::Draining,
Self::STOPPED => EventLifecycleState::Stopped,
_ => unreachable!("EventLifecycle contains an invalid state"),
}
}
pub fn allows_publication(&self) -> bool {
self.state().allows_publication()
}
pub fn begin_draining(&self) {
let _ = self.state.compare_exchange(
Self::SERVING,
Self::DRAINING,
std::sync::atomic::Ordering::AcqRel,
std::sync::atomic::Ordering::Acquire,
);
}
pub fn stop(&self) {
self.state
.store(Self::STOPPED, std::sync::atomic::Ordering::Release);
}
}
impl Default for EventLifecycle {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lifecycle_starts_in_serving_state() {
let lifecycle = EventLifecycle::new();
assert_eq!(lifecycle.state(), EventLifecycleState::Serving);
assert!(lifecycle.allows_publication());
}
#[test]
fn serving_state_allows_publication() {
assert!(EventLifecycleState::Serving.allows_publication());
assert!(!EventLifecycleState::Serving.is_shutting_down());
assert!(!EventLifecycleState::Serving.is_stopped());
}
#[test]
fn draining_state_rejects_new_publication() {
assert!(!EventLifecycleState::Draining.allows_publication());
assert!(EventLifecycleState::Draining.is_shutting_down());
assert!(!EventLifecycleState::Draining.is_stopped());
}
#[test]
fn stopped_state_rejects_new_publication() {
assert!(!EventLifecycleState::Stopped.allows_publication());
assert!(EventLifecycleState::Stopped.is_shutting_down());
assert!(EventLifecycleState::Stopped.is_stopped());
}
#[test]
fn lifecycle_transitions_from_serving_to_draining() {
let lifecycle = EventLifecycle::new();
lifecycle.begin_draining();
assert_eq!(lifecycle.state(), EventLifecycleState::Draining);
assert!(!lifecycle.allows_publication());
}
#[test]
fn lifecycle_transitions_from_draining_to_stopped() {
let lifecycle = EventLifecycle::new();
lifecycle.begin_draining();
lifecycle.stop();
assert_eq!(lifecycle.state(), EventLifecycleState::Stopped);
assert!(!lifecycle.allows_publication());
}
#[test]
fn stopping_serving_directly_closes_publication() {
let lifecycle = EventLifecycle::new();
lifecycle.stop();
assert_eq!(lifecycle.state(), EventLifecycleState::Stopped);
assert!(!lifecycle.allows_publication());
}
#[test]
fn beginning_draining_is_idempotent() {
let lifecycle = EventLifecycle::new();
lifecycle.begin_draining();
lifecycle.begin_draining();
assert_eq!(lifecycle.state(), EventLifecycleState::Draining);
}
#[test]
fn stopping_is_idempotent() {
let lifecycle = EventLifecycle::new();
lifecycle.stop();
lifecycle.stop();
assert_eq!(lifecycle.state(), EventLifecycleState::Stopped);
}
#[test]
fn stopped_lifecycle_cannot_be_resurrected() {
let lifecycle = EventLifecycle::new();
lifecycle.stop();
lifecycle.begin_draining();
assert_eq!(lifecycle.state(), EventLifecycleState::Stopped);
assert!(!lifecycle.allows_publication());
}
#[test]
fn lifecycle_state_display_is_stable() {
assert_eq!(EventLifecycleState::Serving.to_string(), "serving");
assert_eq!(EventLifecycleState::Draining.to_string(), "draining");
assert_eq!(EventLifecycleState::Stopped.to_string(), "stopped");
}
#[test]
fn lifecycle_state_is_copy_and_eq() {
let serving = EventLifecycleState::Serving;
let copied = serving;
assert_eq!(serving, copied);
}
#[test]
fn concurrent_shutdown_is_safe() {
use std::sync::Arc;
use std::thread;
let lifecycle = Arc::new(EventLifecycle::new());
let mut handles = Vec::new();
for _ in 0..16 {
let lifecycle = Arc::clone(&lifecycle);
handles.push(thread::spawn(move || {
lifecycle.begin_draining();
lifecycle.stop();
}));
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(lifecycle.state(), EventLifecycleState::Stopped);
assert!(!lifecycle.allows_publication());
}
}