#futures #actors #actix

lwactors

Lightweight actors for Rust using futures-rs

9 releases

0.2.0 Dec 28, 2020
0.1.1 Jan 8, 2020
0.1.0 Nov 8, 2019
0.0.6 Aug 20, 2019
0.0.4 Feb 25, 2018

41 downloads per month

MIT/Apache

13KB
106 lines

lwactors

Build Status

Lightweight actors for Rust using futures-rs.

Introduction

The TL;DR is that you probably want to use Actix instead.

This library allows standalone "actors" to exist in Applications using Futures.

An actor is created with an initial state. From there the state can only be queried or modified by passing messages using the invoke function, receiving a future which will contain a result once the message has been processed. The messages are processed asynchronously.

See the "counter" example for an suggestion of how to build actors around this library.

Overview

The actor itself

An application would contain a bespoke struct for each actor, which would own a reference to the underlying future:

#[derive(Clone)]
struct Counter {
    queue: ActorSender<CounterAction, i64, CounterError>,
}

When constructed lwactors actor function is called to start the actor running:

fn new(cpu_pool: &CpuPool) -> Counter {
    Counter {
        queue: actor(cpu_pool, 0),
    }
}

it requires a reference to a CpuPool (or another Executor) and an initial state, in this case 0.

Action - the set of possible messages

The application will then define the set of messages that can be sent to the actor:

#[derive(Debug)]
enum CounterAction {
    Add(i64),
    Subtract(i64),
}

and the code that the actor will run when processing the message:

impl Action<i64, i64, CounterError> for CounterAction {
    fn act(self, state: &mut i64) -> Result<i64, CounterError> {
        println!("Acting {:?} on {}", self, state);
        match self {
            CounterAction::Add(i) => *state += i,
            CounterAction::Subtract(i) => *state -= i,
        }
        return Ok(*state);
    }
}

Usage

Finally, to expose a friendly high-level API, the application can implement specific functions which are responsible for sending messages to the actor:

impl Counter {
    // new function quoted above goes here

    fn add(&self, n: i64) -> CounterFuture {
        self.queue.invoke(CounterAction::Add(n))
    }

    fn subtract(&self, n: i64) -> CounterFuture {
        self.queue.invoke(CounterAction::Subtract(n))
    }
}

The struct that contains the reference to the actor Counter is cheaply cloneable, but all refer to the same running future that processes the messages, so all indirectly refer to the same shared state. Each thread that owns a Counter can call add or subtract to send messages, receiving a future that resolves to an i64 which is the state of the counter after that message has been processed.

The running future that processes messages will complete successfully once all the Counters have been dropped.

Dependencies

~1–1.6MB
~35K SLoC