Finitomata :: FSM with rich lifecycle callbacks, persistence, and supervision

A half of year ago I ported most of Erlang supervision tree model to Rust as joerl. It has been tested in the field since then and it came to port of finitomata — a Finite Automata implementation done right.

Welcome finitomata in Rust on top of joerl.

Self-explanatory quick-start from README:

use async_trait::async_trait;
use finitomata::{finitomata, Finitomata, FinitomataSupervisor, TransitionResult};
use std::time::Duration;

#[finitomata(
    fsm = r#"
        [*] --> idle
        idle --> |start| running
        running --> |stop| idle
        idle --> |shutdown| off
        off --> |confirm| [*]
    "#,
    syntax = "mermaid",
    auto_terminate = true
)]
#[derive(Debug, Clone, Default)]
struct MyFsm;

#[derive(Debug, Clone)]
struct Payload { counter: u32 }

#[async_trait]
impl Finitomata for MyFsm {
    type State = MyFsmState;
    type Event = MyFsmEvent;
    type Payload = Payload;

    async fn on_transition(
        &mut self,
        _from: &MyFsmState,
        event: &MyFsmEvent,
        _event_payload: &Payload,
        state_payload: &mut Payload,
    ) -> TransitionResult<MyFsmState, Payload> {
        match event {
            MyFsmEvent::Start => {
                state_payload.counter += 1;
                TransitionResult::Ok(MyFsmState::Running)
            }
            MyFsmEvent::Stop => TransitionResult::Ok(MyFsmState::Idle),
            MyFsmEvent::Shutdown => TransitionResult::Ok(MyFsmState::Off),
            MyFsmEvent::Confirm => TransitionResult::Ok(MyFsmState::Off),
        }
    }
}

#[tokio::main]
async fn main() {
    let graph = MyFsm::build_graph();
    let supervisor = FinitomataSupervisor::<MyFsm>::new("my_sup", graph)
        .with_auto_terminate(true);

    supervisor.start_fsm("instance_1", MyFsm, Payload { counter: 0 }).await.unwrap();

    // Send events
    supervisor.transition("instance_1", MyFsmEvent::Start, Payload { counter: 0 }).await.unwrap();
    tokio::time::sleep(Duration::from_millis(50)).await;

    // Query state (from cache, no actor round-trip)
    let state = supervisor.state("instance_1").unwrap();
    println!("Current: {:?}, Counter: {}", state.current, state.payload.counter);
}

Enjoy!

I'm super interested in actor model implementations in Rust and joerl looks really cool. Though I'm confused what finitomata adds on top of it (apart from a different API) and when would I use one or the other.

Small suggestion on the example: It is somewhat confusing when the Payload type is used for both the actor internal state and the event. Maybe a richer example could demonstrate how different event payloads could be checked at compile time?

TL;DR: if you are confused what’s the difference, you don’t need finitomata yet, you are good to go with joerl.

Yes, joerl comes up with GenStatem trait mirroring the erlang’s gen_statem behaviour. If you are interested in actor model per se, you should be good with joerl.

On the other hand, FSM implementation has nothing to do with the actor model, except it’s drastically easy to build the one on top of it. finitomata is not about the different API by any means, it’s about the complete, fault tolerant, easy-to-use finite automata implementation.

It’s like asking when would I eat apples or oranges. FSM might be implemented on top of actor model, or a factory in OOP, or a typeclass in Haskell, or even on top of jump instructions in assembly language. Basically, if you have bare states, you would most likely want to use an FSM to manage them (I even wrote a long rant on that topic.)

I am not doing Rust for a living, that was just a request from the neighbour team, so I doubt I am to put any additional effort into this in the meanwhile. The Elixir examples might shed a light, andalso I gladly accept PRs.