#state-machine #state-transition #traits #macro #finite #events

finite-state-machine

A type and trait based finite state machine macro

3 unstable releases

0.2.0 Apr 13, 2023
0.1.1 Apr 12, 2023
0.1.0 Apr 12, 2023

#2537 in Algorithms

MIT license

9KB
90 lines

Finite state machine

Create finite state machines in rust with macros. The macro generates a struct, event enums and traits and also a decider trait. The decider trait is used to decide which state to transition to. Each state has a function which is called automatically when the state is entered. An invalid state and an end state are automatically added. You also need to implement a function for each transition. The macro automatically generates traits for everything you need to implement so the validity is checked on compile time.

Usage

use finite_state_machine::state_machine;
// Debug is only needed if the verbose feature is enabled
#[derive(Debug, Default)]
struct Data {
    ...
}
state_machine!(
    // The name of the state machine and the type of the data, you can also use livetimes here
    CircuitBreaker(Data);
    // the first state will automatically become the start state, no matter the name
    Closed {
        Ok => Closed, // on Ok event go to Closed state
        AmperageTooHigh => Open // on AmperageTooHigh event go to open state
    },
    Open {
        AttemptReset => HalfOpen,
        Wait => Open
    },
    HalfOpen {
        Success => Closed,
        AmperageTooHigh => Open,
        MaxAttemps => End
    }
);

use circuit_breaker::*;

// now you need to implement the decider trait which emits events which decide which state to transition to
impl Deciders for CircuitBreaker {
    fn closed(&self) -> circuit_breaker::ClosedEvents {
        if self.data.current_amperage > self.data.max_amperage {
            circuit_breaker::ClosedEvents::AmperageTooHigh
        } else {
            circuit_breaker::ClosedEvents::Ok
        }
    }
    ...
}

// now we need to implement the transition trait for each state
impl ClosedTransitions for CircuitBreaker {
    fn amperage_too_high(&mut self) -> Result<(), &'static str> {
        self.data.tripped_at = Some(SystemTime::now());
        Ok(())
    }
    fn ok(&mut self) -> Result<(), &'static str> {
        self.data.current_amperage += 1;
        std::thread::sleep(Duration::from_millis(500));
        Ok(())
    }
    fn illegal(&mut self) {}
}

For more details, check the examples folder.

How it works

  • The macro will create a module with the name of the state machine which is transformed to snake case. In the examples case: CircuitBreaker -> circuit_breaker.
  • The macro will generate a deciders trait. This trait has a function for each state. You need to implement this trait for your struct. In our case impl Deciders for CircuitBreaker. The decider functions only get a non mutable reference &self and must return an enum with a variant for each transition. This enum is generated by the macro. In our case circuit_breaker::ClosedEvents, etc. There is also an Illegal variant which is used when you encounter a state of the struct which is illegal/impossible. This will transition to the Invalid state. In an isolated state machine this should never happen. But if you use the state machine in a larger system, this can happen.
  • For each state you create, a <StateName>Transitions trait will be generated which you need to implement for your struct. In our case impl ClosedTransitions for CircuitBreaker. These functions get a mutable reference to self &mut self and must return a Result<(), &'static str>. The &'static str is used to return an error message. If you return an Err the state machine will transition to the invalid state which is automatically added. When entering this state the machine stops and returns to you the error message.

Debugging

You can enable the feature verbose to get more information about the state machine. This will print the current state, the transition and the current data.

Dependencies