Crate black_box

Crate black_box 

Source
Expand description

§Black-Box

Crates.io Documentation

A minimal, stage for actors.

§About

Black-box’s API design is inspired by actix, but is built to be as minimal as possible, and to integrate with the recently stabalized async fn in traits.

To get started just define an Actor and implement some message handlers:

use black_box::*;

// Messages do not need to implement anything
struct Event;

struct Shutdown;

struct MyActor;

// All methods are provided, but can be overridden for more control
impl Actor for MyActor {}

impl Handler<Event> for MyActor {
    async fn handle(&mut self, msg: Event, _ctx: &Context<Self>) {
        println!("DEBUG: New event {}", stringify!(msg));
    }
}

impl Handler<Shutdown> for MyActor {
    async fn handle(&mut self, _msg: Shutdown, ctx: &Context<Self>) {
        println!("INFO: Shutting down");
        // shutdown the actor from within the handler
        ctx.shutdown(); 
        // await futures inline thanks to async fn 
        tokio::time::sleep(std::time::Duration::from_millis(200)).await;
        println!("INFO: Shut down");
    }
}

// Spawn the actor on a runtime of your choosing
#[tokio::main]
async fn main() {
    let actor = MyActor;
    let (executor, address) = Executor::new(actor);

    tokio::spawn(async move {
        for _ in 0..5 {
            address.send(Event).await;
            tokio::time::sleep(std::time::Duration::from_millis(500)).await
        }
        address.send(Shutdown).await;
    });

    executor.run().await;
}

§Runtime

Black-box deliberatly does not ship with a runtime, instead it aims to work with the user’s runtime of choice.

§Send Bounds

While it likely won’t always be the case, currently the futures return by Handler::handle must be Send.

§Message Trait

Black-box does have a message trait, but currently private, and just a supertrait for 'static + Send.

This means messages are not special types, any 'static + Send type can immediately be used as a message.

In the future, this might change to accomodate things like message responses, however for now the same things can be accomplished by embedding a oneshot channel in the message type to handle the response.

type Message = (String, tokio::sync::oneshot::Sender<String>);

impl Handler<Message> for MyActor {
    async fn handle(&mut self, mut message: Message, _ctx: &Context<Self>) {
        message.0.push_str("foo");
        let _ = message.1.send(message.0);
    }
}

§Limitations

Before adopting black-box, there are a few drawbacks to consider.

§Concurrency

The lack of a runtime and the decision to provide &mut self in Handler means that there is no native concurrency within actors in black-box. Awaiting a future within Handler::handle will not prevent messages from enqueuing, but it will prevent the processing of those events.

For many applications this will be fine. However, for applications which require high concurrency, you will likely want to either

  1. Spawn many actors to handle the work
  2. Offload the bulk of the asynchronous work to a task spawned onto some executor.

§Allocations

An implementation of the actor pattern which is based purely around channels would likely either:

  1. Create many channels over which to send different message types
  2. Collect all the message types for a given actor into an enum and descructure the enum, and pass it down to functions

To get around these rough spots, black-box conducts type-erasure via Box<dyn Any>, when the message is sent, then reconstructs the message type before passing it to the appropriate message handler.

This unfortunately results in two allocation for every message, one for this, and one for the handle future.

Structs§

Address
A cloneable address which can be used to send messages to the associated Actor
Context
A cloneable context for the actor.
Executor
The event loop for an actor

Traits§

Actor
Abstraction for message handleing
Handler
The implementation for how an actor handles a particular message