A Rust framework for building deterministic, testable, and crash-recoverable state machines with async operations and fallible state access.
Traditional state machines break down in production—race conditions, crashes mid-operation, and bugs that only appear under load. FASM solves this by making correctness verifiable:
- 🎯 Deterministic execution — Same inputs always produce same outputs
- 🔄 Crash recovery — Resume from any failure point automatically
- 🧪 Simulation testing — Verify correctness across millions of operations in seconds
- 🔒 Atomicity — Transactions succeed completely or leave state unchanged
- 💾 Flexible state — In-memory, database transactions, or hybrid
use fasm::{Input, StateMachine, actions::{Action, ActionsContainer, TrackedAction, TrackedActionTypes}};
struct PaymentSystem {
balance: u64,
pending: HashMap<u64, Payment>,
next_id: u64,
}
struct PaymentTracked;
impl TrackedActionTypes for PaymentTracked {
type Id = u64;
type Action = PaymentRequest;
type Result = PaymentResult;
}
impl StateMachine for PaymentSystem {
type State = Self;
type Input = PaymentInput;
type TrackedAction = PaymentTracked;
type UntrackedAction = Notification;
type Actions = Vec<Action<Self::UntrackedAction, Self::TrackedAction>>;
type TransitionError = PaymentError;
type RestoreError = ();
async fn stf<'s, 'a>(
state: &'s mut Self::State,
input: Input<Self::TrackedAction, Self::Input>,
actions: &'a mut Self::Actions,
) -> Result<(), Self::TransitionError> {
match input {
Input::Normal(PaymentInput::Process { amount, user }) => {
// 1. Validate
if state.balance < amount {
return Err(PaymentError::InsufficientFunds);
}
// 2. Prepare (no mutation yet)
let id = state.next_id;
// 3. Fallible operations first
actions.add(Action::Tracked(TrackedAction::new(
id,
PaymentRequest::Charge { amount },
)))?;
// 4. Mutate state (point of no return)
state.next_id += 1;
state.pending.insert(id, Payment { amount, user, status: Pending });
Ok(())
}
Input::TrackedActionCompleted { id, result } => {
let payment = state.pending.get_mut(&id)
.ok_or(PaymentError::NotFound)?;
match result {
PaymentResult::Success => {
state.balance -= payment.amount;
payment.status = Confirmed;
}
PaymentResult::Failed { reason } => {
payment.status = Failed;
}
}
Ok(())
}
}
}
async fn restore<'s, 'a>(
state: &'s Self::State,
actions: &'a mut Self::Actions,
) -> Result<(), Self::RestoreError> {
for (&id, payment) in &state.pending {
if payment.status == Pending {
actions.add(Action::Tracked(TrackedAction::new(
id,
PaymentRequest::CheckStatus { id },
))).map_err(|_| ())?;
}
}
Ok(())
}
}Input (user request, external data)
│
▼
┌─────────────────────────────────────┐
│ State Transition Function (STF) │
│ ───────────────────────────────── │
│ • Validates inputs │
│ • Mutates state atomically │
│ • Emits action descriptions │
└─────────────────────────────────────┘
│
├──► State committed
│
├──► Tracked Actions ──► External Systems ──► Results feed back as Input
│
└──► Untracked Actions (fire-and-forget)
After crash: restore(state) → re-emit pending tracked actions
A deterministic function: (State, Input) → (State', Actions)
- Validates inputs and mutates state
- Emits action descriptions (not executions)
- Must be atomic: if it returns
Err, state is unchanged
Tracked Actions: Results feed back into the STF
- Payment processing, external API calls, background jobs
- Stored in state for crash recovery
- Use when the outcome affects system correctness
Untracked Actions: Fire-and-forget
- Logs, metrics, notifications, UI updates
- Not recovered after crashes
- Use when you don't need confirmation
After a crash, restore() rebuilds pending tracked actions from state:
- Pure function of state (no external queries)
- Runtime clears actions container before calling
- Enables automatic crash recovery
If state is a database transaction, atomicity is automatic:
async fn stf(txn: &mut DbTransaction, input: Input, actions: &mut Actions) -> Result<()> {
let user = txn.get("user:123").await?;
txn.set("balance", new_balance).await?;
actions.add(Action::Tracked(...))?;
Ok(())
// If any operation fails, entire transaction aborts
}For in-memory state, order operations carefully:
async fn stf(state: &mut State, input: Input, actions: &mut Actions) -> Result<()> {
// 1. Validate (can fail)
if state.balance < amount {
return Err(InsufficientFunds);
}
// 2. Prepare values (no mutation)
let id = state.next_id;
// 3. Fallible operations first
actions.add(Action::Tracked(...))?;
// 4. Mutate state last
state.next_id += 1;
state.pending.insert(id, ...);
Ok(())
}- Validate before mutating — Check conditions before changing state
- Deterministic IDs — Generate from state counters, not random/time
- Store tracked actions in state — Before emitting, so restore can recreate them
- Fallible ops before mutations — For in-memory state atomicity
- No side effects in STF — No HTTP calls, no new connections
- No randomness — No
rand::random(), no unseeded RNGs - No system time — Pass timestamps via Input
- No external reads — Except through
stateparameter
Deterministic simulation testing—the killer feature:
#[test]
async fn test_correctness() {
let mut rng = ChaCha8Rng::seed_from_u64(12345);
let mut state = MySystem::new();
let mut actions = Vec::new();
for i in 0..100_000 {
let input = generate_random_input(&mut rng);
let _ = MySystem::stf(&mut state, input, &mut actions).await;
actions.clear();
state.check_invariants()
.expect(&format!("Invariant violated at iteration {}", i));
}
// Same seed = same execution = reproducible bugs
}# Simple counter
cargo run --example csm
# Coffee shop loyalty app with tracked/untracked actions
cargo run --example coffee_shop
# Full booking system with simulation tests
cargo test --package dentist_booking- Payment processing
- Reservation systems
- Workflow engines
- Distributed systems requiring correctness
- Simple CRUD apps
- Stateless services
- Prototypes (unless correctness matters)
- Simplified trait — Uses
async fndirectly (Rust 2024 edition) - Cleaner API — No more manual
Futureimplementations required - Renamed field —
Input::TrackedActionCompleted { id, result }(wasres)
MIT OR Apache-2.0