Skip to content

Latest commit

 

History

History
135 lines (110 loc) · 3.84 KB

README.md

File metadata and controls

135 lines (110 loc) · 3.84 KB

Akt

An actors framework for Rust and Tokio.

It is heavily inspired by Actix and right now it has very similar look and feel. The main difference is that in Actix (at least at the moment of this particular library creation) working with async / await and async handlers in particular is a little cumbersome. This library supports async handlers out of the box and even provide tools to simplify some unusual kinds of communications like waiting for result without even blocking the main message loop.

docs.rs Crates.io

Status

This library is on the very early stage of development. It means that:

  • You may face significant amount of bugs
  • API could be incompatibly changed any moment
  • Test coverage is not complete

However it is already used in production so you may give it a try in your project too.

Overview

Let's implement some actor to manage bank account.

use mpi_actors::{Actor, Context, Handler, Message};
use async_trait::async_trait;
use thiserror::Error;

// A state managed by our actor
struct Account {
    balance: u32,
}

// Implementing Actor trait makes an actor out of the state
impl Actor for Account {}

// Define messages to be handled by the actor

struct Deposit {
    pub amount: u32,
}

impl Message for Deposit {
  // Result the actor will respond with
    type Result = u32;
}

// Implement handler for the Deposit message and Account actor
// We need to use `async_trait` crate "magic" because async trait methods
// is not supported bu rust directly yet.
#[async_trait]
impl Handler<Deposit> for Account {
    async fn handle(&mut self, message: Deposit, _context: &mut Context<Account>) -> u32 {
        self.balance = self.balance + message.amount;

        self.balance
    }
}

struct Withdraw {
  pub amount: u32,
}

// Withdrawal may fail so we could provide specific error for it.
//
// It could be a struct, but for backwards compatibility reasons it could
// be a good idea to make enum even if for now we have only one variant.
//
// Here we use `thiserror` crate for convenient Error
// derivation, but it is completely optional.
#[derive(Debug, Error, PartialEq)]
enum WithdrawalError {
    #[error("Insufficient funds. {requested} was requested but only {available} is available.")]
    InsufficientFunds { requested: u32, available: u32 },
}

impl Message for Withdraw {
    type Result = Result<u32, WithdrawalError>;
}

#[async_trait]
impl Handler<Withdraw> for Account {
    async fn handle(
        &mut self,
        message: Withdraw,
        _context: &mut Context<Account>,
    ) -> Result<u32, WithdrawalError> {
        if self.balance < message.amount {
            return Err(WithdrawalError::InsufficientFunds {
                requested: message.amount,
                available: self.balance,
            });
        }

        self.balance = self.balance - message.amount;

        Ok(self.balance)
    }
}

#[tokio::main]
async fn main() {
   // Create actor
   let actor = Account { balance: 50 };

   // Run actor consuming it and returning its address
   let address = actor.run();

   // Send message and use regular `await` workflow to wait for the response.
   let balance = address.send(Withdraw { amount: 20 }).await;
   assert_eq!(balance, Ok(Ok(30)));

   let balance = address.send(Deposit { amount: 40 }).await;
   assert_eq!(balance, Ok(70));

   let balance = address.send(Withdraw { amount: 100 }).await;
   assert_eq!(
       balance,
       Ok(Err(WithdrawalError::InsufficientFunds {
           requested: 100,
           available: 70
       }))
   );

   // Actors addresses could be safely cloned
   let address_clone = address.clone();
   let balance = address_clone.send(Withdraw { amount: 10 }).await;
   assert_eq!(balance, Ok(Ok(60)));

   let balance = address.send(Withdraw { amount: 10 }).await;
   assert_eq!(balance, Ok(Ok(50)));
}