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

Download history 55/week @ 2025-05-07

55 downloads per month

Apache-2.0

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 and on_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's ActorRef is a concrete type. Messages are dynamically typed (Box<dyn Any + Send>), with runtime type checking for replies in ask calls. Kameo uses a generic ActorRef<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