Message Bus vs Actors

Hi, I'd like to get some feedback about some architectural patterns. I know it isn't incredibly Rust specific, but I think language does often influence design, and I really respect a lot of the people and feedback I've seen here.

I'm curious if anyone has thoughts on when a message bus is appropriate vs actors. Based on my understanding, which could easily be incomplete, actors allow async processing of data, and also allow you to kind of compose data flow in a very flexible way: there's an input stream of stuff to process, and each message has a oneshot channel you can respond with. That seems way more flexible for composing data flows than a message bus, and seems to allow for better encapsulation.

Am I wrong? Why would I want to use a message bus instead of actors? I'd think a message bus as a central construct for passing all messages would be riddled with problems, like noisy neighbors affecting throughput, limits in the channel sizes, leaky abstractions, etc.

It seems like the biggest difference is that a message bus is primarily suited for IPC between multiple hosts, and actors are primarily suited for task communication entirely within a single process. I’m not convinced these are strongly biased this way in practice. For instance, Erlang actors will happily fit both roles.

Another consideration is coupling. A message bus is loosely coupled; any client written in any language can use the bus if it supports the network protocol. Actors are tightly coupled; even in the case of Erlang, actors are more tightly coupled than RPC or pub/sub. It’s more than just a network protocol. Actors have specific roles and behaviors.

2 Likes

That's what I'm confused about - I don't see any reason an actor couldn't just have the input stream come from a socket.

And I'm not sure I understand the loose / tight coupling argument. A message like DoThing::Foo will need to come from somewhere either way, and an actor would simple receive it and send a response. If the message bus can do the same thing, but ultimately just forwards it to something that handles DoThing requests, what's its use? Isn't it just a middleman?

A channel is analogous to a socket. An actor is the functionality that does something with data sent over the channel (or socket). If we are nitpicking terminology, then there is little difference between a channel and a message bus. After all, a message bus is “just a fancy socket”.

The actor is something entirely different. I mentioned it has a role and a behavior. The behavior may be “do useful work with DoThing::Foo and respond when done”. The actor may also have certain responsibilities such as “own and manage access to shared resources” or “monitor this other task and restart it if it fails to respond to pings”. It may have many such responsibilities and behaviors but it only has a single role.

It’s important that the responsibilities and behaviors are identical between cloned actors. Primarily because actors can spawn other actors. And this is one of the features of tight coupling. An actor is an important part of a complex system, and it must work a very specific way. It has a specific role.

I get your point that the distinction between something consuming and producing messages on a message bus may look very actor-like. And that might be the case depending on how it is implemented. But I think that because it is not a requirement that the process be actor-like, that is the true discriminator.

A process that periodically publishes the current temperature from a sensor could hardly be called an actor. It could easily use a raw socket or a message bus, though. It doesn’t have to know anything to do its job other than how to read the temperature and send it over a socket. But an actor has to know a lot about its environment: how to spawn more actors, where to send messages to other actors in the system, whether the message that it receives requires sending a response back to the originating actor. Etc. this is how I have used the term coupling; within the scope of the total complex system.

1 Like

I thought of another thing that might help answer your question:

This seems to imply that Arc<Mutex<T>> is "good enough for everybody". But actors can do local reasoning! Let's say an actor has a policy that if there are too many clients waiting on a response from a database resource that it manages, it should respond with a "retry later" message. (Applying back-pressure.) The message may provide an estimate on when the retry might succeed, allowing the actor to avoid request spikes by distributing retry deadlines with a random bias.

That is pretty hard to do with just a mutex! The logic would have to be replicated everywhere that mutex is accessed. In some cases, that's one function called by many threads, and the benefits of an actor in this case are fairly small. You will see increasing benefits as the code scales and more functions need to touch the mutex. By encapsulating the policy logic within an actor, it can trivially be reused by many clients.

Could you also say that the message bus model is designed for pub/sub, while the actor model is not?

Pub/sub can be implemented with actors, and presumably non-pub/sub messages can be used with a message bus, but only the message bus is focused on pub/sub. The actor model seems focused on state ownership.

The actor model is definitely focused on shared resource management.

Pub/sub is only one possible messaging pattern. Another is message queueing for individual consumers, rather than broadcast to multiple consumers in parallel.

What I'm trying to illustrate is that actors are independent of what they use to send messages to one another. What's important is that message passing is a fundamental component of actors. They communicate only by passing messages, never by sharing a mutex.