1. Introduction

Mailboxes are one of the fundamental parts of the actor model. Through the mailbox mechanism, actors can decouple the reception of a message from its elaboration.

So, let’s see how Akka Typed, the most famous incarnation of the actor system, implements the concept of mailboxes.

2. Akka Dependencies

We’re going to set up the required project dependencies using SBT. To use the Akka Typed library, we need to import the dependency from the akka-actor-typed artifact:

libraryDependencies += "com.typesafe.akka" % "akka-actor-typed_2.12" % "2.6.11"

To test Akka Typed actors, we need to import the dependency from the akka-actor-testkit-typed artifact:

libraryDependencies += "com.typesafe.akka" % "akka-actor-testkit-typed_2.12" % "2.6.11" % Test

3. Logging Users’ Navigations

First, an actor is an object that carries out its actions in response to communications it receives. Hence, in Akka Typed, such communications are the messages an actor Behavior defines.

At this point, we need a concrete example to go on with the discussion. Imagine we have just started a new e-commerce business, selling shoes through a website. To better understand our users’ behaviors, we want to collect information about their navigation inside the e-commerce site.

For example, let’s collect mouse movements, clicks, and the text the users digit on their keyboard:

sealed trait UserEvent {
  val usedId: String
}
case class MouseClick(override val usedId: String, x: Int, y: Int) extends Event
case class TextSniffing(override val usedId: String, text: String) extends Event
case class MouseMove(override val usedId: String, x: Int, y: Int) extends Event

So, to unleash the power of reactive systems, we define an actor collecting such information:

val eventCollector: Behavior[UserEvent] = Behaviors.receive { (ctx, msg) =>
  msg match {
    case MouseClick(user, x, y) => 
      ctx.log.info(s"The user $user just clicked point ($x, $y)")
    case MouseMove(user, x, y) => 
      ctx.log.info(s"The user $user just move the mouse to point ($x, $y)")
    case TextSniffing(user, text) =>
      ctx.log.info(s"The user $user just entered the text '$text'")
  }
  Behaviors.same
}

4. Scaling (or Ruining) Our Business

As we said, the actor eventCollector reads the messages to process from its mailbox. A mailbox is nothing more than the data structure that holds messages. It’s the ActorSystem that is responsible for filling the actor’s mailbox with messages.

One day, the advertisement for our e-commerce site appears on Vogue America, and the day after, the number of users navigating the site increases tenfold. What happens to our eventCollector actor? Is it elastic concerning the unexpected increase of load?

Unfortunately, long story short, the actor will be overwhelmed by a huge number of messages, and the system will end with an OutOfMemoryError. In fact, the default mailbox for an actor is the SingleConsumerOnlyUnboundedMailbox.

As the name suggests, the mailbox is unbounded, which means it doesn’t reject any delivered message. Besides, there’s no back pressure mechanism implemented in the actor system. Hence, if the number of incoming messages is far bigger than the actor’s execution pace, the system will quickly run out of memory.

In addition, an actor using the SingleConsumerOnlyUnboundedMailbox mailbox cannot share it with any other actor, implementing a communication model with multiple producers and only one consumer.

How can we avoid that the poor eventCollector actor ruins our business? Let’s see together how we can configure our actor.

5. Many Kinds of Mailboxes

Fortunately for us, there are quite a few mailbox types available in the Akka Typed library to choose from. We can set up the mailbox type for an actor during its creation rather than accept the default one. Hence, we use the Props object to select the proper mailbox:

ctx.spawn(eventCollector, id, MailboxSelector.bounded(1000))

As we can see, the MailboxSelector factory lets us create a Props object properly configured. In the above example, we configured the actor to provide a bounded mailbox, which stores only a fixed number of messages. We’ll see more about bounded mailboxes later in the article.

Moreover, it’s possible to read the mailbox type from a configuration property through the MailboxSelector.fromConfig factory method:

val props = MailboxSelector.fromConfig("mailboxes.event-collector-mailbox")
ctx.spawn(eventCollector, s"{$id}_1", props)

Hence, the configuration file will be something similar to:

mailboxes {
  event-collector-mailbox {
    mailbox-type = "akka.dispatch.SingleConsumerOnlyUnboundedMailbox"
  }
}

In general, it’s better not to hardcode configurations. So, this approach is preferable, as we never know what life has in store for us.

Now, we just learned that there are many types of mailboxes. Roughly, we can categorize them using two different features:

  1. Bounded vs. Unbounded
  2. Blocking vs. Non-blocking

We can find the full list of available mailbox types in the Akka Typed official documentation.

5.1. Bounded vs. Unbounded

As we said, unbounded mailboxes grow indefinitely, consuming all the available memory if the messages’ producers are far quicker than the consumers. Hence, we use this kind of mailbox only for trivial use cases.

On the other side, bounded mailboxes retain only a fixed number of messages. The actor system will discard all of the messages arriving at the actor when the mailbox is full. This way, we can avoid running out of memory.

As we did a moment ago, we can configure the mailbox’s size directly using the Mailbox.bounded factory method. Or, better, we can specify it through the configuration properties file:

mailboxes { 
  event-collector-mailbox { 
    mailbox-type = "akka.dispatch.BoundedMailbox"
    mailbox-capacity = 100
  } 
}

The example above is a clear example where bounded mailboxes shine. We are not afraid of losing some messages if the counterpart maintains the system up and running.

A new question should arise: Where do the discarded messages go? Are they just thrown away? Fortunately, the actor system lets us retrieve information about discarded messages through the mechanism of dead letters — we’ll soon learn more about how this works.

5.2. Blocking vs. Non-Blocking

It may sound strange for the actor model, but Akka also provides a type of mailbox that blocks the message’s producer. Indeed, using a blocking mailbox, the sender will block until the actor system notifies that the message is successfully delivered to the mailbox.

Clearly, it’s not a good idea to keep a producer waiting forever. Hence, Akka provides bounded mailboxes with a timeout setting, which is called mailbox-push-timeout-time:

mailboxes { 
  event-collector-mailbox { 
    mailbox-type = "akka.dispatch.BoundedMailbox" 
    mailbox-capacity = 100 
    mailbox-push-timeout-time = 1s
  }
}

The sender will wait for the message to be inserted into the mailbox for the specified amount of time. After that, the actor system will send the message to dead letters, and the producer will be free to proceed with its processing. However, if we set the mailbox-push-timeout-time to zero, we obtain a non-blocking mailbox again.

So, the use of blocking, bounded mailboxes is the Akka way to implement backpressure.

6. Dead Letters

Whenever a message fails to be written into an actor mailbox, the actor system redirects it to a synthetic actor called /deadLetters. The delivery guarantees of dead letter messages are the same as any other message in the system. So, it’s better not to trust so much in such messages. The main purpose of dead letters is debugging.

We can retrieve a reference to the default actor listening to dead letters using the dedicated method in the actor system object:

val defaultDeadLettersActor: ActorRef[DeadLetter] = system.deadLetters[DeadLetter]

Moreover, an actor can subscribe to receive akka.actor.DeadLetter messages. The actor system uses a special communication channel to deliver dead letters, called the Event Stream.

So, to subscribe an actor to the event stream, the actor must listen to DeadLetter messages in its behavior:

val deadLettersListener: Behavior[DeadLetter] = Behaviors.receive { (ctx, msg) =>
  msg match {
    case DeadLetter(message, sender, recipient) =>
      ctx.log.debug(s"Dead letter received: ($message, $sender, $recipient)")
      Behaviors.same
  }
}

Then, the actor system must be instrumented to redirect dead letters to that actor:

val deadLettersActor: ActorRef[DeadLetter] = 
  system.systemActorOf(deadLettersListener, "deadLettersListener")
system.eventStream.tell(EventStream.Subscribe[DeadLetter](deadLettersActor))

However, we have to remember that the subscribed actor will receive only dead letters published in the local system since dead letters are not propagated over the network.

7. Conclusion

Summing up, in this article, we introduced the concept of actors’ mailboxes. We exploited the pros and cons of unbounded mailboxes through an example and when to use bounded mailboxes instead. We talked about blocking and non-blocking mailboxes and how these concepts relate to backpressure.

Moreover, there would be many other aspects of mailboxes to treat, such as implementing custom mailboxes, dispatchers, and how mailboxes can be shared among actors.

As always, the code is available over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments