titanclass / stage

A minimal actor model library using nostd Rust and designed to run with any executor

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

* * * EXPERIMENTAL * * *

Stage

Test

A minimal actor model library targeting nostd Rust consisting of just two traits and two types, and designed to run with any executor.

Why are actors useful?

Actors provide a stateful programming convenience for concurrent computations. Actors can only receive messages, send more messages, and create more actors. Actors are guaranteed to only ever receive one message at a time and can maintain their own state without the concern of locks. Given actor references, location transparency is also attainable where the sender of a message has no knowledge of where the actor's execution takes place (current thread, another thread, another core, another machine...).

Actors are particularly good at hosting Finite State Machines, particularly event-driven ones.

Why Stage?

Stage's core library stage_core provides a minimal set of types and traits required to sufficiently express an actor model, and no more. The resulting actors should then be able to run on the popular async runtimes available, including tokio and async-std.

Inspiration

We wish to acknowledge the Akka project as a great source of inspiration, with the authors of Stage having applied Akka over the years. Akka's goal is to, "Build powerful reactive, concurrent, and distributed applications more easily". We hope that Stage can be composed with other projects to achieve the same goals while using Rust.

In a nutshell

The essential traits and types are Actor, ActorRef and ActorContext. Actors need to run on something, and they are dispatched to whatever that is via a Dispatcher. Other crates such as stage_dispatch_crossbeam_executors are available to provide an execution environment.

Actor declarations look like this:

struct Greet {
    whom: String,
}

struct HelloWorld {}

impl Actor<Greet> for HelloWorld {
    fn receive(&mut self, context: &mut ActorContext<Greet>, message: &Greet) {
        println!("Hello {}!", message.whom);
    }
}

For Tokio, dispatcher and mailbox setup looks like the following. We use Tokio's preferred bounded channels being set to 10 pending system messages (unbounded channels are also available):

let (command_tx, command_rx) = channel(10);
let dispatcher = Arc::new(TokioDispatcher { command_tx });

Each actor has its own queue of messages, named a "mailbox", and a channel is supplied by a mailbox_fn factory function. Again, we can use bounded or unbounded channels, and we use a bounded one with 100 pending actor-destined messages. This bound is generally higher for the actor than the dispatcher as the dispatcher tends to do less.

let mailbox_fn = Arc::new(mailbox_fn(100));

As an alternative to Tokio, we can use the stage_dispatch_crossbeam_executors work-stealing pool for 4 processors along with an unbounded channel for communicating with it and for each actor:

let pool = crossbeam_workstealing_pool::small_pool(4);
let (command_tx, command_rx) = unbounded();
let dispatcher = Arc::new(WorkStealingPoolDispatcher { pool, command_tx });

let mailbox_fn = Arc::new(unbounded_mailbox_fn());

No matter what dispatcher is used, the rest of the code is the same. Sending a message to an actor looks like this:

let system = ActorContext::<Greet>::new(
    || Box::new(HelloWorld {}),
    dispatcher,
    mailbox_fn,
);

system.actor_ref.tell(SayHello {
    name: "Stage".to_string(),
});

We are also able to perform request/reply scenarios using ask. For example, using tokio as a runtime:

actor_ref.ask(
    &|reply_to| Request {
        reply_to: reply_to.to_owned(),
    },
    Duration::from_secs(1),
)

The ask method of an actor reference takes a function that is responsible for producing a request with a reply_to actor reference. Asks always take a timeout parameter which, in the case of Tokio, may return an Elapsed error.

For complete examples, please consult the tests associated with each dispatcher library.

What about...

Channels

Actors here build on channels and associate state with the receiver. The type system is used to enforce actor semantics; in particular, requiring a single receiver so that an actor's state can be mutated without contention.

async/await within an actor

Using async/await (Futures) within an actor's receive method would permit calling out to async functions of other libraries. However, a danger here is that these async functions may block indefinitely as there is no contractual obligation to ever return (an issue for discussing the contractual obligations of async functions has been raised on the Rust internals forum). Blocking would prevent an actor from processing other messages in its mailbox.

Another argument here is that actors can be considered orthoganal to async/await. Actors make great state machines, and receiving commands, including ones to stop the state machine, should not be blocked from processing.

Finally, async functions can call into actors by using the ActorRef.ask async method call. Therefore, async functions and actors are able to co-exist and potentially serve distinct use-cases.

What about actor supervision?

Actor model libraries often include supervisory functions, although this is not a requirement of the actor model per se.

We believe that supervisory concerns should be external to Stage. However, we may need to provide the ability to watch actors so that supervisor implementations can be achieved. That said, we have found that trying to recover from unforeseen events may point to a design concern. In many cases, panicking and having the process die (and then restart given OS level supervision), or restarting an embedded device, will be a better course of action.

What about actor naming?

Many libraries permit their actors to be named. We see the naming of actors as an external concern e.g. maintaining a hash map of names to actor refs.

What about distributing actors across a network?

Stage does not concern itself directly with networking. A custom dispatcher should be able to be written that dispatches work across a network.

Contribution policy

Contributions via GitHub pull requests are gladly accepted from their original author. Along with any pull requests, please state that the contribution is your original work and that you license the work to the project under the project's open source license. Whether or not you state this explicitly, by submitting any copyrighted material via pull request, email, or other means you agree to license the material under the project's open source license and warrant that you have the legal authority to do so.

License

This code is open source software licensed under the Apache-2.0 license.

© Copyright Titan Class P/L, 2020

About

A minimal actor model library using nostd Rust and designed to run with any executor

License:Apache License 2.0


Languages

Language:Rust 100.0%