Introduction
This guide introduces speare, covering Actor creation, message handling, and essential library features. The objective is to provide clear, concise instructions for effectively utilizing speare in your Rust projects.
What is speare?
speare is a Rust library designed to simplify the process of actor-based concurrency. It provides an abstraction over tokio green threads and flume channels, allowing for easier management of tokio threads and efficient message passing between them. speare revolves around a main abstraction: Actor -- which lives on its own green thread owning its data, reducing the risks of deadlocks and encouraging a modular design. No more Arc<Mutex<T>> everywhere :)
Quick Look
Below is an example of a very minimal Counter Actor.
use speare::*; use async_trait::async_trait; use tokio::time; struct Counter { count: u32, } enum CounterMsg { Add(u32), Subtract(u32), Print } #[async_trait] impl Actor for Counter { type Props = (); type Msg = CounterMsg; type Err = (); async fn init(ctx: &mut Ctx<Self>) -> Result<Self, Self::Err> { Ok(Counter { count: 0 }) } async fn handle(&mut self, msg: Self::Msg, ctx: &mut Ctx<Self>) -> Result<(), Self::Err> { match msg { CounterMsg::Add(n) => self.count += n, CounterMsg::Subtract(n) => self.count -= n, CounterMsg::Print => println!("Count is {}", self.count) } Ok(()) } } #[tokio::main] async fn main() { let mut node = Node::default(); let counter = node.spawn::<Counter>(()); counter.send(CounterMsg::Add(5)); counter.send(CounterMsg::Subtract(2)); counter.send(CounterMsg::Print); // will print 3 // We wait so the program doesn't end before we print. time::sleep(Duration::from_millis(1)).await; }
What is a Actor?
A Actor is really just a loop that lives in a tokio::task together with some state, yielding to the tokio runtime while it waits for a message received via a flume channel which it then uses to modify its state. All speare does is provide mechanisms to manage lifecycle, communication and supervision of actors..
A Actor is comprised of a few parts:
Trait Implementor
- State: the main struct which implements the
Actortrait. The state is mutable and can be modified every time a messaged is received by theActor.
Associated types
- Props: dependencies needed by the struct that implements
Actorfor instantiation, configuration or anything in between. - Msg: data received and processed by the
Actor. - Err: the error that can be returned by the
Actorwhen it fails.
Trait Functions
-
async fn init:
(ctx: &mut Ctx<Self>) -> Result<Self, Self::Err>Required. Used by
speareto create new instances of yourActorwhenever it is spawned or restarted. -
async fn exit:
(this: Option<Self>, reason: ExitReason<Self>, ctx: &mut Ctx<Self>)Optional. Called every time your
Actoris stopped, be it manually through itsHandle<_>or by the parent through its supervision strategy. -
handle:
(&mut self, msg: Self::Msg, ctx: &mut Ctx<Self>) -> Result<(), Self::Err>Optional. Called when the
Actorreceives a message through its channel. Messages are always processed sequentially. -
supervision:
(props: &Self::Props) -> SupervisionOptional. Called before your
Actoris spawned. Used to customize the supervision strategy for its children. If not implemented, the default supervision strategy will be used: one-for-one infinite restarts without backoff.
Example
Below is an example of a Actor making use of all its associated types and trait functions.
use speare::*;
use async_trait::async_trait;
struct Counter {
count: u32,
}
struct CounterProps {
initial_count: u32,
max_count: u32,
min_count: u32,
}
enum CounterMsg {
Add(u32),
Subtract(u32),
}
#[derive(Debug)]
enum CounterErr {
MaxCountExceeded,
MinCountExceeded,
}
#[async_trait]
impl Actor for Counter {
type Props = CounterProps;
type Msg = CounterMsg;
type Err = CounterErr;
async fn init(ctx: &mut Ctx<Self>) -> Result<Self, Self::Err> {
println!("Counter starting up!");
Ok(Counter {
count: ctx.props().initial_count,
})
}
async fn exit(this: Option<Self>, reason: ExitReason<Self>, ctx: &mut Ctx<Self>) {
println!("Counter exiting due to: {:?}", reason);
}
async fn handle(&mut self, msg: Self::Msg, ctx: &mut Ctx<Self>) -> Result<(), Self::Err> {
match msg {
CounterMsg::Add(n) => {
self.count += n;
if self.count > ctx.props().max_count {
return Err(CounterErr::MaxCountExceeded);
}
}
CounterMsg::Subtract(n) => {
self.count -= n;
if self.count < ctx.props().max_count {
return Err(CounterErr::MinCountExceeded);
}
}
}
Ok(())
}
fn supervision(props: &Self::Props) -> Supervision {
Supervision::one_for_one().directive(Directive::Restart)
}
}
#[tokio::main]
async fn main() {
let mut node = Node::default();
let counter = node.spawn::<Counter>(CounterProps {
initial_count: 5,
max_count: 20,
min_count: 5
});
counter.send(CounterMsg::Add(5));
counter.send(CounterMsg::Subtract(2));
}
TBD 🏗️
# Working with Actors - an actor can be communicated with through its Handle, which is received once it is spawned - an actor can be spawned from a node as an unsupervised top-level actor (more on that later) or spawned from other actors which will be their parent and supervise them. - ctx what is it etc communication between actors - send and send_in to send messages - req and req_timeout to send requests
TBD 🏗️
actor lifecycle - actor spawns and lives in memory forever until its stopped - stopped by its parent being stopped, by being stopped manually or by its parent supervision strategy - when a actor is spawned, the init method is called to create the actor instance - when a actor is stopped the exit method is called. - when a actor is restarted, the exit method is first called, and then the init to create a new instance of the actor. the handle remains the same though.
TBD 🏗️
supervisoin - top level actors created by a node are unsupervised - every actor created by another actor is superivsed by its parent - supervision means that the parent decide can decide what to do with its children when one of them errors. - two main strategies, one for one, one for all - can restart, stop, resume or escalate - a actor will finish handling whatever message it is currently handling before stopping (or restarting) itself