Background Workers & Task Queues
Drive asynchronous, distributed statechart workflows using Tokio worker pools and background task queues (e.g. Apalis / Redis queue workers).
This guide covers the Rust counterpart of Python’s Celery integration (xstate-statemachine[celery]).
🎯 Architecture: Persistent Worker Pipeline
flowchart LR
Queue[(Task Queue: Redis / RabbitMQ / SQS)] -->|Pop Message| W[Tokio Worker Task]
W -->|Acquire Lock & Load Snapshot| S[xstate-persistence]
S -->|Restore Actor| I[SyncInterpreter / Tokio Actor]
I -->|Execute Microstep & Actions| R[Receipt]
R -->|Save Snapshot & Outbox| S
W -->|ACK Message| Queue
W -.->|Transient Failure| Retry[Exponential Backoff Queue]
📦 Dependencies
[dependencies]
tokio = { version = "1", features = ["full"] }
xstate-statemachine = { version = "0.1", features = ["tokio", "persistence", "sqlite"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
🚀 Implementation: Durable Tokio Task Consumer Loop
#![allow(unused)]
fn main() {
use std::sync::Arc;
use tokio::sync::mpsc;
use xstate_statemachine::prelude::*;
use xstate_statemachine::persistence::{SqliteStore, SqliteStoreOptions, persisted, PersistedOptions};
#[derive(Debug)]
pub struct BackgroundTask {
pub workflow_id: String,
pub event_name: String,
pub payload: serde_json::Value,
}
pub async fn run_worker_pool(
mut queue_rx: mpsc::Receiver<BackgroundTask>,
store: Arc<SqliteStore>,
machine: Arc<Machine>,
logic: Arc<MachineLogic>,
) {
println!("⚙️ Background Statechart Worker Pool active...");
while let Some(task) = queue_rx.recv().await {
let store = store.clone();
let machine = machine.clone();
let logic = logic.clone();
tokio::spawn(async move {
let res = persisted(
&*store,
&task.workflow_id,
&*machine,
&*logic,
&PersistedOptions::default(),
|interp, _| {
let ev = Event::new(&task.event_name).with_payload(task.payload);
let receipt = interp.send(ev)?;
Ok(receipt.state_ids())
},
);
match res {
Ok(new_states) => {
tracing::info!(
workflow = %task.workflow_id,
states = ?new_states,
"Workflow step processed successfully"
);
}
Err(err) => {
tracing::error!(
workflow = %task.workflow_id,
error = %err,
"Failed to process background workflow step"
);
}
}
});
}
}
}
⏱️ Durable Timer & Deadline Worker (DueTimerScanner)
In long-running business processes, transitions may be triggered after minutes, days, or months ("after": {"86400000": "escalated"}).
xstate-persistence includes a built-in background scanner that polls durable deadlines across SQLite or PostgreSQL:
use std::sync::Arc;
use std::time::Duration;
use xstate_statemachine::persistence::{SqliteStore, timers::DueTimerScanner};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Arc::new(SqliteStore::open_default("workflows.db")?);
// Spawn scanner that checks for expired deadlines every second
let scanner = DueTimerScanner::new(store.clone(), Duration::from_secs(1));
tokio::spawn(async move {
scanner.run().await;
});
println!("⏰ Durable timer scanner running in background...");
tokio::signal::ctrl_c().await?;
Ok(())
}