mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
231 lines
6.4 KiB
Rust
231 lines
6.4 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
#[repr(u32)]
|
|
pub enum State {
|
|
Idle = 0,
|
|
Starting = 1,
|
|
Running = 2,
|
|
Stopping = 3,
|
|
Stopped = 4,
|
|
}
|
|
|
|
impl State {
|
|
fn from_u32(v: u32) -> State {
|
|
match v {
|
|
0 => State::Idle,
|
|
1 => State::Starting,
|
|
2 => State::Running,
|
|
3 => State::Stopping,
|
|
_ => State::Stopped,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
pub enum TransitionError {
|
|
IllegalTransition,
|
|
DoubleStart,
|
|
StopBeforeStart,
|
|
StartWhileStopping,
|
|
}
|
|
|
|
pub fn is_allowed(from: State, to: State) -> bool {
|
|
match from {
|
|
State::Idle => matches!(to, State::Starting | State::Stopped),
|
|
State::Starting => matches!(to, State::Running | State::Stopped),
|
|
State::Running => matches!(to, State::Stopping | State::Stopped),
|
|
State::Stopping => matches!(to, State::Stopped),
|
|
State::Stopped => false,
|
|
}
|
|
}
|
|
|
|
pub struct Machine {
|
|
state: AtomicU32,
|
|
}
|
|
|
|
impl Machine {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
state: AtomicU32::new(State::Idle as u32),
|
|
}
|
|
}
|
|
|
|
pub fn current(&self) -> State {
|
|
State::from_u32(self.state.load(Ordering::Acquire))
|
|
}
|
|
|
|
fn cas(&self, from: State, to: State) -> Result<(), TransitionError> {
|
|
if !is_allowed(from, to) {
|
|
return Err(TransitionError::IllegalTransition);
|
|
}
|
|
self.state
|
|
.compare_exchange(from as u32, to as u32, Ordering::AcqRel, Ordering::Acquire)
|
|
.map(|_| ())
|
|
.map_err(|_| TransitionError::IllegalTransition)
|
|
}
|
|
|
|
pub fn request_start(&self) -> Result<(), TransitionError> {
|
|
match self.current() {
|
|
State::Idle => self.cas(State::Idle, State::Starting),
|
|
State::Starting | State::Running => Err(TransitionError::DoubleStart),
|
|
State::Stopping => Err(TransitionError::StartWhileStopping),
|
|
State::Stopped => Err(TransitionError::IllegalTransition),
|
|
}
|
|
}
|
|
|
|
pub fn mark_running(&self) -> Result<(), TransitionError> {
|
|
self.cas(State::Starting, State::Running)
|
|
}
|
|
|
|
pub fn request_stop(&self) -> Result<(), TransitionError> {
|
|
match self.current() {
|
|
State::Running => self.cas(State::Running, State::Stopping),
|
|
State::Idle => Err(TransitionError::StopBeforeStart),
|
|
State::Starting | State::Stopping | State::Stopped => {
|
|
Err(TransitionError::IllegalTransition)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn mark_stopped(&self) -> Result<(), TransitionError> {
|
|
self.cas(State::Stopping, State::Stopped)
|
|
}
|
|
|
|
pub fn cancel_idle(&self) -> Result<(), TransitionError> {
|
|
self.cas(State::Idle, State::Stopped)
|
|
}
|
|
|
|
pub fn mark_fatal(&self) -> State {
|
|
loop {
|
|
let raw = self.state.load(Ordering::Acquire);
|
|
let prev = State::from_u32(raw);
|
|
if prev == State::Stopped {
|
|
return prev;
|
|
}
|
|
if self
|
|
.state
|
|
.compare_exchange(
|
|
raw,
|
|
State::Stopped as u32,
|
|
Ordering::AcqRel,
|
|
Ordering::Acquire,
|
|
)
|
|
.is_ok()
|
|
{
|
|
return prev;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Machine {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn is_allowed_exhaustive() {
|
|
let all = [
|
|
State::Idle,
|
|
State::Starting,
|
|
State::Running,
|
|
State::Stopping,
|
|
State::Stopped,
|
|
];
|
|
let allowed: &[(State, State)] = &[
|
|
(State::Idle, State::Starting),
|
|
(State::Idle, State::Stopped),
|
|
(State::Starting, State::Running),
|
|
(State::Starting, State::Stopped),
|
|
(State::Running, State::Stopping),
|
|
(State::Running, State::Stopped),
|
|
(State::Stopping, State::Stopped),
|
|
];
|
|
for &from in &all {
|
|
for &to in &all {
|
|
let expected = allowed.iter().any(|p| p.0 == from && p.1 == to);
|
|
assert_eq!(expected, is_allowed(from, to), "{:?} -> {:?}", from, to);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn happy_path() {
|
|
let m = Machine::new();
|
|
assert_eq!(State::Idle, m.current());
|
|
m.request_start().unwrap();
|
|
assert_eq!(State::Starting, m.current());
|
|
m.mark_running().unwrap();
|
|
assert_eq!(State::Running, m.current());
|
|
m.request_stop().unwrap();
|
|
assert_eq!(State::Stopping, m.current());
|
|
m.mark_stopped().unwrap();
|
|
assert_eq!(State::Stopped, m.current());
|
|
}
|
|
|
|
#[test]
|
|
fn double_start_rejected() {
|
|
let m = Machine::new();
|
|
m.request_start().unwrap();
|
|
assert_eq!(Err(TransitionError::DoubleStart), m.request_start());
|
|
m.mark_running().unwrap();
|
|
assert_eq!(Err(TransitionError::DoubleStart), m.request_start());
|
|
}
|
|
|
|
#[test]
|
|
fn stop_before_start_rejected() {
|
|
let m = Machine::new();
|
|
assert_eq!(Err(TransitionError::StopBeforeStart), m.request_stop());
|
|
}
|
|
|
|
#[test]
|
|
fn start_while_stopping_rejected() {
|
|
let m = Machine::new();
|
|
m.request_start().unwrap();
|
|
m.mark_running().unwrap();
|
|
m.request_stop().unwrap();
|
|
assert_eq!(Err(TransitionError::StartWhileStopping), m.request_start());
|
|
}
|
|
|
|
#[test]
|
|
fn cancel_idle_short_circuits() {
|
|
let m = Machine::new();
|
|
m.cancel_idle().unwrap();
|
|
assert_eq!(State::Stopped, m.current());
|
|
assert_eq!(Err(TransitionError::IllegalTransition), m.request_start());
|
|
}
|
|
|
|
#[test]
|
|
fn mark_fatal_forces_stopped() {
|
|
for &start in &[
|
|
State::Idle,
|
|
State::Starting,
|
|
State::Running,
|
|
State::Stopping,
|
|
] {
|
|
let m = Machine::new();
|
|
m.state.store(start as u32, Ordering::Release);
|
|
let prev = m.mark_fatal();
|
|
assert_eq!(start, prev);
|
|
assert_eq!(State::Stopped, m.current());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mark_fatal_idempotent() {
|
|
let m = Machine::new();
|
|
m.state.store(State::Stopped as u32, Ordering::Release);
|
|
let prev = m.mark_fatal();
|
|
assert_eq!(State::Stopped, prev);
|
|
assert_eq!(State::Stopped, m.current());
|
|
}
|
|
}
|