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