Finite state machines in rust; bendns fork to add types.
Implement a generic state machine and add an example
A generic state machine trait that allows to define any possible determined finite state machine/reducer, e.g. it allows to provide state, inputs and outputs of the machine. A wrapper for generic state machines for practical usage. An example of the library usage based on the Circuit Breaker pattern.
Yevhenii Babichenko 2019-04-28
parent 1a7d22b · commit 1d0c97d
-rw-r--r--examples/circuit_breaker.rs105
-rw-r--r--src/lib.rs12
-rw-r--r--src/machine.rs22
-rw-r--r--src/machine_wrapper.rs52
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);
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 31e1bb2..492aa2e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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
+ }
+}