Reference

FAQ

Frequently asked questions about acton-reactive.


Frequently Asked Questions

What's the difference between acton-reactive and Actix?

Both are actor frameworks for Rust, but they have different goals:

acton-reactiveActix
FocusSimplicity, accessibilityPerformance, ecosystem
Message TypesAny Clone + Debug typeRequires actix::Message trait
Learning CurveGentlerSteeper
EcosystemStandaloneactix-web, actix-rt, etc.
MaturityStable (8.x), actively developedMature, stable

Choose acton-reactive if: You want to learn actor patterns without fighting boilerplate, or you need a lightweight actor system for a specific use case.

Choose Actix if: You need maximum performance, want to build on the actix-web ecosystem, or need a battle-tested solution for production.


How do I send a reply back to the sender?

Use the reply_envelope from the message context:

actor.mutate_on::<RequestMessage>(|actor, ctx| {
    let reply = ctx.reply_envelope();
    let data = actor.model.some_data.clone();

    Reply::pending(async move {
        reply.send(ResponseMessage { data }).await;
    })
});

How do I wait for that reply?

Use ask, after declaring which reply the request expects:

impl Request for RequestMessage {
    type Response = ResponseMessage;
}

let response = handle.ask(RequestMessage).await?;

The handler is unchanged: it still answers through the reply envelope, and an actor cannot tell an ask from a send. Because inboxes are FIFO, a completed ask also proves everything sent to that actor beforehand has been processed, which is what makes it the right replacement for a sleep.

Do not ask from inside a mutate_on handler — mutable handlers are awaited inline on the message loop, so waiting for a reply stops the actor from producing it.


How do I wait for a broadcast to be delivered?

broadcast completes when the broker has the message, not when subscribers do, and a broadcast carries no reply address for a subscriber to answer. Ask the broker:

broker.ask(FlushBroadcasts).await?;

That is delivery. For completion at a particular subscriber, ask that subscriber afterwards. shutdown_all flushes the broker for you before signalling anything.


How do I create child actors?

Register the child with a blueprint, which is what lets the framework restart it:

let parent_handle = parent.start().await;

let config = ActorConfig::for_supervised_child("worker", parent_handle.clone(), None)?;

let child = parent_handle
    .supervise_with::<ChildState>(&runtime, config, |actor| {
        actor.mutate_on::<Task>(handle_task);
    })
    .await?;

// SupervisedChild, not ActorHandle: current() names the live incarnation
child.current()?.send(Task).await;

// When parent stops, child stops automatically

From inside a handler, use supervise_deferred, which queues the start instead of awaiting it.

The older supervise(child) still works and still gives cascading shutdown, but the supervisor holds no blueprint, so that child is never restarted.


Can actors communicate across processes?

Yes! Enable the ipc feature and use Unix Domain Sockets:

[dependencies]

See the IPC Setup guide for details.


Is acton-reactive production ready?

Yes. acton-reactive is stable, well-tested, and follows semantic versioning: breaking changes only land in major releases. We recommend:

  • Pinning to a major version in production (e.g. acton-reactive = "8")
  • Reviewing changelogs before upgrading across major versions

How do I debug message flow?

Enable tracing to see what's happening:

use tracing_subscriber;

#[acton_main]
async fn main() {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::DEBUG)
        .init();

    // Your code here
}

You'll see messages like:

DEBUG acton_reactive: Actor actor_1 received message: Increment
DEBUG acton_reactive: Handler for Increment completed

How fast is acton-reactive?

It depends on your use case, but generally:

  • Message throughput: Hundreds of thousands of messages per second on modern hardware
  • Latency: Sub-millisecond for simple handlers
  • Memory: Each actor adds minimal overhead (mostly channel buffers)

For performance-critical applications, profile your specific workload. The act_on handlers run concurrently, which can significantly improve throughput for read-heavy workloads.


Can I use acton-reactive with async database clients?

Absolutely! Handlers return futures, so you can use any async code:

actor.mutate_on::<SaveUser>(|actor, ctx| {
    let user = ctx.message().user.clone();
    let db = actor.model.db_pool.clone();

    Reply::pending(async move {
        sqlx::query("INSERT INTO users ...")
            .bind(&user.name)
            .execute(&db)
            .await
            .expect("DB error");
    })
});

How do I handle errors in handlers?

Use fallible handlers with Reply::try_* helpers:

// Immediate result (sync)
actor.try_mutate_on::<RiskyOperation>(|actor, ctx| {
    if something_bad() {
        Reply::try_err(MyError::new("something went wrong"))
    } else {
        Reply::try_ok(SuccessResult)
    }
});

// Or with async operations
actor.try_mutate_on::<RiskyOperation>(|actor, ctx| {
    Reply::try_pending(async move {
        let result = do_risky_thing().await?;
        Ok(SuccessResult { data: result })
    })
});

// Register error handler
actor.on_error::<RiskyOperation, MyError>(|actor, ctx, error| {
    println!("Error: {}", error);
    Reply::ready()
});

Common Gotchas

Gotcha: Borrowing actor in async blocks

Problem:

actor.mutate_on::<MyMessage>(|actor, ctx| {
    Reply::pending(async move {
        // ERROR: actor.model is borrowed in async block
        println!("{}", actor.model.value);
    })
});

Solution: Clone what you need before the async block:

actor.mutate_on::<MyMessage>(|actor, ctx| {
    let value = actor.model.value; // Clone before async

    Reply::pending(async move {
        println!("{}", value);  // Use the clone
    })
});

Gotcha: Forgetting to await shutdown

Problem:

fn main() {
    let runtime = tokio::runtime::Runtime::new().unwrap();
    runtime.block_on(async {
        let mut app = ActonApp::launch_async().await;
        // ... do stuff
    }); // Runtime drops, actors may not finish!
}

Solution: Always await shutdown_all:

#[acton_main]
async fn main() {
    let mut app = ActonApp::launch_async().await;
    // ... do stuff
    app.shutdown_all().await.expect("Shutdown failed");
}

Gotcha: Using the wrong handler type

Problem: Using act_on (read-only) when you need to mutate state:

// This won't compile!
actor.act_on::<Increment>(|actor, ctx| {
    actor.model.count += 1;  // ERROR: actor.model is immutable
    Reply::ready()
});

Solution: Use mutate_on for mutations:

actor.mutate_on::<Increment>(|actor, ctx| {
    actor.model.count += 1;  // Works!
    Reply::ready()
});

Gotcha: Subscription order matters

Problem: Subscribing after messages are sent means you miss them:

broker.broadcast(ImportantEvent).await;  // Sent before subscription!
actor.handle().subscribe::<ImportantEvent>().await;  // Too late

Solution: Subscribe before starting, or before any broadcasts:

actor.handle().subscribe::<ImportantEvent>().await;  // Subscribe first
let handle = actor.start().await;
// Now safe to broadcast

Gotcha: Deadlock from synchronous send

Problem: Calling send_sync from within a handler can deadlock if the channel is full:

actor.mutate_on::<Trigger>(|actor, ctx| {
    // DANGER: If the channel is full, this blocks forever
    some_handle.send_sync(BlockingMessage, &address);
    Reply::ready()
});

Solution: Use async send inside handlers:

actor.mutate_on::<Trigger>(|actor, ctx| {
    let handle = some_handle.clone();

    Reply::pending(async move {
        handle.send(AsyncMessage).await;  // Non-blocking
    })
});

Gotcha: Message type confusion

Problem: Two message types with the same name in different modules:

mod a {
    use acton_reactive::prelude::*;
    #[acton_message]
    pub struct Event { pub value: i32 }
}

mod b {
    use acton_reactive::prelude::*;
    #[acton_message]
    pub struct Event { pub data: String }  // Different type!
}

// Handler registered for a::Event
actor.mutate_on::<a::Event>(|actor, ctx| { ... });

// But sending b::Event - handler won't fire!
handle.send(b::Event { data: "test".into() }).await;

Solution: Be explicit about types, and consider unique naming:

use acton_reactive::prelude::*;

#[acton_message]
pub struct SensorEvent { pub value: i32 }

#[acton_message]
pub struct UserEvent { pub data: String }

Performance Tips

Tip: Use act_on for read-heavy workloads

If a handler only reads state and doesn't modify it, use act_on instead of mutate_on. Multiple act_on handlers can run concurrently:

// These can run in parallel
actor.act_on::<Query1>(|actor, ctx| { /* read-only */ });
actor.act_on::<Query2>(|actor, ctx| { /* read-only */ });
actor.act_on::<Query3>(|actor, ctx| { /* read-only */ });

Tip: Clone data, not the whole actor

When you need data in an async block, clone just what you need:

// Good - clone only what's needed
let value = actor.model.expensive_data.clone();

Reply::pending(async move {
    use_value(value).await;
})

// Bad - cloning entire model when you only need one field
let model = actor.model.clone();

Reply::pending(async move {
    use_value(model.expensive_data).await;
})

Tip: Adjust inbox capacity for your workload

High-throughput actors might benefit from larger inbox buffers:

# ~/.config/acton/config.toml
[limits]
actor_inbox_capacity = 1000  # Default is 512

Tip: Use MessagePack for IPC

If you're using IPC heavily, MessagePack is faster and smaller than JSON:

[dependencies]

Still Stuck?

If you can't find the answer here:

  1. Check the Examples - They cover most common patterns
  2. Read the API Reference - Every type is documented
  3. Look at the tests in the acton-reactive repository - They're essentially documentation
  4. Open an issue on GitHub - We're happy to help

Migration Guide: v4.x to v5.0

Version 5.0 renamed the handler methods for clarity:

Handler Renames

v4.xv5.0Purpose
act_on (mutable)mutate_onMutable state access, sequential
-act_onRead-only state access, concurrent

Before (v4.x)

builder.act_on::<MyMessage>(|actor, _| {
    actor.model.value += 1;  // Was mutable in v4
    Reply::ready()
});

After (v5.0)

// For mutations, use mutate_on
builder.mutate_on::<MyMessage>(|actor, _| {
    actor.model.value += 1;
    Reply::ready()
});

// act_on is now read-only and concurrent
builder.act_on::<QueryMessage>(|actor, _| {
    let value = actor.model.value;  // Read-only
    Reply::ready()
});

Quick Migration

  1. Replace all act_on calls that mutate state with mutate_on
  2. Keep act_on for read-only operations (they'll now run concurrently!)
  3. Run cargo build - the compiler will catch any mistakes
Previous
Troubleshooting