Is Rust an Appropriate Language for an Actor System?

Being an experienced Elixir, Erlang and Java / Scala the temptation to bring the Actor model to Rust was huge. Having come here I learned of Actix and after a deep inspection I concluded it wasn’t really a pure actor system ala-Erlang but more like a strongly typed concurrent execution framework. Why? Well I think to further discussion lets define what I think an actor system is.

  1. Actors can communicate only with messages.
  2. Actors can only process one message at a time.
  3. Actors may process a message only once.
  4. An actor can send any message to any other actor without knowledge of that actor’s internals.
  5. Actors shouldn’t need to know the internals of ANY other actor that might send it messages in order to function.

Also there is one other rule that SHOULD be implemented for an actor system.

  1. Actors should communicate only with immutable data. (This is endemic to Erlang / Elixir and simply a guideline in Akka)

It is honestly the fourth and fifth required rules that give me all the headache in rust and the one that most of the attempts thus far at actor systems violate. On the send side the mailbox should be agnostic to the kind of messages carried. Actix, Kay and other attempts use hard static typing to prevent messages from going to the mailbox of the actor via various forms of type coercion and static binding. The problem with that is when you get in a general purpose actor based program that is composed of actors from various libraries you have problems with the static typing. Consider a System actor that reports node status. That actor should be able to send the status to ANY actor whatsoever and never crash because of sending the message. The recipient may decide to discard the message or blow up himself but that should never cause a problem for the sender. Hence the problem.

To implement rule four and five in my attempt thus far rsimmonsjr/ramp I creates a channel that can take any kind of message. Although this sort of works, the ergonomics of the actors themselves becomes a bit of a challenge. The user is forced to downcast at the receive side in a rather unweildy mechanism because unlike Erlang / Elixir and Java, Rust doesn’t implement Very Late Binding for Runtime polymorphism. I cant just declare a handler per message type and have it polymorphically called, we have to laboriously downcast each message.

fn handle_message(&mut self, msg: Arc<Message>) -> DequeueResult {
dispatch(self, msg.clone(), Counter::handle_i32)
    .or_else(|| dispatch(self, msg.clone(), Counter::handle_op))
    .or_else(|| dispatch(self, msg.clone(), Counter::handle_bool))
    .or_else(|| dispatch(self, msg.clone(), Counter::handle_f32))
    .unwrap_or(DequeueResult::Panic)
}

Having gone through several experimental branches this is the only mechinism I could consider to handle the problem but it kind of breaks with the feel and intent of rust. I never thought before that not having polymorphic VLB dispatch would be a major problem but perhaps it is. It also makes me wonder a few things.

  1. Is there a way to implement rules 4 and 5 without using Any as the communication mechanism between actors?
  2. Is it even possible given the current state of rust to do this in an ergonomic and efficient manner that obeys all of the rules above?

Feedback is appreciated.

This guy built an actor system and is building a game around it.

Yeah I have looked into the code and its definitely got the best of the rest in mix of items but it is actually quite a bit tailor made to his use case

If you’re ok with having a double indirection you can do this

impl Actor for Counter {
    fn handle_message(&mut self, msg: Arc<Message>) -> DequeueResult {
       if let Some(m) = msg.downcast_ref::<Box<dyn CounterMessage>>() {
           m.handle(self)
       } else {
           DequeueResult::Panic
       }
    }
}

impl Counter {
    fn make_message<T: CounterMessage>(msg: T) -> Arc<Message> {
        let msg: Box<dyn CounterMessage> = Box::new(msg);
        Arc::new(msg)
    }
}

trait CounterMessage: 'static + Sync + Send {
    fn handle(&self, counter: &mut Counter) -> DequeueResult;
}

https://crates.io/crates/actix

Why not, Rust and Erlang is static vs dynamic typing.
Why actor framework for Rust should have dynamic typing?

2 Likes

Why do you need this property? Honestly I think type-checked message is the huge improvement from type-casted one. Correct me if I’m wrong, but I think in Java messages are passed as an Object form and casted to exact class using instanceof operator. In Rusty system, you may declare an enum type for your message that contains every variance of expected types. This acts as a compiler-enforced api for your actor.

Also, with enum-based variance you can avoid heap allocation which is a huge win for performance.

4 Likes

I have asked myself a more generic question: is any language with native async support compatible with actor system design?
In actor system message handling must be synchronous. If your handler contains http request, you can not do it async, because actors would be totally confused if hnadler function complete but background operation continues. So, you would have to execute any IO in blocking way, or replace any async IO with another set of messages and handlers, which is inconvenient and makes hard to read code.

So, I am thinking either it would be possible to take from Erlang what it does the best: hierarchy of actors and their resiliency and recovery policies, and implement async in modern way.

I think I would debate that erlang isnt modern. Neither is an actor system endemic to non concurrent languages. The reality is that consistent concurrency in large environments is very hard to pull off and actors give you the tools to implement such systems easily