AgentSkillsCN

Rsactor Guide

Rsactor 指南

SKILL.md

rsactor-guide

Quick reference guide for rsactor actor framework.

When to use

Use this skill when the user needs information about rsactor APIs, patterns, or troubleshooting.

Quick Start

toml
# Cargo.toml
[dependencies]
rsactor = "0.x"  # Check crates.io for latest version
tokio = { version = "1", features = ["full"] }
anyhow = "1"
rust
use rsactor::{Actor, ActorRef, message_handlers, spawn};

struct Ping;

#[derive(Actor)]
struct MyActor;

#[message_handlers]
impl MyActor {
    #[handler]
    async fn handle_ping(&mut self, _: Ping, _: &ActorRef<Self>) {
        println!("Pong!");
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let (actor_ref, handle) = spawn::<MyActor>(MyActor);
    actor_ref.tell(Ping).await?;
    actor_ref.stop().await?;
    handle.await?;
    Ok(())
}

Core Types

TypePurpose
ActorTrait defining actor lifecycle
ActorRef<T>Strong reference for sending messages
ActorWeak<T>Weak reference (won't keep actor alive)
ActorResultEnum for actor completion status
ActorControlType-erased lifecycle control (strong)
WeakActorControlType-erased lifecycle control (weak)
TellHandler<M>Type-erased fire-and-forget message sending
AskHandler<M, R>Type-erased request-reply message sending

Actor Lifecycle

code
spawn()
  → on_start()     [initialization, returns Self]
  → on_run() loop  [concurrent with message processing]
  → on_stop()      [cleanup on shutdown]
  → ActorResult    [Completed or Failed]

Lifecycle Methods

rust
impl Actor for MyActor {
    type Args = MyActorArgs;  // Arguments passed to spawn()
    type Error = anyhow::Error;

    // Required: Initialize actor state
    async fn on_start(args: Self::Args, actor_ref: &ActorRef<Self>) -> Result<Self, Self::Error>;

    // Optional: Background task (called repeatedly while returning Ok(true))
    // Return Ok(true) to keep calling, Ok(false) to stop idle processing
    async fn on_run(&mut self, actor_ref: &ActorWeak<Self>) -> Result<bool, Self::Error> {
        Ok(false)  // Default: no idle processing
    }

    // Optional: Cleanup on shutdown
    // killed: true if via kill(), false if via stop()
    async fn on_stop(&mut self, actor_ref: &ActorWeak<Self>, killed: bool) -> Result<(), Self::Error> {
        Ok(())  // Default: no-op
    }
}

Message Handling

Defining Handlers

rust
#[message_handlers]
impl MyActor {
    #[handler]
    async fn any_name(&mut self, msg: MessageType, actor_ref: &ActorRef<Self>) -> ReturnType {
        // Process message
    }
}

Communication Patterns

rust
// Fire-and-forget (tell) - handler returns ()
actor_ref.tell(MyMessage).await?;

// Request-response (ask) - handler returns a value
let result: Response = actor_ref.ask(MyQuery).await?;

// With timeout
use std::time::Duration;
let result = actor_ref.ask_with_timeout(MyQuery, Duration::from_secs(5)).await?;

Actor Control

rust
// Graceful stop (process remaining messages first)
actor_ref.stop().await?;

// Immediate stop (skip remaining messages)
actor_ref.kill().await?;

// Check if alive
if actor_ref.is_alive() { /* ... */ }

// Get unique identity
let id = actor_ref.identity();

ActorResult Handling

rust
let result = join_handle.await?;

match result {
    ActorResult::Completed { actor, killed } => {
        // actor: The final actor state
        // killed: true if stopped via kill(), false if stop()
        println!("Final state: {:?}", actor);
    }
    ActorResult::Failed { actor, error, phase, killed } => {
        // actor: Option<A> - may be None if failed in on_start
        // error: The error that caused failure
        // phase: "on_start", "on_run", "on_stop", or "handler"
        eprintln!("Failed in {}: {}", phase, error);
    }
}

// Helper methods
result.stopped_normally();  // true if Completed && !killed
result.was_killed();        // true if killed flag is set
result.into_actor();        // Extract actor (panics if Failed without actor)

Handler Traits (Polymorphism)

Store different actor types handling the same message:

rust
use rsactor::{TellHandler, AskHandler, WeakTellHandler, WeakAskHandler};

// Strong handlers
let handlers: Vec<Box<dyn TellHandler<Ping>>> = vec![
    (&actor_a).into(),
    (&actor_b).into(),
];

// Access lifecycle control via as_control()
for handler in &handlers {
    println!("Actor {} alive: {}", handler.as_control().identity(), handler.as_control().is_alive());
    handler.tell(Ping).await?;
}

// Weak handlers (don't keep actors alive)
let weak: Box<dyn WeakTellHandler<Ping>> = ActorRef::downgrade(&actor_ref).into();

// Access weak control via as_weak_control()
println!("Weak identity: {}", weak.as_weak_control().identity());

if let Some(strong) = weak.upgrade() {
    strong.tell(Ping).await?;
}

ActorControl (Type-Erased Lifecycle Management)

Manage different actor types without knowing their message types:

rust
use rsactor::ActorControl;

// Store different actor types in one collection
let controls: Vec<Box<dyn ActorControl>> = vec![
    (&worker_actor).into(),
    (&logger_actor).into(),
];

// Unified lifecycle management
for control in &controls {
    println!("Actor {} alive: {}", control.identity(), control.is_alive());
}

// Stop all actors
for control in &controls {
    control.stop().await?;
}

Common Patterns

Periodic Tasks

rust
async fn on_run(&mut self, _: &ActorWeak<Self>) -> Result<bool, Self::Error> {
    tokio::select! {
        _ = self.interval.tick() => {
            self.do_periodic_work();
        }
        _ = self.some_future => {
            self.handle_event();
        }
    }
    Ok(true)  // Continue calling on_run
}

Actor Supervision (Parent-Child)

rust
struct ParentActor {
    children: Vec<ActorRef<ChildActor>>,
}

#[message_handlers]
impl ParentActor {
    #[handler]
    async fn spawn_child(&mut self, _: SpawnChild, _: &ActorRef<Self>) {
        let (child_ref, _handle) = spawn::<ChildActor>(ChildArgs {});
        self.children.push(child_ref);
    }
}

Forwarding Messages

rust
#[handler]
async fn handle_forward(&mut self, msg: ForwardRequest, _: &ActorRef<Self>) {
    if let Some(target) = &self.target_actor {
        let _ = target.tell(msg.inner).await;
    }
}

Feature Flags

toml
[dependencies]
rsactor = { version = "0.x", features = ["tracing", "metrics"] }
FeatureDescription
tracingEnable tracing instrumentation for observability
metricsEnable actor performance metrics
test-utilsEnable dead letter counter for testing (never use in production)

Error Handling

Error Methods

rust
use rsactor::Error;

fn handle_error(err: &Error) {
    // Check if operation can be retried
    if err.is_retryable() {
        // Only Timeout errors are retryable
        println!("Retrying...");
    }

    // Get actionable debugging tips
    for tip in err.debugging_tips() {
        eprintln!("Tip: {}", tip);
    }
}

Retry Pattern

rust
async fn send_with_retry<T, M>(
    actor: &ActorRef<T>,
    msg: M,
    max_attempts: usize,
) -> Result<(), Error>
where
    T: Actor + Message<M>,
    M: Clone + Send + 'static,
{
    let mut attempts = 0;
    loop {
        match actor.tell(msg.clone()).await {
            Ok(()) => return Ok(()),
            Err(e) if e.is_retryable() && attempts < max_attempts => {
                attempts += 1;
                tokio::time::sleep(Duration::from_millis(100 * attempts as u64)).await;
            }
            Err(e) => return Err(e),
        }
    }
}

Dead Letter Tracking

Dead letters are messages that could not be delivered. They are automatically logged via tracing:

text
WARN rsactor::dead_letter: Dead letter: message could not be delivered
    actor.id=42
    actor.type_name="MyActor"
    message.type_name="PingMessage"
    dead_letter.reason="actor stopped"
    dead_letter.operation="tell"

Dead Letter Reasons

ReasonWhen it occurs
ActorStoppedMessage sent to stopped actor
Timeouttell_with_timeout or ask_with_timeout exceeded
ReplyDroppedHandler failed before sending reply

Testing Dead Letters

rust
// Enable test-utils feature
use rsactor::{dead_letter_count, reset_dead_letter_count};

#[tokio::test]
async fn test_dead_letters() {
    reset_dead_letter_count();
    let initial = dead_letter_count();

    // ... cause dead letters ...

    assert_eq!(dead_letter_count() - initial, expected_count);
}

Blocking Operations with Timeout

rust
use std::time::Duration;

// Without timeout
actor_ref.blocking_tell(msg, None)?;
actor_ref.blocking_ask(query, None)?;

// With timeout
actor_ref.blocking_tell(msg, Some(Duration::from_secs(5)))?;
let result: String = actor_ref.blocking_ask(query, Some(Duration::from_secs(10)))?;

Troubleshooting

"Handler not found" at compile time

  • Ensure #[message_handlers] is on the impl block
  • Ensure #[handler] is on each handler method
  • Check message type matches exactly

Actor stops unexpectedly

  • Check on_run returns Ok(true) or Ok(false) (errors cause shutdown)
  • Check handler errors
  • Use ActorResult::Failed to see the error and phase

Message not delivered

  • Check actor_ref.is_alive() before sending
  • Use ask instead of tell to get errors
  • Check for panics in handlers (actor will fail)
  • Enable tracing to see dead letter logs: RUST_LOG=rsactor=warn
  • Use err.debugging_tips() for actionable suggestions

Imports Cheatsheet

rust
use rsactor::{
    // Core
    Actor, ActorRef, ActorWeak, ActorResult, Error,
    // Macros
    message_handlers,
    // Functions
    spawn,
    // Handler traits (for polymorphism)
    TellHandler, AskHandler, WeakTellHandler, WeakAskHandler,
    // Control traits (for type-erased lifecycle management)
    ActorControl, WeakActorControl,
    // Dead letters (requires test-utils feature)
    // dead_letter_count, reset_dead_letter_count,
    // Debugging
    DeadLetterReason,
};