This guide will help you get started with rsActor, a lightweight, Tokio-based actor framework for Rust.
Add rsActor to your Cargo.toml:
[dependencies]
rsactor = "0.12" # Check crates.io for the latest version
# Optional: Enable tracing support for detailed observability
# rsactor = { version = "0.12", features = ["tracing"] }Let's create a simple counter actor that demonstrates the core concepts:
use rsactor::{Actor, ActorRef, message_handlers, spawn};
// 1. Define message types
struct Increment;
struct GetCount;
// 2. Define your actor struct and derive Actor
#[derive(Actor)]
struct CounterActor {
count: u32,
}
// 3. Use the message_handlers macro for automatic message handling
#[message_handlers]
impl CounterActor {
#[handler]
async fn handle_increment(&mut self, _msg: Increment, _: &ActorRef<Self>) -> () {
self.count += 1;
}
#[handler]
async fn handle_get_count(&mut self, _msg: GetCount, _: &ActorRef<Self>) -> u32 {
self.count
}
}
// 4. Usage
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let actor = CounterActor { count: 0 };
let (actor_ref, _join_handle) = spawn::<CounterActor>(actor);
// Send messages to the actor
actor_ref.tell(Increment).await?;
let count = actor_ref.ask(GetCount).await?;
println!("Count: {}", count); // Prints: Count: 1
actor_ref.stop().await?;
Ok(())
}For actors that need complex initialization logic:
use rsactor::{Actor, ActorRef, ActorWeak, message_handlers, spawn};
use anyhow::Result;
// Define actor struct
struct CounterActor {
count: u32,
name: String,
}
// Implement Actor trait for complex initialization
impl Actor for CounterActor {
type Args = (u32, String); // Tuple of initial count and name
type Error = anyhow::Error;
// on_start is required - this is where the actor instance is created
async fn on_start(args: Self::Args, actor_ref: &ActorRef<Self>) -> Result<Self, Self::Error> {
let (initial_count, name) = args;
println!("CounterActor '{}' (ID: {}) starting with count: {}",
name, actor_ref.identity(), initial_count);
Ok(CounterActor {
count: initial_count,
name,
})
}
// on_run is optional - idle handler called when message queue is empty
async fn on_run(&mut self, _actor_weak: &ActorWeak<Self>) -> Result<bool, Self::Error> {
// Called when there are no messages to process
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
println!("Actor '{}' heartbeat - current count: {}", self.name, self.count);
Ok(true) // Return true to continue idle processing, false to disable it
}
// on_stop is optional - called when the actor is terminating
async fn on_stop(&mut self, actor_weak: &ActorWeak<Self>, killed: bool) -> Result<(), Self::Error> {
println!("Actor '{}' stopping (killed: {}), final count: {}",
self.name, killed, self.count);
Ok(())
}
}
// Define message types
struct IncrementMsg(u32);
struct GetCountMsg;
// Use message_handlers macro
#[message_handlers]
impl CounterActor {
#[handler]
async fn handle_increment(&mut self, msg: IncrementMsg, _: &ActorRef<Self>) -> u32 {
self.count += msg.0;
println!("Actor '{}' incremented by {}, new count: {}",
self.name, msg.0, self.count);
self.count
}
#[handler]
async fn handle_get_count(&mut self, _msg: GetCountMsg, _: &ActorRef<Self>) -> u32 {
self.count
}
}
#[tokio::main]
async fn main() -> Result<()> {
// Spawn actor with initialization arguments
let (actor_ref, join_handle) = spawn::<CounterActor>((10, "MyCounter".to_string()));
// Allow actor to run and emit heartbeats
tokio::time::sleep(std::time::Duration::from_millis(2500)).await;
// Send messages
let new_count = actor_ref.ask(IncrementMsg(5)).await?;
println!("Received count: {}", new_count);
let current_count = actor_ref.ask(GetCountMsg).await?;
println!("Current count: {}", current_count);
// Gracefully stop the actor
actor_ref.stop().await?;
// Wait for actor to complete and check the result
match join_handle.await? {
rsactor::ActorResult::Completed { actor, killed } => {
println!("Actor completed successfully. Final count: {}, killed: {}",
actor.count, killed);
}
rsactor::ActorResult::Failed { error, phase, .. } => {
println!("Actor failed during {:?}: {}", phase, error);
}
}
Ok(())
}on_start(required): Creates the actor instance from initialization argumentson_run(optional): Idle handler called when message queue is empty, returnsboolto control continuationon_stop(optional): Cleanup logic when the actor terminates
tell: Fire-and-forget messagingask: Request-reply messagingtell_with_timeout/ask_with_timeout: Variants with timeout supportblocking_tell/blocking_ask: Blocking variants for use from any thread (no runtime context required)
stop(): Graceful shutdown - processes remaining messageskill(): Immediate termination - stops processing immediately
rsActor provides strong compile-time type safety:
// This will compile - CounterActor handles IncrementMsg
let count: u32 = actor_ref.ask(IncrementMsg(5)).await?;
// This will NOT compile - CounterActor doesn't handle this message type
// let result = actor_ref.ask("invalid message").await?; // Compile error!- Learn about advanced features like actor lifecycle management
- Explore tracing support for production observability
- Check out the examples directory for more complex use cases
- Read the FAQ for common questions and best practices
Enable comprehensive observability:
[dependencies]
rsactor = { version = "0.12", features = ["tracing"] }
tracing-subscriber = "0.3"Initialize tracing in your application:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize tracing
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.with_target(false)
.init();
// Your actor code here...
Ok(())
}This provides detailed logs of actor lifecycle events, message handling, and performance metrics.