# FASM - Fallible Async State Machines
[](LICENSE)
[](https://www.rust-lang.org)
A Rust framework for building **deterministic, testable, and crash-recoverable** state machines with async operations and fallible state access.
## Why FASM?
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
## Quick Example
```rust
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(())
}
}
```
## How It Works
```
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
```
## Core Concepts
### State Transition Function (STF)
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
### Actions
**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
### Restore
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
## Atomicity
### Transactional State (Database)
If state is a database transaction, atomicity is automatic:
```rust
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
}
```
### In-Memory State
For in-memory state, order operations carefully:
```rust
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(())
}
```
## Key Rules
### β
Must Do
1. **Validate before mutating** β Check conditions before changing state
2. **Deterministic IDs** β Generate from state counters, not random/time
3. **Store tracked actions in state** β Before emitting, so restore can recreate them
4. **Fallible ops before mutations** β For in-memory state atomicity
### β Must Not Do
1. **No side effects in STF** β No HTTP calls, no new connections
2. **No randomness** β No `rand::random()`, no unseeded RNGs
3. **No system time** β Pass timestamps via Input
4. **No external reads** β Except through `state` parameter
## Testing
Deterministic simulation testingβthe killer feature:
```rust
#[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
}
```
## Examples
```bash
# 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
```
## When to Use FASM
### β
Great For
- Payment processing
- Reservation systems
- Workflow engines
- Distributed systems requiring correctness
### β Overkill For
- Simple CRUD apps
- Stateless services
- Prototypes (unless correctness matters)
## Version 0.3
- **Simplified trait** β Uses `async fn` directly (Rust 2024 edition)
- **Cleaner API** β No more manual `Future` implementations required
- **Renamed field** β `Input::TrackedActionCompleted { id, result }` (was `res`)
## Documentation
- [Core Concepts](docs/01_core_concepts.md)
- [Critical Invariants](docs/02_invariants.md)
- [Performance Guide](docs/03_performance.md)
- [Testing Guide](docs/04_testing.md)
- [Database State](docs/05_database_state.md)
## License
MIT OR Apache-2.0