use std::sync::atomic::{AtomicU8, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum LifecycleState {
Starting = 0,
Ready = 1,
Draining = 2,
Stopped = 3,
}
impl LifecycleState {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Starting => "starting",
Self::Ready => "ready",
Self::Draining => "draining",
Self::Stopped => "stopped",
}
}
#[must_use]
pub const fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Self::Starting),
1 => Some(Self::Ready),
2 => Some(Self::Draining),
3 => Some(Self::Stopped),
_ => None,
}
}
#[must_use]
pub const fn is_live(self) -> bool {
!matches!(self, Self::Stopped)
}
}
pub(crate) struct StateCell(AtomicU8);
impl StateCell {
pub(crate) const fn new() -> Self {
Self(AtomicU8::new(LifecycleState::Starting as u8))
}
pub(crate) fn load(&self) -> LifecycleState {
let value = self.0.load(Ordering::Acquire);
LifecycleState::from_u8(value).unwrap_or(LifecycleState::Starting)
}
pub(crate) fn store(&self, state: LifecycleState) {
self.0.store(state as u8, Ordering::Release);
}
pub(crate) fn compare_exchange(
&self,
current: LifecycleState,
next: LifecycleState,
) -> Result<(), LifecycleState> {
match self.0.compare_exchange(
current as u8,
next as u8,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => Ok(()),
Err(actual) => Err(LifecycleState::from_u8(actual).unwrap_or(LifecycleState::Starting)),
}
}
}
#[cfg(test)]
mod tests {
use super::{LifecycleState, StateCell};
#[test]
fn state_roundtrips_through_u8() {
for state in [
LifecycleState::Starting,
LifecycleState::Ready,
LifecycleState::Draining,
LifecycleState::Stopped,
] {
assert_eq!(LifecycleState::from_u8(state as u8), Some(state));
}
assert_eq!(LifecycleState::from_u8(4), None);
assert_eq!(LifecycleState::from_u8(255), None);
}
#[test]
fn state_names_are_stable_lowercase() {
assert_eq!(LifecycleState::Starting.as_str(), "starting");
assert_eq!(LifecycleState::Ready.as_str(), "ready");
assert_eq!(LifecycleState::Draining.as_str(), "draining");
assert_eq!(LifecycleState::Stopped.as_str(), "stopped");
}
#[test]
fn liveness_is_true_except_stopped() {
assert!(LifecycleState::Starting.is_live());
assert!(LifecycleState::Ready.is_live());
assert!(LifecycleState::Draining.is_live());
assert!(!LifecycleState::Stopped.is_live());
}
#[test]
fn cell_starts_in_starting_and_transitions_atomically() {
let cell = StateCell::new();
assert_eq!(cell.load(), LifecycleState::Starting);
assert!(
cell.compare_exchange(LifecycleState::Starting, LifecycleState::Ready)
.is_ok()
);
assert_eq!(cell.load(), LifecycleState::Ready);
let err = cell
.compare_exchange(LifecycleState::Starting, LifecycleState::Ready)
.expect_err("cas from stale state must fail");
assert_eq!(err, LifecycleState::Ready);
cell.compare_exchange(LifecycleState::Ready, LifecycleState::Draining)
.expect("ready -> draining");
assert_eq!(cell.load(), LifecycleState::Draining);
cell.store(LifecycleState::Stopped);
assert_eq!(cell.load(), LifecycleState::Stopped);
}
}