Skip to main content

Command Palette

Search for a command to run...

Turning actors inside-out

Or... do we really need actor systems?

Updated
17 min read
Turning actors inside-out
E

Software engineer at the intersection of functional programming, product development and open-source

The actor model, and its concrete incarnation in platforms like Erlang/OTP or Akka, has been presented as a great way to create scalable and resilient systems.

However, there is a dimension in which actor systems fall short in my experience. They don't scale in terms of complexity. Each time I've had to look at applications built with actors I found the overall behavior of the system pretty hard to reason about. Why?

The original sin

The reason is quite simple. As Paul Chiusano describes in "Actors are not a Good Concurrency Model" the fundamental signature of an Actor is A => Unit. You send it a message and ... something happens. This presents 2 major drawbacks:

  • You need to look inside the implementation of an actor to have the faintest idea of what it is effectively doing

  • Since its behavior is a side-effect, any refactoring can have unintended consequences

Despite these issues, proponents of the Actor model still claim that their approach is a great way to build concurrent and reliable systems. This is not an entirely baseless claim since a company like WhatsApp, using Erlang, was supporting 900 million users with 50 engineers in 2015! Surely I must be very wrong. Or,... is it a question of context?

In this blog post, I want to examine:

  • What are the properties that make actor systems attractive?

  • Are there ways to keep those desirable properties while still making the system as a whole a lot more amenable to navigating and reasoning?

  • What kind of systems are well-suited to the Actor model?

What is so good then?

For this post, I am going to ignore the use of actors for distributed systems and focus on actors used in local, concurrent systems.

There are 4 main advantages to actors:

  1. The sequential access to in-memory data to avoid data races

  2. An inherently asynchronous programming model

  3. The modeling of actors as state machines mapping 1-1 to some conceptual entity

  4. The use of supervisors for error management

Taming concurrency with actors

One major difficulty with concurrent programming is the necessity to keep our data consistent. If I keep a list of bank accounts and want to model a transfer of money between 2 accounts, I'd better make sure that the transfer happens atomically. It should not be possible to observe that money has left Alice's account without being credited to Bob's account. This kind of problem is compounded when multi-threading is involved and when the compiler can freely re-arrange your code.

In that situation, an object like a Bank does not have a concurrency-safe interface

trait Bank {
  fn withdraw(amount: Amount, from: Account, to: Account) 
     -> Result<(), WithdrawError>;
  fn credit(amount: Amount, from: Account, to: Account);
  fn balance(account: Account) -> Result<Amount>;
}

Even if you combine withdraw and credit into one function, the implementation might not be thread-safe if we implement things naively:

trait Bank {
  fn transfer(amount: Amount, from: Account, to: Account)
     -> Result<(), WithdrawError>;
  fn balance(account: Account) -> Result<Amount>;
}

The solution that actors propose is to "lift" the methods into data, as messages:

enum Message {
  Transfer { 
    amount: Amount,
    from: Account,
    to: Account 
  },
  Balance(),
}

trait Bank: Actor {
  fn handle_message(message: Message);
}

Those messages can be sent to the message queue of an actor and become effectively serialized. The actor system of your choice then makes sure that the execution of each actor is single-threaded: problem solved!

Problem solved, at a massive cost

All the problems in computer science can be solved by another level of indirection

https://www2.dmst.aueb.gr/dds/pubs/inbook/beautiful_code/html/Spi07g.html

We solved this local, tactical, problem at the expense of a massive indirection. Now our program turns from:

if let Ok(_) = bank.transfer(Amount(100), alice, bob) {
  sms_system.send_message(bob, "transfer done!")
}

to:

bank.send_message(Transfer::new(Amount(100), alice, bob));

This has many implications in terms of practical concerns:

  • We cannot anymore click on the transfer call and have a look at the implementation. Instead, we need to go through the entire pattern match inside handle_message to see where the case for Transfer is handled. If there are several message variants, the actual code dealing with transfers is probably tucked away in an internal function somewhere else

  • How do we get back a result telling us that the transfer was successful? Now the Bank actor needs to get a reference to the sender and send a result back

In general, specifying what happens next is not obvious. In the first example, with functions, we notify the SMS system that Bob's account has been credited. With actors, we would probably start the Bank actor with a reference to the Sms actor and send a Credited message from inside the actor implementation.

With actors, we have to read the code to understand what is the flow of data in our system. When reading the code is not enough we have to instrument the actors at runtime to get some insight about what's going on.

It's not a magic wand

Each actor has its own internal state, free of race conditions, great. Yet this does not solve all state-related issues. What if we try to model each Account as an actor? How can we implement a transfer scenario so that the transfer between two accounts becomes atomic?

We have to come up with other patterns and pieces of infrastructure to solve these problems. For example:

Don't (a)wait for me!

A system like Akka became quite popular at a time when async programming was becoming an absolute requirement. A web server dealing with thousands of requests could not allow its threads to be idly waiting for responses from other services or a database. On the contrary, an actor system can spin off millions of actors quietly waiting for an IO request to complete:

  • The caller of the actor posts a message and doesn't have to block until that message is processed

  • It can be notified, via a message, when some data has arrived

This is a great recipe for scalability: a few cores handling millions of requests!

But this is not the only solution to that problem, or, let's say, not the only way to package it. The heart of the solution is to have some sort of "continuation": what should I do when some data finally arrives?

Nowadays there are many other alternatives than actors for dealing with such continuations:

The major difference between these approaches and actors is the fact that they don't expose a programming model made of A => Unit functions. With a Future you make as if you already had the value you're waiting on and you map it to describe what to do next. Now the flow of data between computations is obvious and computing resources are used wisely. We will come back to that later.

Actors as top-models

According to Carl Hewitt, unlike previous models of computation, the actor model was inspired by physics, including general relativity and quantum mechanics.

https://en.wikipedia.org/wiki/Actor_model

Actors are a tool of choice to represent independent entities when they have their own internal state and interfaces. For example, it seems quite natural to model a game like Halo with an actor system (using Microsoft's Orleans platform).

Moreover, we can use state machines to represent:

  • The internal state of an actor as some state variables

  • The received messages as state events

  • The processing of messages as transitions in the state machine

This is wonderful because there exist a large number of tools to:

  • Represent state machines

  • Reason about them: are there unreachable states? Will we always arrive at an end state? Are we missing some events?

  • Test states machines: what is the minimal number of tests covering each state?

However, any time we have several independent systems collaborating, we also get a combinatorial explosion in complexity. Each local state might be well specified, but the overall combination of all states might exhibit some emergent behavior that we did not expect. Think about how the Game of Life generates amazing behaviors out of very simple cells.

Not every system is a physics simulation

The thing is, many of our systems do not need to be represented as small individual processes. And the fewer states we need to maintain, the better off we are.

The least amount of state is a pure function. Look ma', no state:

fn compute_taxes(income: Amount) -> Amount

Yet it is not always practical, or even possible, to compute only with non-stateful functions. It would be quite repetitive to pass around all the state required for computations:

fn compute_taxes(
   income: Amount, 
   tax_rules: TaxRules,
   previous_tax_paid: Vec<Amount>) -> Amount

Worse, it is not even desirable. There are implementation details that we wish to hide, like the fact that the tax algorithm is parameterized by rules or that it uses the amount of taxes paid in the previous year to calculate this year's taxes.

In any case, even if we end up with a function having some side effects, by enclosing some state, we are still in a better position than when using actors. Because functions give us a flow of data. "Something in, something out". And the "something out" is ready to become "something in" for another function.

It is not entirely by accident if an Actor system like Akka ended up proposing a Stream abstraction:

We have found it tedious and error-prone to implement all the proper measures in order to achieve stable streaming between actors, since in addition to sending and receiving we also need to take care to not overflow any buffers or mailboxes in the process. Another pitfall is that Actor messages can be lost and must be retransmitted in that case. Failure to do so would lead to holes at the receiving side

For me, this is thinking backward:

  1. "Let's start with a general system where we can do everything"

  2. "Let's constrain our system by adding another layer"

In some ways you can see a similar issue with dynamic vs. static typing in programming languages:

  1. "Haha, no pesky compiler to prevent me from treating this variable as an int here and a bool there. It just works. Why would I care?"

  2. "Oops my system is really hard to understand, give me types please":

    1. for Python: https://mypy-lang.org

    2. for Javascript: https://www.typescriptlang.org

    3. for Ruby: https://sorbet.org

In the words of Runàr Bjarnason

Constraints liberate, liberties constraint

very relevant talk excerpt: https://youtu.be/GqmsQeSzMdw?t=1452

I'm tolerant. To a fault.

One often-cited advantage of actor systems is the idea that those systems can be made fault-tolerant. How does that work?

Turn it off and on again! | The PuterTutor Computer Repair Services

No really, that's it. If some state ends up being corrupted in an actor, a sensible strategy is to start afresh. And if several actors are cooperating to accomplish any task it makes sense to restart all of them to ensure that their global state is correct.

This is the role of Supervisors in a system like Erlang/OTP or Akka. Since actors can spawn other actors, an actor system defines a tree of actors in a parent/child relationship. At each level, we can associate a supervisor. In case of a failure of an actor at that level it can decide to:

  • Restart that actor

  • Restart all the actors on that level

  • Propagate the failure one level up

This way of dealing with faults is efficient for two reasons:

  1. It addresses some common reasons for failures: incorrect state, maxed-out internal queue, or unreachable resources.

  2. It allows the definition of independent processes or sub-processes that can be restarted individually thus containing errors to a limited part of the system

Magic wand again?

I think we must not conflate the problem with the solution. Ideally, we want to

  1. Avoid inconsistent state

  2. Isolate failures

The first point should foremost be addressed by having a minimal amount of state and being able to reason about it. We don't want to end up with a database like Oracle where the fix to some combinatorial state issue is to add even more states!

If an actor fails deterministically with some corrupted state, you might restart it 100 times, you will still get the same result. The "Let it crash!" motto seems to me to be a way of saying "Let us acknowledge that our system is so complex that we will enter some undefined states. Hopefully turning it off/on will work 🙏".

It is easy to understand that this cannot be the only strategy to tame the complexity related to state management. Yet, actors, as a network of interacting state machines, encourage a combinatorial explosion of state!

The second condition, "isolate failures", also does not come out magically from supervisors. Maybe a hundred actors represent individuals in a chat room but if one of them can make the whole chat room fail, the whole system fails. We need to carefully understand the functions of our system and their dependencies to be able to isolate them. Yet, actors, by being able to start other actors and communicate with them, hide the functional dependencies of our systems!

Eventually, it seems to me that the main benefit of supervisors is to force us to think about error modes and how to address them.

Can we do better?

I think so. We don't have to reinvent the wheel. We have ways to program differently for a vast category of systems. I would describe those systems as "linear".

The communication graph is not always complete

A "Linear" system is a system where data flows almost exclusively in one direction (think "activity diagram" in UML). Such a system:

  • Accepts a request, possibly after a round of negotiation, authentication, and authorization. Alternatively, they pull out some event from a queue

  • Process the request or event data in several steps, possibly incorporating some local persisted state

  • Update their local state

  • Publish events for other systems to consume

  • Send back a response

In a linear system:

  • There is a clear direction of data, which makes progress mostly in one direction

  • Data comes front and center, the identity of its processor is less important

  • There is some level of independence between each request and how they are processed

  • Concurrency or asynchronism is desirable, especially if IO is involved

  • Parallelism is desirable if some processes are CPU intensive (cryptographic operations, numerical computations)

Examples of linear systems include many forms of web services, APIs, or even point-to-point communications. Non-linear systems are typically multi-player games like Halo4 or group chat applications like WhatsApp and Discord.

The Future is already there

I hinted earlier that actors have been successful at a moment where asynchronous computing was sorely needed. Yet, actors are not the only solution to that problem. Case in point, the Actix web framework in Rust. It used to be built on an actor library, but:

its usefulness as a general tool is diminishing as the futures and async/await ecosystem matures

https://actix.rs/docs/whatis

Indeed futures and async support in our programming languages and libraries give us all the support we need. We actually get a lot more:

  • We get types. A computation Future<T> produces a value of type T

  • We get combinators to process the value future.map(f). We know what is the type of the resulting value!

  • We get combinators to run processes concurrently and, for example, take the first value that succeeds in being computed give_me_first_available_quote

  • We get a lot closer to structured concurrency which allows us to reason about errors and resources in a concurrent setting. You can read more about this in the fabulous blog post "go statement considered harmful".

There's blocking and blocking

Great, it seems that we solve the question of blocking while still being able to read our code as a series of typed transformations of data. What solving the issues with inconsistent state that actors solve with their message queue?

One obvious solution is to introduce locks to protect resources and guarantee their atomic update:

struct Bank {
  accounts: RwLock<Vec<Account>>
}

impl Bank {
  pub async fn transfer(
    amount: Amount,
    from: Account,
    to: Account) -> Result<(), TransferError> {
    let accounts = accounts.unlock();
    ...   
  }
}

Compared to the actor example:

  • Calls to transfer are serialized so the data will be consistent

  • We don't have to reify the transfer function into a full data type

  • A caller waiting for its transfer action to be processed will not consume any major resource system, like a thread. The async runtime will park that computation until it is ready to proceed and continue

  • We lose the full asynchronicity. Maybe the caller was truly not interested in the result of the transfer.

The last point seems to be a problem. Previously the caller of an actor was able to just leave a message and then could go away and process its own messages. Now, the caller of the transfer function has to block until the underlying resource is free to be updated. This might not block a thread but this will still prevent the function caller from doing anything else useful. I'll come back to that soon.

Note: I am aware that locks come with their own set of pitfalls!

Stateful components

A set of async functions operating on some shared data can be regrouped in a component:

trait ReviewComittee {
   async fn propose_talk(name: Name, talk: Talk) -> Result<Acceptance>;
   async fn withdraw_talk(name: Name, talk: Talk) -> Result<()>;
   async fn get_final_talks() -> Result<Vec<Talk>>;
}

That component might be the source of data for another component:

if let Accepted(talk) = committee.propose_talk(
    eric, 
    turning_actors_inside_out).await?) {
    scheduler.schedule(talk).await?   
}

If we lock the internal state of the ReviewCommitee and use async functions we don't need actors. Actually, this is not different from what GenServer is doing, on top of actors, in Elixir!

defmodule ReviewCommittee do
  use GenServer

  def handle_call({:propose_talk, name, talk}, _from, talks) do
    {:reply, :accepted, talk | talks}
  end

  def handle_call(:get_final_talks}, _from, talks) do
    {:reply, talks, talks}
  end

  def handle_cast({:withdraw_talk, name, talk}, state) do
    {:noreply, ...}
  end
end

The documentation says:

We can primarily interact with the server by sending two types of messages. call messages expect a reply from the server (and are therefore synchronous) while cast messages do not.

With GenServer we get the same benefits as a stateful component in async languages and the same constraints. But we lose types and have to consider the fact that the implementation can spawn other independent processes that we won't know about.

Turning actors inside-out

One other interesting viewpoint for linear systems is to see them as stream-processing systems. I wrote earlier that actors have this non-blocking mode of interaction where an actor can just leave a message for another actor and go on with his day. This is a way of saying: "Here is a bit of information you might be interested in, do whatever you want to do with it, I don't care".

We can invert that pattern and have components that say: "I am producing a piece of information, take it if you want". Essentially have a producer-consumer pattern.

This pattern introduces a level of indirection, like actors do. We now need to create data types to encapsulate the kind of information we want to communicate between computations. But the intent is very different! Instead of creating a message that says "schedule this talk" we create a fact saying "this talk was approved".

We can now have streams of facts that we can transform, join, store, reuse etc... This idea is very well presented by the wonderful talk from Martin Kleppmann, "Turning the database inside-out".

By turning actors inside out as publishers and subscribers we get amazing powers:

  • We get back types: A Subscriber<T> can only subscribe to a Producer<T>. Our software components now come with a manual on how to assemble them!

  • We get combinators: I can map a Producer<T> with a function T -> S to get a Producer<S> (and contramap a Subscriber<T> with a function S -> T to get a Subscriber<S>)

  • We decouple our software: a Producer truly doesn't have to know about its Subscribers. Whereas actors have to know about each other

  • We get a full specification on how to deal with gnarly problems like cancellation and backpressure

Final scene

I don't want to dismiss all the great work that has been done by Erlang/Elixir, and Akka users around the world. People have built large and successful systems with these technologies. Besides, the Erlang VM has some impressive features for distribution and hot code replacement, which are worth considering.

I believe however that the actor model is way too dynamic and untyped for many of the data-oriented systems that we are building every day. The same can be said, in my opinion, about dynamic programming languages. We can do amazing things with a language like Racket, but the safety net and discoverability of Haskell (cf. Hoogle) is preferable in many cases. In the words of John Ky:

Static typing is Lego, dynamic typing is playdough

Which I would steal as

Reactive streams are Lego, actors are playdough

As far as I'm concerned, I prefer building with Legos 😊.

P
Praveen V2y ago

https://devtohash.hashnode.dev/frontend-development-practice-html-and-css

1
P
Praveen V2y ago

learn frontend

1
P
Praveen V2y ago

https://devtohash.hashnode.dev/frontend-development-practice-html-and-css

1
P
Praveen V2y ago

Learn frontend with code

1
M

We cannot anymore click on the transfer call and have a look at the implementation. Instead, we need to go through the entire pattern match inside handle_message to see where the case for Transfer is handled. If there are several message variants, the actual code dealing with transfers is probably tucked away in an internal function somewhere else

I agree that there are more level of indirections when using an actor model. Below is an example in Erlang from https:// github.com/francescoc/scalabilitywitherlangotp/blob/master/ch3/frequency.erl where the loop method that processes messages calls other methods depending on the message:

loop(Frequencies) ->
    receive
{request, Pid, allocate} ->
   {NewFrequencies, Reply} = allocate(Frequencies, Pid),
   reply(Pid, Reply),
   loop(NewFrequencies);
{request, Pid , {deallocate, Freq}} ->
   NewFrequencies = deallocate(Frequencies, Freq),
   reply(Pid, ok),
   loop(NewFrequencies);
{request, Pid, stop} ->
   reply(Pid, ok)
    end.


%% The Internal Helper Functions used to allocate and
%% deallocate frequencies.
allocate({[], Allocated}, _Pid) ->
    {{[], Allocated}, {error, no_frequency}};
allocate({[Freq|Free], Allocated}, Pid) ->
    {{Free, [{Freq, Pid}|Allocated]}, {ok, Freq}}.

deallocate({Free, Allocated}, Freq) ->
    NewAllocated=lists:keydelete(Freq, 1, Allocated),
    {[Freq|Free], NewAllocated}.

But I personally find it very readable. And when using behaviours such as gen_server, I still find the code readable (https:// github.com/francescoc/scalabilitywitherlangotp/blob/master/ch4/frequency.erl):

handle_call({allocate, Pid}, _From, Frequencies) ->
    {NewFrequencies, Reply} = allocate(Frequencies, Pid),
    {reply, Reply, NewFrequencies}.

handle_cast({deallocate, Freq}, Frequencies) ->
    NewFrequencies = deallocate(Frequencies, Freq),
    {noreply, NewFrequencies};

handle_cast(stop, LoopData) ->
    {stop, normal, LoopData}.

allocate({[], Allocated}, _Pid) ->
    {{[], Allocated}, {error, no_frequency}};
allocate({[Res|Resources], Allocated}, Pid) ->
    {{Resources, [{Res, Pid}|Allocated]}, {ok, Res}}.

deallocate({Free, Allocated}, Res) ->
    NewAllocated = lists:keydelete(Res, 1, Allocated),
    {[Res|Free],  NewAllocated}.

One of the benefits of this code structure is that tracking and logging the changes of the actor state can be done in a single place (the infinite loop that processes messages), and such a feature is even avalaible for some behaviours defined in the OTP library in Erlang/Elixir.

1
M

Note about directly calling send_message: The Erlang and Elixir communities actually recommend to expose functions that wrap calls of functions such as send_message to abstract the communication protocol for readability and maintainability, see https:// hexdocs.pm/elixir/GenServer.html#module-client-server-apis for an example. Note that, if such an interface function (a)waits for the reply from the actor, the function becomes synchronous and can have a return type.

With actors, we have to read the code to understand what is the flow of data in our system. When reading the code is not enough we have to instrument the actors at runtime to get some insight about what's going on.

I think this depends on the implementation, and I am sure that there are implementations that would make it easier to understand the flow of data in the system. Actors are very similar to objects in OOP (Object Oriented Programming), and there should be a way to pass a callback with the message as it is done in the OOP version.

1
M

Disclaimer: I am not an actor expert and I don't know anything about actors in Rust; I just read a couple of books on Erlang such as the excellent book "Designing for Scalability with Erlang/OTP" written by Francesco Cesarini.

I personally think that actors are true microservices in the sense that:

  • creating an actor does consume little resources
  • an actor can perform "small" operations that may change a state encapsulated inside the actor.

Therefore, an actor system is not always the ideal design. For example, I think that Futures or async/await are much better for data processing pipelines that do not require state management. Or that designing an actor system for bank transfers is not trivial because of the nature of bank transactions that makes the system state hard to split.

Still, I find the actor system a very elegant solution in other cases. For example, in his book "Designing for Scalability with Erlang/OTP", Francesco Cesarini demonstrates how to implement a coffee machine in Erlang, and I find the resulting code, in particular the handling of invalid actions, impressive:

  • Pure Erlang: https:// github.com/francescoc/scalabilitywitherlangotp/blob/master/ch6/erlang/coffee_fsm.erl
  • Using FSM (Finite State Machine) Behaviour: https:// github.com/francescoc/scalabilitywitherlangotp/blob/master/ch6/otp/coffee_fsm.erl

I think that actors are a good option if a large distributed system state can be split into smaller parts. For example, let's consider a system to book a ticket with a seat number at a concert. The system has to store for each seat if a seat is free, being reserved or reserved, and, if it is not free, the person who is reserving or has reserved the seat. We could implement this system as follows:

  • A Client actor C asks the BookingManager actor which seats are still free
  • The BookingManager, who stores the state of each seat (free, reservation_in_progress(client), reserved(client)), returns the list of free seats
  • The Client C provides the BookingManager with the seat Seat he would like to reserve
  • The BookingManager changes the state of Seat to 'reservation in progress' by client C and requests Client C to pay for the seat
    • Note: if another client D has started a reservation process for Seat in between, the BookingManager will return an error.
  • The Client pays the BookingManager
  • The BookingManager marks the seat as 'reserved' by client C and gives client C the corresponding ticket.

If the concert venue has lots of seats, and the number of person interested in getting a ticket is high, a single BookingManager actor cannot handle all the requests quickly. In this case, we need multiple BookingManager actors, and a way to share the state of the seats between them. One way to design the system consists in splitting the seats state across multiple SeatsAreaManager actors, each of them being responsible of the seats of a distinct area of the concert venue. The booking process would work as follows:

  • A Client actor C asks a BookingManager actor B which seats are still free
  • BookingManager B asks all the SeatsAreaManager actors to return the seats that are still free
  • Each SeatsAreaManager actor, who stores the state of each seat in his area, returns the list of free seats in his area to BookingManager B
  • BookingManager B aggregates the results and return them to Client C
  • Client C provides BookingManager B with the seat Seat he would like to reserve
  • BookingManager B notifies the SeatsAreaManager actor S responsible of Seat that Client C requested to reserve Seat
  • SeatsAreaManager S changes the state of Seat to 'reservation in progress' by client C and asks BookingManager B to request the payment of Seat
    • If Seat was reserved by another Client in between , SeatsAreaManager returns an error message.
  • BookingManager B asks Client C to pay for Seat
  • Client C pays for Seat
  • BookingManager B notifies SeatsAreaManager S that Client C paid for Seat
  • SeatsAreaManager S marks Seat as booked and returns the corresponding ticket to BookingManager B
  • BookingManager B returns the ticket to Client C.

This change in the design enables to scale the number of BookingManager and the number of SeatsAreaManager (the maximum number of SeatsAreaManager is the number of seats in the concert venue).

If the number of SeatsAreaManager actors is high, BookingManager actors can become slow because they need to query all the SeatsAreaManager to know which seats are free. In this case, we could introduce additional levels of SeatsAreaManager actors who would not store state but contact SeatsAreaManager actors at the level below to gather free seats in their areas. Another option could consist in making BookingManager actors store a cache of the free seats that would be frequently refreshed, for example every 100ms.

I hope that this example makes sense and helps to show the power of actors to scale a system. I am not sure how easy it would be to develop such a system using Futures or async/await because I don't see at the moment how such constructs could help to handle this type of "distributed state".

Note on the "Let it crash" paradigm: The point of the "Let it crash" is that the actor that crashes cannot properly propagate the error to other actors it was in contact with. Instead, other actors should be notified of the crash and will take care of updating their state accordingly. For example, if the Client C crashes, this crash could notify the BookingManager actor B that Client C was in contact with. BookingManager B could then decide to cancel any reservation in progress done by Client C.

E
Ed2y ago

At the start, you point out a practical concern for actors:

  • We cannot anymore click on the transfer call and have a look at the implementation.

At the end, you highlight an amazing power for streams:

  • We decouple our software: a Producer truly doesn't have to know about its Subscribers.

Aren't these the same thing?

Also, I'm not sure what you mean by "actors have to know about each other" but Akka has adapted response interaction pattern (hashnode says I can't post links).

1
E

Aren't these the same thing?

With streams, the place where you connect a publisher and a subscriber is explicit. With actors, the fact that actor A publishes some information to actor B is hidden inside actor A.

Also, I'm not sure what you mean by "actors have to know about each other" but Akka has adapted response interaction pattern

I was talking about having to know about each other references. Thanks for the pointer on the adapted response pattern. I see how it can be helpful. But that is a lot of ceremony for my taste!

M
Mao Lin2y ago

The actor model has undeniably been a game-changer in building scalable and resilient systems, with platforms like Erlang/OTP and Akka leading the way. Yet, as we delve deeper, it becomes evident that actor systems have their limitations. The intricacies of turning actors "inside-out" to harness their full potential reveal the evolving nature of this model and the need for continued exploration in the realm of system design and development. It's a fascinating journey, uncovering the nuances that drive innovation in the ever-evolving landscape of technology.

1
E

There are some very interesting thoughts (and background history) about why the Erlang/OTP approach was successful in Stevan's post: https://github.com/stevana/armstrong-distributed-systems/blob/main/docs/erlang-is-not-about.md