Skip to content

asymmetry/acktor

Repository files navigation

acktor

Crates.io docs.rs CI codecov License: MIT

A pure-Rust actor framework built on top of the Tokio async runtime, inspired by Alice Ryhl's Actors with Tokio.

About

acktor is an actor framework for Rust that builds on the patterns described in Alice Ryhl's blog post and extends them into a structured library. Each actor runs as an independent tokio task with its own mailbox, processing messages one at a time. Actors communicate exclusively through message passing — there is no shared mutable state. The framework provides lifecycle hooks, supervision, an observer pattern, and support for periodic tasks.

Installation

Install acktor by adding it to your Cargo.toml:

[dependencies]
acktor = "1.0"

Requires Rust 1.88 or later.

Quick Start

An example Counter actor that handles arithmetic messages might be the following:

use acktor::{Actor, Context, Handler, Message, Signal};

// 1. Define your actor
#[derive(Debug)]
struct Counter(i64);

impl Actor for Counter {
    type Context = Context<Self>;
    type Error = String;
}

// 2. Define a message
#[derive(Debug, Message)]
#[result_type(i64)]
enum CounterMsg {
    Increment,
    Get,
}

// 3. Implement the handler
impl Handler<CounterMsg> for Counter {
    type Result = i64;

    async fn handle(&mut self, msg: CounterMsg, _ctx: &mut Self::Context) -> i64 {
        match msg {
            CounterMsg::Increment => self.0 += 1,
            CounterMsg::Get => {}
        }
        self.0
    }
}

#[tokio::main]
async fn main() {
    let (addr, handle) = Counter(0).start("counter").unwrap();

    // fire-and-forget
    addr.do_send(CounterMsg::Increment).await.unwrap();

    // request-reply
    let result = addr.send(CounterMsg::Get).await.unwrap().await.unwrap();
    println!("Counter: {}", result); // Counter: 1

    addr.do_send(Signal::Stop).await.unwrap();
    handle.await.unwrap();
}

Supervision

Implement Handler<SupervisionEvent<A>> on the supervisor actor. Use the command Supervisor::Set to attach the supervisor actor to the child (or Supervisor::Unset to detach). Since every actor handles Supervisor<A> automatically, no extra wiring is needed on the child side.

use acktor::{Actor, Context, Handler, supervisor::SupervisionEvent};

struct Worker;

impl Actor for Worker {
    type Context = Context<Self>;
    type Error = String;
}

#[derive(Default)]
struct Watchdog;

impl Actor for Watchdog {
    type Context = Context<Self>;
    type Error = String;
}

impl Handler<SupervisionEvent<Worker>> for Watchdog {
    type Result = ();

    async fn handle(&mut self, event: SupervisionEvent<Worker>, _ctx: &mut Self::Context) {
        println!("worker event: {:?}", event);
    }
}

Observer

For a subject actor, implement the SubjectActor<Event> trait so it can emit Events to registered observers. Every subject actor automatically gets a Handler<Observer<Event>> implementation that manages the observers, so the observers can be registered by sending Observer::Register commands to the subject actor (or Observer::Unregister to stop receiving events).

use acktor::{Actor, Context, Message, observer::{ObserverSet, SubjectActor}};

#[derive(Clone, Message)]
#[result_type(())]
struct Tick;

#[derive(Default)]
struct Clock { observers: ObserverSet<Tick> }

impl Actor for Clock {
    type Context = Context<Self>;
    type Error = String;
}

impl SubjectActor<Tick> for Clock {
    fn observers_mut(&mut self) -> &mut ObserverSet<Tick> { &mut self.observers }
}

Cron Tasks

Use CronContext<Self> as the actor's context and implement CronActor trait to opt in this feature. CronActor trait defines a task method that is invoked repeatedly with a delay determined by its return value.

use std::time::Duration;
use acktor::{Actor, cron::{CronActor, CronContext}};

struct Heartbeat;

impl Actor for Heartbeat {
    type Context = CronContext<Self>;
    type Error = String;
}

impl CronActor for Heartbeat {
    async fn task(&mut self, _ctx: &mut Self::Context) -> Result<Duration, Self::Error> {
        println!("tick");
        Ok(Duration::from_secs(1))
    }
}

IPC Support

Actors in different processes can talk to each other through the acktor-ipc crate. Enable the ipc feature on acktor, add acktor-ipc to your dependencies, mark the actors you want to expose with #[derive(RemoteAddressable)] + #[remote], and connect them through a Node over a pipe or websocket transport. See the acktor-ipc README and the pingpong example for a full walkthrough.

Feature Flags

Defaults: derive, observer, cron.

Feature Purpose
derive Re-exports the derive macros from acktor-derive.
observer Enables the observer module.
cron Enables the cron module.
identifier Enables stable type identifiers.
ipc Enables IPC support (codec module, remote addressing).
prost-codec Use an all-prost primitive codec instead of the zerocopy mix.
bottleneck-warning Logs when an observer's mailbox is full.
tokio-tracing Names actor tasks for tokio-console (needs tokio_unstable).

License

This project is licensed under MIT.

About

Pure-Rust actor framework built on top of the Tokio async runtime

Resources

Stars

Watchers

Forks

Releases

Used by

Contributors

Languages