Finite state machines in rust; bendns fork to add types.
| -rw-r--r-- | examples/circuit_breaker.rs | 105 | ||||
| -rw-r--r-- | src/lib.rs | 12 | ||||
| -rw-r--r-- | src/machine.rs | 22 | ||||
| -rw-r--r-- | src/machine_wrapper.rs | 52 |
4 files changed, 184 insertions, 7 deletions
diff --git a/examples/circuit_breaker.rs b/examples/circuit_breaker.rs new file mode 100644 index 0000000..1ba15e3 --- /dev/null +++ b/examples/circuit_breaker.rs @@ -0,0 +1,105 @@ +/// A dummy implementation of the Circuit Breaker pattern to demonstrate +/// capabilities of this library. +/// https://martinfowler.com/bliki/CircuitBreaker.html +use rust_fsm::*; +use std::sync::{Arc, Mutex}; + +#[derive(Debug)] +enum CircuitBreakerInput { + Successful, + Unsuccessful, + TimerTriggered, +} + +#[derive(Debug, PartialEq)] +enum CircuitBreakerState { + Closed, + Open, + HalfOpen, +} + +#[derive(Debug, PartialEq)] +struct CircuitBreakerOutputSetTimer; + +#[derive(Debug)] +struct CircuitBreakerMachine; + +impl StateMachine for CircuitBreakerMachine { + type Input = CircuitBreakerInput; + type State = CircuitBreakerState; + type Output = CircuitBreakerOutputSetTimer; + const INITIAL_STATE: Self::State = CircuitBreakerState::Closed; + + fn transition(state: &Self::State, input: &Self::Input) -> Option<Self::State> { + match (state, input) { + (CircuitBreakerState::Closed, CircuitBreakerInput::Unsuccessful) => { + Some(CircuitBreakerState::Open) + } + (CircuitBreakerState::Open, CircuitBreakerInput::TimerTriggered) => { + Some(CircuitBreakerState::HalfOpen) + } + (CircuitBreakerState::HalfOpen, CircuitBreakerInput::Successful) => { + Some(CircuitBreakerState::Closed) + } + (CircuitBreakerState::HalfOpen, CircuitBreakerInput::Unsuccessful) => { + Some(CircuitBreakerState::Open) + } + _ => None, + } + } + + fn output(state: &Self::State, input: &Self::Input) -> Option<Self::Output> { + match (state, input) { + (CircuitBreakerState::Closed, CircuitBreakerInput::Unsuccessful) => { + Some(CircuitBreakerOutputSetTimer) + } + (CircuitBreakerState::HalfOpen, CircuitBreakerInput::Unsuccessful) => { + Some(CircuitBreakerOutputSetTimer) + } + _ => None, + } + } +} + +fn main() { + let machine: StateMachineWrapper<CircuitBreakerMachine> = StateMachineWrapper::new(); + + // Unsuccessful request + let machine = Arc::new(Mutex::new(machine)); + { + let mut lock = machine.lock().unwrap(); + let res = lock.consume_anyway(&CircuitBreakerInput::Unsuccessful); + assert_eq!(res, Some(CircuitBreakerOutputSetTimer)); + assert_eq!(lock.state(), &CircuitBreakerState::Open); + } + + // Set up a timer + let machine_wait = machine.clone(); + std::thread::spawn(move || { + std::thread::sleep_ms(5000); + let mut lock = machine_wait.lock().unwrap(); + let res = lock.consume_anyway(&CircuitBreakerInput::TimerTriggered); + assert_eq!(res, None); + assert_eq!(lock.state(), &CircuitBreakerState::HalfOpen); + }); + + // Try to pass a request when the circuit breaker is still open + let machine_try = machine.clone(); + std::thread::spawn(move || { + std::thread::sleep_ms(1000); + let mut lock = machine_try.lock().unwrap(); + let res = lock.consume_anyway(&CircuitBreakerInput::Successful); + assert_eq!(res, None); + assert_eq!(lock.state(), &CircuitBreakerState::Open); + }); + + // Test if the circit breaker was actually closed + std::thread::sleep_ms(7000); + { + std::thread::sleep_ms(5000); + let mut lock = machine.lock().unwrap(); + let res = lock.consume_anyway(&CircuitBreakerInput::Successful); + assert_eq!(res, None); + assert_eq!(lock.state(), &CircuitBreakerState::Closed); + } +} @@ -1,7 +1,5 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} +mod machine; +mod machine_wrapper; + +pub use machine::StateMachine; +pub use machine_wrapper::StateMachineWrapper; diff --git a/src/machine.rs b/src/machine.rs new file mode 100644 index 0000000..a38f7eb --- /dev/null +++ b/src/machine.rs @@ -0,0 +1,22 @@ +/// This trait is designed to describe any possible deterministic finite state +/// machine/transducer. This is just a formal definition that may be +/// inconvenient to be used in practical programming, but it is used throughout +/// this library for more practical things. +pub trait StateMachine { + /// The input alphabet. + type Input; + /// The set of possible states. + type State; + /// The output alphabet. + type Output; + /// The initial state of the machine. + const INITIAL_STATE: Self::State; + /// The transition fuction that outputs a new state based on the current + /// state and the provided input. Outputs `None` when there is no transition + /// for a given combination of the input and the state. + fn transition(state: &Self::State, input: &Self::Input) -> Option<Self::State>; + /// The output function that outputs some value from the output alphabet + /// based on the current state and the given input. Outputs `None` when + /// there is no output for a given combination of the input and the state. + fn output(state: &Self::State, input: &Self::Input) -> Option<Self::Output>; +} diff --git a/src/machine_wrapper.rs b/src/machine_wrapper.rs new file mode 100644 index 0000000..a48cb1a --- /dev/null +++ b/src/machine_wrapper.rs @@ -0,0 +1,52 @@ +use crate::StateMachine; + +/// A convenience wrapper around the `StateMachine` trait that encapsulates the +/// state and transition and output function calls. +pub struct StateMachineWrapper<T: StateMachine> { + state: T::State, +} + +impl<T> StateMachineWrapper<T> +where + T: StateMachine, +{ + /// Create a new instance of this wrapper which encapsulates the initial + /// state. + pub fn new() -> Self { + StateMachineWrapper { + state: T::INITIAL_STATE, + } + } + + /// Consumes the provided input, gives an output and performs a state + /// transition. If a state transition with the current state and the + /// provided input is not allowed, returns an error. + pub fn consume(&mut self, input: &T::Input) -> Result<Option<T::Output>, ()> { + // Operations are reodered for optimization. When the transition is not + // allowed this code exits as soon as possible without calculating the + // output. + let state = match T::transition(&self.state, input) { + Some(state) => state, + None => return Err(()), + }; + let output = T::output(&self.state, input); + self.state = state; + Ok(output) + } + + /// Consumes the provided input, gives an output and performs a state + /// transition. If a state transition is not allowed, this function just + /// provides an output. + pub fn consume_anyway(&mut self, input: &T::Input) -> Option<T::Output> { + let output = T::output(&self.state, input); + if let Some(state) = T::transition(&self.state, input) { + self.state = state; + } + output + } + + /// Returns the current state. + pub fn state(&self) -> &T::State { + &self.state + } +} |