Crate tactix

Source
Expand description

A simple actor model implementation based on Tokio

§Tactix

Tactix is an erganomic Actor Model framework for Rust inspired by Actix

Actix provides a great API for working with actors but holds a large amount of technical debt and confusing code within it’s implementation. Actix as an early solution for asynchrony originally built out it’s own async runtime before moving to Tokio as a default runtime and as a consequence holds a fair amount of baggage from that time.

Alice Ryhl a maintainer of tokio wrote a great article on creating an actor model with tokio where it is outlined how to create an actor model system using tokio channels. This however leads to relatively verbose code as events must be discriminated.

Tactix attempts to apply some techniques from Alice Ryhl’s article and combine them with Actix’s handler syntax whilst enabling safe async handlers in order to get the best of both worlds.

§Installation

You can install Tactix with Cargo:

cargo add tactix

§Creating an Actor

You can create an actor by simply implementing the Actor trait on a struct.

use tactix::{Actor,Message,Context,Sender,Recipient,Handler};
use async_trait::async_trait;
use tokio::time::{sleep, Duration};

// Define an Actor struct
struct Counter {
  count: u64
}

// Implement the Actor trait on the struct
impl Actor for Counter {
  type Context = Context<Self>;
}

// Define a message
struct Increment;
impl Message for Increment {
  type Response = ();
}

// Define a handler for the message.
// Note: this requires an async_trait macro!
#[async_trait]
impl Handler<Increment> for Counter {
  async fn handle(&mut self, msg:Increment, _:Self::Context) {
    println!("Increment");
    self.count += 1;
  }
}

// We can do the same for Decrement
struct Decrement;
impl Message for Decrement {
  type Response = ();
}

#[async_trait]
impl Handler<Decrement> for Counter {
  async fn handle(&mut self, msg:Decrement, _:Self::Context) {
    println!("Decrement");
    self.count -= 1;
  }
}

// An event for getting the count
struct GetCount;
impl Message for GetCount {
  type Response = u64;
}

#[async_trait]
impl Handler<GetCount> for Counter {
  async fn handle(&mut self, msg:GetCount, _:Self::Context) -> u64 {
    println!("GetCount");
    self.count
  }
}

#[tokio::main]
async fn main() -> Result<(),String> {
  // Construct the actor
  let counter = Counter { count: 0 };

  // Start the actor
  let addr = counter.start();

  // Let's get a Recipient of the decrement message
  // This is often useful when injecting actors as dependencies
  let decrementor:Recipient<Decrement> = addr.clone().recipient();

  // Tell the actor to `Increment`
  addr.do_send(Increment);
  addr.do_send(Increment);
  addr.do_send(Increment);

  // And decrement
  addr.do_send(Decrement);
  decrementor.do_send(Decrement);

  // Wait for all messages to arrive
  sleep(Duration::from_millis(1)).await;

  // To receive a response we use the send async method
  let count = addr.send(GetCount).await.map_err(|e|e.to_string())?;
  assert_eq!(count, 1);
  Ok(())
}

Structs§

Addr
A struct to represent an actors address
Context
Methods concerned with the context of the given Actor
Recipient
An object that has the ability to send messages of a given type M

Traits§

Actor
Defines an Actor. An Actor is an independent unit that processes messages, makes decisions, and communicates with other Actors without shared state.
Handler
Defines an asynchronous message handler for processing actor messages and returning responses.
Message
Defines a message type for actor communication, specifying an associated response type.
Sender
Encapsulates the idea of a channel transmitter. Represents the ability to send messages