| Crates.io | obzenflow-fsm |
| lib.rs | obzenflow-fsm |
| version | 0.3.0 |
| created_at | 2026-01-21 02:45:09.20004+00 |
| updated_at | 2026-01-21 02:45:09.20004+00 |
| description | Async-first Finite State Machine library inspired by Akka (Classic) FSM |
| homepage | https://github.com/ObzenFlow/obzenflow-fsm |
| repository | https://github.com/ObzenFlow/obzenflow-fsm |
| max_upload_size | |
| id | 2058105 |
| size | 333,269 |
obzenflow-fsm is an async-first finite state machine library for Rust, built around a Mealy-machine core and explicit actions.
(State, Event, Context) -> (State', Actions)&mut Context)fsm!) plus optional derive helpersFsmError) and strict builder validationObzenFlow’s architecture leans heavily on event-sourced finite state machines: keep state evolution deterministic, make effects explicit, and make “what happened” auditable.
This crate was extracted as a standalone library so the FSM engine can be reused independently (it has no dependencies on other ObzenFlow crates).
[dependencies]
obzenflow-fsm = "0.3"
You’ll typically also want a Tokio runtime (timeouts use tokio::time) and async-trait for implementing actions.
A tiny “door” FSM with explicit actions.
Note: fsm! stores handlers behind trait objects, so each handler closure returns a boxed pinned future
(Box::pin(async move { ... })).
use obzenflow_fsm::{fsm, types::FsmResult, FsmAction, FsmContext, Transition};
#[derive(Clone, Debug, PartialEq, obzenflow_fsm::StateVariant)]
enum DoorState {
Closed,
Open,
}
#[derive(Clone, Debug, obzenflow_fsm::EventVariant)]
enum DoorEvent {
Open,
Close,
}
#[derive(Clone, Debug, PartialEq)]
enum DoorAction {
Ring,
Log(String),
}
#[derive(Default)]
struct DoorContext {
log: Vec<String>,
}
impl FsmContext for DoorContext {}
#[async_trait::async_trait]
impl FsmAction for DoorAction {
type Context = DoorContext;
async fn execute(&self, ctx: &mut Self::Context) -> FsmResult<()> {
match self {
DoorAction::Ring => ctx.log.push("Ring!".to_string()),
DoorAction::Log(msg) => ctx.log.push(msg.clone()),
}
Ok(())
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> FsmResult<()> {
let mut door = fsm! {
state: DoorState;
event: DoorEvent;
context: DoorContext;
action: DoorAction;
initial: DoorState::Closed;
state DoorState::Closed {
on DoorEvent::Open => |_s: &DoorState, _e: &DoorEvent, _ctx: &mut DoorContext| {
Box::pin(async move {
Ok(Transition {
next_state: DoorState::Open,
actions: vec![
DoorAction::Ring,
DoorAction::Log("Door opened".into()),
],
})
})
};
}
state DoorState::Open {
on DoorEvent::Close => |_s: &DoorState, _e: &DoorEvent, _ctx: &mut DoorContext| {
Box::pin(async move {
Ok(Transition {
next_state: DoorState::Closed,
actions: vec![DoorAction::Log("Door closed".into())],
})
})
};
}
};
let mut ctx = DoorContext::default();
let actions = door.handle(DoorEvent::Open, &mut ctx).await?;
door.execute_actions(actions, &mut ctx).await?;
assert_eq!(door.state(), &DoorState::Open);
let actions = door.handle(DoorEvent::Close, &mut ctx).await?;
door.execute_actions(actions, &mut ctx).await?;
assert_eq!(door.state(), &DoorState::Closed);
assert_eq!(
ctx.log,
vec![
"Ring!".to_string(),
"Door opened".to_string(),
"Door closed".to_string(),
]
);
Ok(())
}
obzenflow-fsm is a Mealy machine: outputs depend on both the current state and the input event.
Transition { next_state, actions }.StateMachine::execute_actions.This keeps decision-making deterministic and makes side effects auditable.
For more examples (timeouts, entry/exit hooks, unhandled handlers, host-loop patterns), see the crate docs on https://docs.rs/obzenflow-fsm.
obzenflow-fsm is designed to be driven by a host loop (often an actor/supervisor task):
&mut Context) and controls effect execution.StateMachine::handle(event, &mut ctx) returns actions; the engine never runs effects implicitly.StateMachine::check_timeout(&mut ctx) when it makes sense for your runtime.This maps cleanly to “retry actions, map failures into explicit error events, and keep state evolution deterministic”.
For outcomes that stay stable under duplicates, interleavings, and reshaping (batching/sharding), the tests are essentially pointing at the "unholy trinity" of distributed systems failures: fuzzy or broken idempotence, commutativity, and associativity guarantees. These are sufficient conditions for many dataflow operators, not universal requirements (some domains are intentionally order-dependent).
In practice:
The test suite is intentionally written as documentation. It tells a story about real failure modes and the guarantees that keep an async FSM correct under distributed-systems pressure (the “unholy trials”).
It’s also loosely modeled after Dante’s Divine Comedy. Think of these failure modes as “circles of hell”.
tests/test_race_condition.rstests/test_async_coordination.rstests/test_journal_subscription.rstests/test_mathematical_properties.rstests/test_timeout_cancellation.rstests/test_memory_corruption.rscargo test
Run one “circle” with output:
cargo test test_4_mark_of_the_beast_mathematical_properties -- --nocapture
Other feature-focused tests worth skimming:
tests/test_dsl_basic.rs, tests/test_dsl_features.rstests/test_builder_enforcement.rs, tests/test_builder_only_construction.rstests/test_compile_safety.rs, tests/test_edge_cases.rs, tests/test_comprehensive.rsCHANGELOG.mdCONTRIBUTING.mdSECURITY.mdCODE_OF_CONDUCT.mdTRADEMARKS.mdDual-licensed under MIT OR Apache-2.0.