Building Apps

Request-Response

While send is great for fire-and-forget, many scenarios require getting data back from an actor. Acton uses the reply envelope pattern for request-response communication.

The Reply Envelope Pattern

Every message arrives in an envelope that knows where it came from. Use envelope.reply_envelope() to send a response back:

// Actor that responds to queries
builder.act_on::<GetCount>(|actor, envelope| {
    let count = actor.model.count;
    let reply_envelope = envelope.reply_envelope();

    Reply::pending(async move {
        reply_envelope.send(CountResponse(count)).await;
    })
});

The sender must have a handler for the response message:

// Requesting actor handles the response
requester.mutate_on::<CountResponse>(|actor, envelope| {
    let count = envelope.message().0;
    println!("Received count: {}", count);
    Reply::ready()
});

Example: Key-Value Store

use acton_reactive::prelude::*;
use std::collections::HashMap;

#[acton_actor]
#[derive(Default)]
struct KVStore {
    data: HashMap<String, String>,
}

#[acton_actor]
#[derive(Default)]
struct Client;

#[acton_message]
struct Set { key: String, value: String }

#[acton_message]
struct Get { key: String }

#[acton_message]
struct GetResponse(Option<String>);

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

    // Create the store
    let mut store = runtime.new_actor::<KVStore>();

    store
        .mutate_on::<Set>(|actor, envelope| {
            let msg = envelope.message();
            actor.model.data.insert(msg.key.clone(), msg.value.clone());
            Reply::ready()
        })
        .act_on::<Get>(|actor, envelope| {
            let key = &envelope.message().key;
            let value = actor.model.data.get(key).cloned();
            let reply_envelope = envelope.reply_envelope();

            Reply::pending(async move {
                reply_envelope.send(GetResponse(value)).await;
            })
        });

    let store_handle = store.start().await;

    // Create a client that queries the store
    let mut client = runtime.new_actor::<Client>();

    client.mutate_on::<GetResponse>(|_actor, envelope| {
        let value = &envelope.message().0;
        println!("Got: {:?}", value);
        Reply::ready()
    });

    let client_handle = client.start().await;

    // Store some data
    store_handle.send(Set {
        key: "name".into(),
        value: "Acton".into()
    }).await;

    // Query using an envelope addressed to the store, with reply going to client
    let query_envelope = client_handle.create_envelope(
        Some(store_handle.reply_address())
    );
    query_envelope.send(Get { key: "name".into() }).await;

    // Give time for async processing
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;

    runtime.shutdown_all().await.ok();
}

Output:

Got: Some("Acton")

When to Use Each Pattern

Use send (fire-and-forget)Use reply envelopes
Commands (do something)Queries (get something)
NotificationsData requests
High throughput neededCoordination required
Don't care about resultNeed the result

Fire-and-Forget Default

send is Acton's primary pattern. Use reply envelopes when you specifically need response data. This keeps your system decoupled and performant.


Handling Missing Data

Return meaningful responses when data might not exist:

builder.act_on::<FindUser>(|actor, envelope| {
    let user_id = &envelope.message().id;
    let user = actor.model.users.get(user_id).cloned();
    let reply_envelope = envelope.reply_envelope();

    Reply::pending(async move {
        match user {
            Some(u) => reply_envelope.send(UserFound(u)).await,
            None => reply_envelope.send(UserNotFound).await,
        }
    })
});

The requesting actor handles both cases:

requester
    .mutate_on::<UserFound>(|_actor, envelope| {
        let user = &envelope.message().0;
        println!("Found user: {}", user.name);
        Reply::ready()
    })
    .mutate_on::<UserNotFound>(|_actor, _envelope| {
        println!("User not found");
        Reply::ready()
    });

Next

Error Handling — Building resilient systems

Previous
Parent-child actors