Reference
FAQ
Frequently asked questions about acton-reactive.
Frequently Asked Questions
What's the difference between acton-reactive and Actix?
Both are actor frameworks for Rust, but they have different goals:
| acton-reactive | Actix | |
|---|---|---|
| Focus | Simplicity, accessibility | Performance, ecosystem |
| Message Types | Any Clone + Debug type | Requires actix::Message trait |
| Learning Curve | Gentler | Steeper |
| Ecosystem | Standalone | actix-web, actix-rt, etc. |
| Maturity | Stable (8.x), actively developed | Mature, stable |
Choose acton-reactive if: You want to learn actor patterns without fighting boilerplate, or you need a lightweight actor system for a specific use case.
Choose Actix if: You need maximum performance, want to build on the actix-web ecosystem, or need a battle-tested solution for production.
How do I send a reply back to the sender?
Use the reply_envelope from the message context:
actor.mutate_on::<RequestMessage>(|actor, ctx| {
let reply = ctx.reply_envelope();
let data = actor.model.some_data.clone();
Reply::pending(async move {
reply.send(ResponseMessage { data }).await;
})
});
How do I wait for that reply?
Use ask, after declaring which reply the request expects:
impl Request for RequestMessage {
type Response = ResponseMessage;
}
let response = handle.ask(RequestMessage).await?;
The handler is unchanged: it still answers through the reply envelope, and an actor cannot tell an ask from a send. Because inboxes are FIFO, a completed ask also proves everything sent to that actor beforehand has been processed, which is what makes it the right replacement for a sleep.
Do not ask from inside a mutate_on handler — mutable handlers are awaited inline on the message loop, so waiting for a reply stops the actor from producing it.
How do I wait for a broadcast to be delivered?
broadcast completes when the broker has the message, not when subscribers do, and a broadcast carries no reply address for a subscriber to answer. Ask the broker:
broker.ask(FlushBroadcasts).await?;
That is delivery. For completion at a particular subscriber, ask that subscriber afterwards. shutdown_all flushes the broker for you before signalling anything.
How do I create child actors?
Register the child with a blueprint, which is what lets the framework restart it:
let parent_handle = parent.start().await;
let config = ActorConfig::for_supervised_child("worker", parent_handle.clone(), None)?;
let child = parent_handle
.supervise_with::<ChildState>(&runtime, config, |actor| {
actor.mutate_on::<Task>(handle_task);
})
.await?;
// SupervisedChild, not ActorHandle: current() names the live incarnation
child.current()?.send(Task).await;
// When parent stops, child stops automatically
From inside a handler, use supervise_deferred, which queues the start instead of awaiting it.
The older supervise(child) still works and still gives cascading shutdown, but the supervisor holds no blueprint, so that child is never restarted.
Can actors communicate across processes?
Yes! Enable the ipc feature and use Unix Domain Sockets:
[dependencies]
See the IPC Setup guide for details.
Is acton-reactive production ready?
Yes. acton-reactive is stable, well-tested, and follows semantic versioning: breaking changes only land in major releases. We recommend:
- Pinning to a major version in production (e.g.
acton-reactive = "8") - Reviewing changelogs before upgrading across major versions
How do I debug message flow?
Enable tracing to see what's happening:
use tracing_subscriber;
#[acton_main]
async fn main() {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
// Your code here
}
You'll see messages like:
DEBUG acton_reactive: Actor actor_1 received message: Increment
DEBUG acton_reactive: Handler for Increment completed
How fast is acton-reactive?
It depends on your use case, but generally:
- Message throughput: Hundreds of thousands of messages per second on modern hardware
- Latency: Sub-millisecond for simple handlers
- Memory: Each actor adds minimal overhead (mostly channel buffers)
For performance-critical applications, profile your specific workload. The act_on handlers run concurrently, which can significantly improve throughput for read-heavy workloads.
Can I use acton-reactive with async database clients?
Absolutely! Handlers return futures, so you can use any async code:
actor.mutate_on::<SaveUser>(|actor, ctx| {
let user = ctx.message().user.clone();
let db = actor.model.db_pool.clone();
Reply::pending(async move {
sqlx::query("INSERT INTO users ...")
.bind(&user.name)
.execute(&db)
.await
.expect("DB error");
})
});
How do I handle errors in handlers?
Use fallible handlers with Reply::try_* helpers:
// Immediate result (sync)
actor.try_mutate_on::<RiskyOperation>(|actor, ctx| {
if something_bad() {
Reply::try_err(MyError::new("something went wrong"))
} else {
Reply::try_ok(SuccessResult)
}
});
// Or with async operations
actor.try_mutate_on::<RiskyOperation>(|actor, ctx| {
Reply::try_pending(async move {
let result = do_risky_thing().await?;
Ok(SuccessResult { data: result })
})
});
// Register error handler
actor.on_error::<RiskyOperation, MyError>(|actor, ctx, error| {
println!("Error: {}", error);
Reply::ready()
});
Common Gotchas
Gotcha: Borrowing actor in async blocks
Problem:
actor.mutate_on::<MyMessage>(|actor, ctx| {
Reply::pending(async move {
// ERROR: actor.model is borrowed in async block
println!("{}", actor.model.value);
})
});
Solution: Clone what you need before the async block:
actor.mutate_on::<MyMessage>(|actor, ctx| {
let value = actor.model.value; // Clone before async
Reply::pending(async move {
println!("{}", value); // Use the clone
})
});
Gotcha: Forgetting to await shutdown
Problem:
fn main() {
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
let mut app = ActonApp::launch_async().await;
// ... do stuff
}); // Runtime drops, actors may not finish!
}
Solution: Always await shutdown_all:
#[acton_main]
async fn main() {
let mut app = ActonApp::launch_async().await;
// ... do stuff
app.shutdown_all().await.expect("Shutdown failed");
}
Gotcha: Using the wrong handler type
Problem: Using act_on (read-only) when you need to mutate state:
// This won't compile!
actor.act_on::<Increment>(|actor, ctx| {
actor.model.count += 1; // ERROR: actor.model is immutable
Reply::ready()
});
Solution: Use mutate_on for mutations:
actor.mutate_on::<Increment>(|actor, ctx| {
actor.model.count += 1; // Works!
Reply::ready()
});
Gotcha: Subscription order matters
Problem: Subscribing after messages are sent means you miss them:
broker.broadcast(ImportantEvent).await; // Sent before subscription!
actor.handle().subscribe::<ImportantEvent>().await; // Too late
Solution: Subscribe before starting, or before any broadcasts:
actor.handle().subscribe::<ImportantEvent>().await; // Subscribe first
let handle = actor.start().await;
// Now safe to broadcast
Gotcha: Deadlock from synchronous send
Problem: Calling send_sync from within a handler can deadlock if the channel is full:
actor.mutate_on::<Trigger>(|actor, ctx| {
// DANGER: If the channel is full, this blocks forever
some_handle.send_sync(BlockingMessage, &address);
Reply::ready()
});
Solution: Use async send inside handlers:
actor.mutate_on::<Trigger>(|actor, ctx| {
let handle = some_handle.clone();
Reply::pending(async move {
handle.send(AsyncMessage).await; // Non-blocking
})
});
Gotcha: Message type confusion
Problem: Two message types with the same name in different modules:
mod a {
use acton_reactive::prelude::*;
#[acton_message]
pub struct Event { pub value: i32 }
}
mod b {
use acton_reactive::prelude::*;
#[acton_message]
pub struct Event { pub data: String } // Different type!
}
// Handler registered for a::Event
actor.mutate_on::<a::Event>(|actor, ctx| { ... });
// But sending b::Event - handler won't fire!
handle.send(b::Event { data: "test".into() }).await;
Solution: Be explicit about types, and consider unique naming:
use acton_reactive::prelude::*;
#[acton_message]
pub struct SensorEvent { pub value: i32 }
#[acton_message]
pub struct UserEvent { pub data: String }
Performance Tips
Tip: Use act_on for read-heavy workloads
If a handler only reads state and doesn't modify it, use act_on instead of mutate_on. Multiple act_on handlers can run concurrently:
// These can run in parallel
actor.act_on::<Query1>(|actor, ctx| { /* read-only */ });
actor.act_on::<Query2>(|actor, ctx| { /* read-only */ });
actor.act_on::<Query3>(|actor, ctx| { /* read-only */ });
Tip: Clone data, not the whole actor
When you need data in an async block, clone just what you need:
// Good - clone only what's needed
let value = actor.model.expensive_data.clone();
Reply::pending(async move {
use_value(value).await;
})
// Bad - cloning entire model when you only need one field
let model = actor.model.clone();
Reply::pending(async move {
use_value(model.expensive_data).await;
})
Tip: Adjust inbox capacity for your workload
High-throughput actors might benefit from larger inbox buffers:
# ~/.config/acton/config.toml
[limits]
actor_inbox_capacity = 1000 # Default is 512
Tip: Use MessagePack for IPC
If you're using IPC heavily, MessagePack is faster and smaller than JSON:
[dependencies]
Still Stuck?
If you can't find the answer here:
- Check the Examples - They cover most common patterns
- Read the API Reference - Every type is documented
- Look at the tests in the
acton-reactiverepository - They're essentially documentation - Open an issue on GitHub - We're happy to help
Migration Guide: v4.x to v5.0
Version 5.0 renamed the handler methods for clarity:
Handler Renames
| v4.x | v5.0 | Purpose |
|---|---|---|
act_on (mutable) | mutate_on | Mutable state access, sequential |
| - | act_on | Read-only state access, concurrent |
Before (v4.x)
builder.act_on::<MyMessage>(|actor, _| {
actor.model.value += 1; // Was mutable in v4
Reply::ready()
});
After (v5.0)
// For mutations, use mutate_on
builder.mutate_on::<MyMessage>(|actor, _| {
actor.model.value += 1;
Reply::ready()
});
// act_on is now read-only and concurrent
builder.act_on::<QueryMessage>(|actor, _| {
let value = actor.model.value; // Read-only
Reply::ready()
});
Quick Migration
- Replace all
act_oncalls that mutate state withmutate_on - Keep
act_onfor read-only operations (they'll now run concurrently!) - Run
cargo build- the compiler will catch any mistakes