Reference

Cheatsheet

Quick reference for common patterns. Copy, paste, and adapt.

Basic Actor Setup

use acton_reactive::prelude::*;

#[acton_actor]
struct MyActor {
    // your state here
}

#[acton_message]
struct MyMessage;

#[acton_main]
async fn main() {
    let mut runtime = ActonApp::launch_async().await;

    let mut builder = runtime.new_actor::<MyActor>();
    builder.mutate_on::<MyMessage>(|actor, _envelope| {
        // handle message
        Reply::ready()
    });

    let handle = builder.start().await;

    handle.send(MyMessage).await;
    runtime.shutdown_all().await.ok();
}

Message Patterns

Fire-and-Forget

handle.send(DoSomething).await;

Request-Response (ask)

// Declare the reply type once
impl Request for GetValue {
    type Response = ValueResponse;
}

// Server actor — unchanged; it answers through the reply envelope
builder.act_on::<GetValue>(|actor, envelope| {
    let value = actor.model.value;
    let reply = envelope.reply_envelope();
    Reply::pending(async move {
        reply.send(ValueResponse(value)).await;
    })
});

// Caller waits for the answer
let value = handle.ask(GetValue).await?;

// With an explicit deadline instead of DEFAULT_ASK_TIMEOUT (30s)
let value = handle.ask_with_timeout(GetValue, Duration::from_millis(250)).await?;

A completed ask also proves everything sent to that actor beforehand was processed, because inboxes are FIFO. Never ask from inside a mutate_on handler — it deadlocks.

Request-Response (Reply Envelope, actor to actor)

For an exchange inside a handler, where asking would deadlock, or when one request produces several replies:

// Requester sends, and handles the answer as an ordinary message
client.mutate_on::<ValueResponse>(|actor, envelope| {
    let value = envelope.message().0;
    println!("Got: {}", value);
    Reply::ready()
});

Broadcast

// Publisher
let broker = runtime.broker();
broker.broadcast(Event { data: "hello".into() }).await;

// Subscriber (before starting)
builder.mutate_on::<Event>(|actor, envelope| {
    println!("Got: {}", envelope.message().data);
    Reply::ready()
});
builder.handle().subscribe::<Event>().await;
let handle = builder.start().await;

// Stop receiving Event broadcasts (automatic on actor stop);
// use unsubscribe_async::<Event>().await to await delivery to the broker
handle.unsubscribe::<Event>();

Wait for a Broadcast to Land

broker.broadcast(Event { data: "hello".into() }).await;

// Delivery: every earlier broadcast is now in every subscriber's inbox
broker.ask(FlushBroadcasts).await?;

// Completion for one subscriber: the broadcast is ahead of this in its inbox
let seen = subscriber_handle.ask(GetSeen).await?;

shutdown_all flushes the broker for you, so broadcasting and then shutting down is not a race.


Handler Patterns

Mutate State

builder.mutate_on::<Increment>(|actor, _envelope| {
    actor.model.count += 1;
    Reply::ready()
});

Mutate State (Sync — No Future Allocation)

builder.mutate_on_sync::<Increment>(|actor, _envelope| {
    actor.model.count += 1;
});

Read State with Reply

builder.act_on::<GetCount>(|actor, envelope| {
    let count = actor.model.count;
    let reply = envelope.reply_envelope();
    Reply::pending(async move {
        reply.send(CountResponse(count)).await;
    })
});

Async Handler

builder.act_on::<Compute>(|actor, envelope| {
    let input = envelope.message().input;
    let reply = envelope.reply_envelope();
    Reply::pending(async move {
        let result = compute(input).await;
        reply.send(ComputeResponse { result }).await;
    })
});

Handler futures must be Send + Sync

The async block you pass to Reply::pending must be Send + **Sync**. That's stricter than the usual Send, and it rules out plenty of third-party futures (many HTTP and database clients). If the compiler says "future created by async block is not Sync", use the spawn-and-report-back pattern below.

Async Handler (Non-Sync Future — HTTP, DB, …)

Spawn the work with tokio::spawn (which only needs Send) and message the result back:

builder.mutate_on::<FetchData>(|actor, envelope| {
    let handle = actor.handle().clone();
    let url = envelope.message().url.clone();

    tokio::spawn(async move {
        let resp = reqwest::get(&url).await.unwrap();
        let body = resp.text().await.unwrap();
        handle.send(FetchResponse { body }).await;
    });

    Reply::ready()
});

// Then handle the result as a normal message
builder.mutate_on::<FetchResponse>(|actor, envelope| {
    actor.model.last_body = envelope.message().body.clone();
    Reply::ready()
});

Read-Only Handler (Sync — No Future Allocation)

builder.act_on_sync::<LogCount>(|actor, _envelope| {
    tracing::info!("count = {}", actor.model.count);
});

Fallible Handlers

Return a Result from a handler and handle the error separately.

builder
    .try_mutate_on::<Withdraw, Receipt, BankError>(|actor, ctx| {
        let amount = ctx.message().amount;
        let balance = actor.model.balance;
        Reply::try_pending(async move {
            if balance < amount {
                Err(BankError::InsufficientFunds { balance, amount })
            } else {
                Ok(Receipt { remaining: balance - amount })
            }
        })
    })
    .on_error::<Withdraw, BankError>(|actor, ctx, err| {
        tracing::error!("Withdrawal failed: {}", err);
        Reply::ready()
    });

Immediate results skip the async block entirely:

builder.try_act_on::<GetBalance, Balance, BankError>(|actor, _ctx| {
    Reply::try_ok(Balance(actor.model.balance))
});

Child Actors

Create and Supervise Child

Name the child and its parent, then register it with a blueprint the framework replays on every restart:

// Start the parent
let parent = runtime.new_actor::<ParentState>();
let parent_handle = parent.start().await;

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

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

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

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

Don't store an ActorHandle for a supervised child

A handle names one incarnation and goes stale when the child restarts, silently. Hold the SupervisedChild and call current() at the point of use.

create_child() is Idle-only

create_child() exists only on a builder (ManagedActor<Idle, _>), not on a running actor — so you can't call it from inside a handler, and it returns an actor with the same state type as its parent. To spawn a differently-typed worker, use runtime.new_actor_with_config() as above.

Access Children

children() lives on the handle, not on the actor:

for child in actor.handle().children().iter() {
    child.value().send(Ping).await;
}

Lifecycle Hooks

Before Start

builder.before_start(|actor| async move {
    println!("Actor starting!");
});

After Stop

builder.after_stop(|actor| async move {
    println!("Actor stopped");
});

Common State Patterns

With Default

#[acton_actor]
struct Counter {
    count: i32,  // defaults to 0
}

With Custom Default

Use no_default to stop the macro deriving Default, then write your own. (Adding #[derive(Default)] and a manual impl Default collides — you'd get "conflicting implementations".)

#[acton_actor(no_default)]
struct Config {
    timeout: Duration,
}

impl Default for Config {
    fn default() -> Self {
        Self { timeout: Duration::from_secs(30) }
    }
}

no_default is also how you hold a field whose type has no Default (a Stdout, a client handle, and so on).

With External Resources

#[acton_actor]
struct DbActor {
    pool: Option<PgPool>,
}

// Initialize via message or before_start

Error Handling

In Handlers

builder.mutate_on::<RiskyOp>(|actor, envelope| {
    match do_risky_thing() {
        Ok(result) => {
            actor.model.data = result;
            Reply::ready()
        }
        Err(e) => {
            tracing::error!("Failed: {}", e);
            Reply::ready()  // Actor continues
        }
    }
});

Signal Errors to Other Actors

builder.mutate_on::<Query>(|actor, envelope| {
    let reply = envelope.reply_envelope();
    match do_query() {
        Ok(data) => Reply::pending(async move {
            reply.send(QuerySuccess(data)).await;
        }),
        Err(e) => Reply::pending(async move {
            reply.send(QueryFailed(e.to_string())).await;
        }),
    }
});

Testing

Basic Test

#[tokio::test]
async fn test_actor() {
    let mut runtime = ActonApp::launch_async().await;

    let mut counter = runtime.new_actor::<Counter>();
    counter
        .mutate_on::<Increment>(|actor, _env| {
            actor.model.count += 1;
            Reply::ready()
        });

    let handle = counter.start().await;
    handle.send(Increment).await;

    // Wait on a reply, never on a sleep.
    assert_eq!(handle.ask(GetCount).await?.0, 1);

    runtime.shutdown_all().await?;
}

IPC

Expose an Actor (Server)

let mut runtime = ActonApp::launch_async().await;

// Register the message type so it can be deserialized from the wire
runtime.ipc_registry().register::<GetPrice>("GetPrice");

let mut service = runtime.new_actor_with_name::<PriceService>("prices".to_string());
service
    .act_on::<GetPrice>(|actor, ctx| { /* ... */ })
    .expose_for_ipc();          // reachable as "prices"
let handle = service.start().await;

let listener = runtime.start_ipc_listener().await?;

Connect and Send (Client)

IpcClient is the channel-based client — connect once, then send, request, or subscribe:

use acton_reactive::ipc::{IpcClient, IpcConfig, IpcEnvelope};

let client = IpcClient::connect(IpcConfig::load().socket_path()).await?;

// Request-response — new_request() sets expects_reply and generates a correlation ID
let response = client.request(IpcEnvelope::new_request(
    "prices",
    "GetPrice",
    serde_json::json!({ "symbol": "ACTON" }),
)).await?;

if response.success {
    println!("{:?}", response.payload);
}

// Fire-and-forget — IpcEnvelope::new() instead
client.send(IpcEnvelope::new(
    "prices",
    "RefreshCache",
    serde_json::json!({}),
)).await?;

client.disconnect().await?;

Subscribe to Broadcasts (Client)

client.subscribe(vec!["PriceUpdate".to_string()]).await?;
let mut pushes = client.take_push_receiver().expect("already taken");

while let Some(note) = pushes.recv().await {
    println!("push: {:?}", note.payload);
}

See IPC Setup and IPC Patterns for the full picture.


Quick Imports

// Everything you need
use acton_reactive::prelude::*;

// For IPC clients
use acton_reactive::ipc::{IpcClient, IpcConfig, IpcEnvelope};

// Only if you're speaking the wire protocol by hand
use acton_reactive::ipc::protocol::{write_envelope, read_response};

Common Mistakes

WrongRight
ActonApp::launch() from an async context (panics)ActonApp::launch_async().await. launch() is fine from fn main().
ctx.messagectx.message() — it's an accessor, not a field
actor.children()actor.handle().children()
actor.create_child(..) inside a handlercreate_child is Idle-only; use supervise_deferred(config, blueprint) from a handler
#[acton_actor] + #[derive(Default)] + manual impl Default#[acton_actor(no_default)] + manual impl Default
runtime.new_actor::<T>().mutate_on::<M>(h).start()Configure via &mut first, then builder.start().await
handle.ask(msg) inside a mutate_on handlerDeadlocks. Send instead, or ask from a task, a test, or main
ActorConfig::new(id, Some(parent), broker)?ActorConfig::for_supervised_child(name, parent, broker)?
ActorConfig::new(id, None, broker)?ActorConfig::new(id, broker) — infallible, no parent argument
Storing an ActorHandle for a supervised childStore the SupervisedChild; call current() at the point of use
Restarting a child from your own ChildTerminated handlerThe framework restarts children with blueprints. Yours would restart it twice
unsupervise to stop supervising but keep the child runningunsupervise stops the child; use release to keep it running
runtime.ipc_expose(name, handle);It returns Result<(), IpcNameInUse> — handle it
tokio::time::sleep to wait for a messagehandle.ask(..), or broker.ask(FlushBroadcasts) for a broadcast
Reply::with(value)Use Reply::pending + reply envelope
Forgetting .await on start()builder.start().await
Mutating in act_onUse mutate_on for state changes
Expecting a panic to kill an actorPanics are caught by default; use try_mutate_on + on_error
Previous
Examples