3 unstable releases
new 0.2.0 | May 15, 2025 |
---|---|
0.1.1 | May 14, 2025 |
0.1.0 | May 13, 2025 |
#266 in Asynchronous
55 downloads per month
57KB
617 lines
rsActor: A Simplified Actor Framework for Rust
rsActor
is a lightweight, Tokio-based actor framework in Rust, inspired by Kameo. It prioritizes simplicity and ease of use for local, in-process actor systems by stripping away more complex features like remote actors and supervision trees.
Note: This project is in a very early stage of development. APIs are subject to change, and features are still being actively developed.
Core Features
- Minimalist Actor System: Focuses on the core actor model primitives.
- Asynchronous Message Passing:
ask
: Send a message and asynchronously await a reply.tell
: Send a message without waiting for a reply (fire-and-forget).
- Actor Lifecycle: Actors implement
on_start
andon_stop
hooks. - Graceful & Immediate Termination: Actors can be stopped gracefully (processing remaining messages) or killed immediately.
- Macro-Assisted Message Handling: The
impl_message_handler!
macro simplifies routing different message types to their respective handlers within an actor. - Tokio-Native: Built exclusively for the
tokio
asynchronous runtime.
Comparison with Kameo
While rsActor
shares the goal of providing an actor system, it makes different design choices compared to Kameo:
- No Remote Actor Support:
rsActor
is for local actors only. - Non-Generic
ActorRef
:rsActor
'sActorRef
is a concrete type. Messages are dynamically typed (Box<dyn Any + Send>
), with runtime type checking for replies inask
calls. Kameo uses a genericActorRef<A: Actor>
. - No Actor Linking or Supervision:
rsActor
does not include built-in support for linking actor lifecycles or supervision strategies. - Tokio-Specific:
rsActor
is tightly coupled with Tokio. Kameo is designed for broader async runtime compatibility. impl_message_handler!
Macro:rsActor
uses a macro to generate the boilerplate for handling multiple message types, whereas Kameo might use generic trait implementations per message.
Getting Started
1. Add Dependency
Add rsActor
to your Cargo.toml
:
[dependencies]
rsactor = "0.1"
(Note: Update the dependency source once rsActor
is published or if you're using a local path.)
2. Basic Usage Example
Here's a simple counter actor:
use rsactor::{Actor, ActorRef, Message, ActorStopReason, impl_message_handler, spawn};
use anyhow::Result;
use log::info;
// Define your actor struct
struct CounterActor {
count: u32,
}
// Implement the Actor trait
impl Actor for CounterActor {
type Error = anyhow::Error;
async fn on_start(&mut self,
actor_ref: ActorRef
) -> Result<(), Self::Error> {
info!("CounterActor (id: {}) started. Initial count: {}", actor_ref.id(), self.count);
Ok(())
}
async fn on_stop(&mut self,
actor_ref: ActorRef,
stop_reason: &ActorStopReason
) -> Result<(), Self::Error> {
info!("CounterActor (id: {}) stopping. Final count: {}. Reason: {:?}", actor_ref.id(), self.count, stop_reason);
Ok(())
}
}
// Define message types
struct IncrementMsg(u32); // Message to increment the counter by a value
struct GetCountMsg; // Message to get the current count
// Implement Message<T> for CounterActor to handle IncrementMsg
impl Message<IncrementMsg> for CounterActor {
type Reply = u32; // This message expects a u32 reply (the new count)
async fn handle(&mut self, msg: IncrementMsg) -> Self::Reply {
self.count += msg.0;
self.count // Return the new count
}
}
// Implement Message<T> for CounterActor to handle GetCountMsg
impl Message<GetCountMsg> for CounterActor {
type Reply = u32; // This message expects a u32 reply (the current count)
async fn handle(&mut self, _msg: GetCountMsg) -> Self::Reply {
self.count // Return the current count
}
}
// Use the impl_message_handler! macro to generate boilerplate
// for routing IncrementMsg and GetCountMsg to their respective handlers.
impl_message_handler!(CounterActor, [IncrementMsg, GetCountMsg]);
#[tokio::main]
async fn main() -> Result<()> {
// Initialize the logger (e.g., env_logger) to see log messages.
// Ensure you have `env_logger` and `log` in your Cargo.toml.
env_logger::init();
let initial_count = 0u32;
let counter_actor_instance = CounterActor { count: initial_count };
info!("Creating CounterActor with initial count: {}", initial_count);
// Spawn the actor.
// spawn returns a tuple:
// 1. ActorRef: A handle to send messages to the actor.
// 2. JoinHandle: A handle to await the actor's termination and retrieve its final state.
info!("Spawning CounterActor...");
let (actor_ref, join_handle) = spawn(counter_actor_instance);
info!("CounterActor spawned with ID: {}", actor_ref.id());
// Send an IncrementMsg using 'ask' to get a reply.
let increment_value = 5u32;
info!("Sending IncrementMsg({}) to CounterActor (ID: {})...", increment_value, actor_ref.id());
let count_after_increment: u32 = actor_ref.ask(IncrementMsg(increment_value)).await?;
info!("Received reply after increment: new count = {}", count_after_increment);
// Send a GetCountMsg using 'ask'.
info!("Sending GetCountMsg to CounterActor (ID: {})...", actor_ref.id());
let current_count: u32 = actor_ref.ask(GetCountMsg).await?;
info!("Received reply for GetCountMsg: current count = {}", current_count);
// Stop the actor gracefully.
// The actor will process all messages currently in its mailbox before stopping.
// The on_stop hook will be called.
info!("Sending stop signal to CounterActor (ID: {})...", actor_ref.id());
actor_ref.stop().await?;
info!("Stop signal sent. CounterActor (ID: {}) will shut down gracefully.", actor_ref.id());
// Wait for the actor's task to complete.
// This is important to ensure the actor has fully stopped and resources are cleaned up.
// join_handle.await returns a Result containing the actor's final state and stop reason.
info!("Waiting for CounterActor (ID: {}) to complete its task...", actor_ref.id());
match join_handle.await {
Ok((stopped_actor, reason)) => {
info!(
"CounterActor (ID: {}) task completed. Final count: {}. Stop reason: {:?}",
actor_ref.id(), // Note: actor_ref.id() is still usable here
stopped_actor.count,
reason
);
}
Err(e) => {
log::error!(
"Error waiting for CounterActor (ID: {}) task to complete: {:?}",
actor_ref.id(), // It's good practice to log the ID if available
e
);
}
}
info!("Example finished.");
Ok(())
}
Running the Example
The project includes a basic example in examples/basic.rs
. You can run it using:
cargo run --example basic
This will demonstrate actor creation, message passing, and lifecycle logging.
Motivation
The primary goal of this project is to provide a streamlined and efficient actor-based framework by focusing on core functionalities while reducing complexity. This makes it suitable for scenarios where a full-featured actor system like actix
might be overkill, but the actor model's benefits (concurrency, state encapsulation) are still desired.
License
This project is licensed under the Apache License 2.0. You can find a copy of the license in the LICENSE-APACHE file.
Contribution
Contributions are welcome! Feel free to open issues and submit pull requests to improve the project.
Dependencies
~2.2–8.5MB
~63K SLoC