13 releases

0.4.2 Nov 30, 2020
0.4.1 Jun 19, 2020
0.4.0 May 7, 2020
0.3.2 Aug 25, 2019
0.1.0 Jul 17, 2018

#665 in Asynchronous

Download history 1/week @ 2023-11-20 6/week @ 2023-11-27 2/week @ 2024-02-12 54/week @ 2024-02-19 41/week @ 2024-02-26 18/week @ 2024-03-04

115 downloads per month
Used in 2 crates

MIT license

11KB

Riker Patterns

Overview

The patterns crate provides a collection of common patterns used in actor systems.

Currently this only includes the Ask pattern and a behavior change pattern that provides a transform! macro.

The intention is to add many patterns, some of which are well documented in popular actor programming books, such as Reactive Messaging Patterns with the Actor Model.

Patterns:

Ask

The Ask pattern allows values to be sent by actors to outside of the actor system. The value is delivered as a Future.

Let's look at how this works: Cargo.toml:

[dependencies]
riker = "0.3.0"
riker-patterns = "0.3.0"

main.rs:

use futures::future::RemoteHandle;
use riker_patterns::ask::*;

struct MyActor;

impl Actor for MyActor {
    type Msg = u32;

    fn recv(&mut self,
            ctx: &Context<Self::Msg>,
            msg: Self::Msg,
            sender: Sender) {

        // sender is the Ask, waiting to a message to be sent back to it
        sender.try_tell(msg * 2, Some(ctx.myself().into()));
    }
}

fn main() {
    let sys = ActorSystem::new().unwrap();

    let props = Props::new(Box::new(MyActor::new));
    let my_actor = sys.actor_of(props, "my-actor");

    // ask returns a future that automatically is driven
    // to completion by the system.
    let res: RemoteHandle<u32> = ask(&sys, &my_actor, 100);

    // the result future can be passed to a library or fuction that
    // expects a future, or it can be extracted locally using `block_on`.

    let res = block_on(res).unwrap();
    println!("The result value is: {}", res);
}

In the background Ask sets up a temporary intermediate actor that lives for the lifetime of the ask. Other actors see this temporary actor as the sender and can send a message back to it. When the temporary ask actor receives a message it fulfills the outstanding future and performs a stop on itself to cleanup.

Ask is particularly useful when you have part of an application that runs outside of the actor system, or in another actor system, such as a web server (e.g. Hyper) serving API requests. The resulting future can then be chained as part of the future stack.

Transform

Transform makes changing actor behavior based on its current state easier to reason about. Since actors maintain state, and indeed is a primary concern, being able to handle messages differently based on that state is important. The Transform pattern separates message handling by dedicating a receive function per state. This saves excessive matching to handle several possible states, i.e. handling behavior is pre-empted at the time of state change instead of on each message receive.

!!! info If you're familair with Akka on the JVM, transform resembles become.

Example:

#[macro_use]
use riker_patterns::transform::*;

impl ShoppingCart {
    // created state
    fn created(&mut self,
                ctx: &Context<MyMsg>,
                msg: MyMsg,
                sender: Sender) {

        match msg {
            MyMsg::AddItem(item) => {
                // add item to cart
                ...
            }
            MyMsg::Cancel => {
                // future messages will be handled by `fn cancelled`
                transform!(self, UserActor::cancelled);
            }
            MyMsg::Checkout => {
                // future messages will be handled by `fn checkout`
                transform!(self, UserActor::checkout);
            }
        }
    }

    // cancelled state
    fn cancelled(&mut self,
                ctx: &Context<MyMsg>,
                msg: MyMsg,
                sender: Sender) {

        match msg {
            MyMsg::AddItem(item) => {
                // can't add item to cancelled cart
                ...
            }
            MyMsg::Cancel => {
                // can't cancel an already cancelled cart
                ...
            }
            MyMsg::Checkout => {
                // can't checkout out a canclled cart
                ...
            }
        }
    }

    // checkout state
    fn checkout(&mut self,
                ctx: &Context<MyMsg>,
                msg: MyMsg,
                sender: Sender) {

        match msg {
            MyMsg::AddItem(item) => {
                // can't add item to cart during checkout
                ...
            }
            MyMsg::Cancel => {
                // future messages will be handled by `fn cancelled`
                transform!(self, UserActor::cancelled);
            }
            MyMsg::Checkout => {
                // cart already in checkout process
                ...
            }
        }
    }
}

impl Actor for ShoppingCart {
    type Msg = MyMsg;

    fn recv(&mut self,
            ctx: &Context<Self::Msg>,
            msg: Self::Msg,
            sender: Sender) {

        // just call the currently set transform function
        (self.rec)(self, ctx, msg, sender)
    }
}

The transform! macro expects the field name of the current receive function on self to be named rec. It's easy to use a different name and either use your own macro, or just set the fuction using standard code. The advantage of transform! is that it is easy to read and identify when transformation is happening since it is distinct from standard code.

Dependencies

~10MB
~194K SLoC