Core Concepts
Messages and Handlers
Messages are the only way actors interact. This constraint is what makes the actor model powerful: actors exchange data through explicit, typed messages. Rust's type system ensures you can only send messages an actor knows how to handle.
Defining Messages
A message in Acton is any type marked with #[acton_message]:
use acton_reactive::prelude::*;
#[acton_message]
struct AddItem {
name: String,
quantity: u32,
}
#[acton_message]
struct GetTotal;
Message Requirements
Messages must implement Clone and Debug. The #[acton_message] attribute handles this automatically.
Two Types of Handlers: mutate_on vs act_on
This is the most important concept in Acton.
mutate_on: Sequential State Changes
Use mutate_on when your handler needs to modify the actor's state:
builder.mutate_on::<AddItem>(|actor, envelope| {
let msg = envelope.message();
actor.model.items.push(msg.name.clone());
actor.model.total += msg.quantity;
Reply::ready()
});
Handlers with mutate_on:
- Get
&mutaccess to actor state - Run sequentially — only one runs at a time
- Are guaranteed no concurrent state modifications
act_on: Concurrent Read-Only Operations
Use act_on when your handler only needs to read state:
builder.act_on::<GetTotal>(|actor, envelope| {
let total = actor.model.total;
let reply_envelope = envelope.reply_envelope();
Reply::pending(async move {
reply_envelope.send(TotalResponse(total)).await;
})
});
Handlers with act_on:
- Get
&(immutable) access to actor state - Can run concurrently with other
act_onhandlers - Cannot modify actor state (the compiler enforces this)
Why This Distinction Matters
// WRONG: Using act_on when you need to mutate
builder.act_on::<Increment>(|actor, _envelope| {
actor.model.counter += 1; // Compile error!
Reply::ready()
});
// CORRECT: Use mutate_on for mutations
builder.mutate_on::<Increment>(|actor, _envelope| {
actor.model.counter += 1; // Works
Reply::ready()
});
The compiler prevents accidental mutation in concurrent handlers. Bugs that would be runtime races become compile-time errors.
Choosing the Right Handler
Use mutate_on when:
- The handler changes any field in the actor's state
- You need to guarantee the operation completes before other handlers run
Use act_on when:
- The handler only reads from state
- You want maximum throughput for read operations
Working with Message Data
Handlers receive an envelope, not the raw message. Access the message through envelope.message():
builder.mutate_on::<AddItem>(|actor, envelope| {
let msg = envelope.message(); // Get &AddItem
actor.model.items.push(msg.name.clone());
Reply::ready()
});
For messages without data, you can ignore the envelope:
builder.mutate_on::<Increment>(|actor, _envelope| {
actor.model.count += 1;
Reply::ready()
});
Replying to Messages
No Response Needed
builder.mutate_on::<LogEvent>(|actor, envelope| {
let msg = envelope.message();
actor.model.events.push(msg.clone());
Reply::ready() // Done, no response
});
Sending a Response
Use the reply envelope pattern to send data back:
builder.act_on::<GetCount>(|actor, envelope| {
let count = actor.model.count;
let reply_envelope = envelope.reply_envelope();
Reply::pending(async move {
reply_envelope.send(CountResponse(count)).await;
})
});
The receiving actor must have a handler for CountResponse.
Next
The Actor System — Managing actors with ActonApp