30 releases

0.4.1 Aug 12, 2023
0.3.17 Aug 12, 2023
0.3.16 Jul 28, 2023
0.3.12 Jan 22, 2023
0.3.11 Oct 7, 2022

#1786 in Asynchronous

MIT license

130KB
3K SLoC

agner — processes actors.

Note: Right now this is a research project, i.e. it is possible that the API will undergo incompatible changes within the version 0.3.x.

As it has been stated, agner is inspired by Erlang/OTP, so all similarities to the actual frameworks, supported or obsolete, are purely intentional. :)

Actors

An actor is an activity with the following properties:

  • runs in parallel (implemented as a Future);
  • has a handle (ActorID);
  • can receive messages;
  • when terminates — yields an exit reason (Exit);
  • any two actors can be linked with each other:
    • if one of the linked actors exits with a reason other than Exit::normal() — the other receives an exit-signal;
    • if the process receiving an exit-signal does not "trap exits", it will also be terminated.

Implementing an Actor

The actor's behaviour is defined by:

  • the type of its argument;
  • the type of the message it accepts;
  • the behaviour function.

In order to implement an actor one should define an async function that

  • returns a value for which the trait Into<Exit> is defined
  • and accepts two arguments:

Example:

use agner::actors::{Context, Exit, Never};

async fn shutdown_after_six_messages(context: &mut Context<String>, actor_name: String) {
    for i in 0..6 {
        let message = context.next_message().await;
        eprintln!("Actor {:?} received {:?}", actor_name, message);
    }
}

Spawning an Actor

Actors cannot run on their own, they need an actor system to be spawned in. This is necessary to avoid having a global state imposed by mere usage of the library. A System is a scope within which the actors run.

Example:

use agner::actors::{System, Context, Exit, Never};

async fn a_dummy(context: &mut Context<Option<String>>, actor_name: String) {
    eprintln!("{}: hi!", actor_name);

    while let Some(message) = context.next_message().await {
        eprintln!("{}: received {:?}", actor_name, message);
    }

    eprintln!("{}: bye!", actor_name);
}

#[tokio::main]
async fn main() {
    // create a system with default configuration
    let system = System::new(Default::default());

    let actor_id = system.spawn(
        a_dummy,
        "the-dummy".to_owned(),
        Default::default())
            .await.expect("Failed to spawn an actor");

    system.send(actor_id, Some("one".to_owned())).await;
    system.send(actor_id, Some("two".to_owned())).await;
    system.send(actor_id, Some("three".to_owned())).await;
    system.send(actor_id, Option::<String>::None).await;

    let exit_reason = system.wait(actor_id).await;
    eprintln!("{} exited: {:?}", actor_id, exit_reason);
}

Terminating an Actor

"Willful" Termination

Returning from the Behaviour Function

If the actor's behaviour function returns — the actor terminates. The return type of the behaviour function must implement the trait Into<Exit>.

Example:

use std::convert::Infallible;
use agner::actors::{Context, Exit};

async fn unit_is_normal_exit(_context: &mut Context<Infallible>, _args:()) {}

async fn result_into_exit(_context: &mut Context<Infallible>, _args:()) -> Result<(), Exit> {
    Ok(()) // Equivalent to `Err(Exit::normal())`
}

Invoking Context::exit

Example:

use std::convert::Infallible;
use agner::actors::{Context, Exit};

async fn normal_exit(context: &mut Context<Infallible>, args: ()) -> Infallible {
    context.exit(Exit::normal()).await;
    unreachable!()
}

Terminating from Outside

An actor can be terminated by invoking System::exit(&self, ActorID, Exit).

In this case an actor receives an exit-signal. If the actor "traps exits", it can perform a graceful shutdown (or even keep running). That is if the exit reason is not Exit::kill(): in this case the actor just terminates, and its linked actors in their turn receive exit-signals.

Supervision

As this whole project is based on the Erlang/OTP, it should not be a surprise that there are some (quite a lot of) similarities to the OTP Design Principles, namely:

Design Principles

Starting and Stopping

TBD:

Uniform Supervisor

TBD:

Mixed Supervisor

TBD:

Introspection

TBD:

Testing

TBD:

Dependencies

~3–9.5MB
~93K SLoC