Skip to Content
Course content

67: Akka Supervision Strategies

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this pattern a dozen times: a developer builds a system of actors, tests it with a few happy-path scenarios, and then deploys it to production only to find their logs flooded with thousands of "Actor Restarting" messages. They aren't actually fixing the problem; they're just stuck in a "death spiral."

Let's look at a piece of code that does exactly this. Imagine we're building a DatabaseWriter actor that handles persisting user profiles. The developer has written it to simply throw an exception when something goes wrong, assuming Akka's default supervision will "just handle it."

class DatabaseWriter extends Actor {
  def receive: Receive = {
    case profile: UserProfile =>
      // Imagine this throws a ConstraintViolationException if the email exists
      // or a TimeoutException if the DB is lagging
      db.save(profile) 
      println(s"Saved ${profile.name}")
  }
}

class PersistenceManager extends Actor {
  val writer = context.actorOf(Props[DatabaseWriter], "writer")
  
  def receive: Receive = {
    case profile => writer ! profile
  }
}

The Infinite Restart Loop

Here is the problem: Akka's default supervision strategy is to restart the actor. Now, if the DatabaseWriter hits a TimeoutException, a restart is actually great. It clears the internal state and tries again. But what happens when the DatabaseWriter hits a ConstraintViolationException (like a duplicate email)?

The actor crashes, the parent sees the exception, and says, "Oh, I'll just restart the child!" The child comes back to life, receives the exact same message from the mailbox, and crashes again. Repeat this 10,000 times a second, and you've just DDoS'd your own logging system without actually processing a single record.

Tailoring the Response with OneForOneStrategy

To fix this, we need to stop treating all failures as equal. We need a OneForOneStrategy. This allows us to decide the fate of the child based on the type of exception thrown. I generally prefer this over the "All For One" strategy unless the children are so tightly coupled that one failure makes the others useless.

Here is how I would rewrite the PersistenceManager to handle this intelligently:

import akka.actor.SupervisorStrategy._
import akka.actor.{OneForOneStrategy, Actor}
import scala.concurrent.duration._

class PersistenceManager extends Actor {
  override val supervisorStrategy: SupervisorStrategy = OneForOneStrategy() {
    case _: TimeoutException    => Restart // Transient error, try again
    case _: ConstraintViolation => Stop    // Fatal data error, kill the actor
    case _: Exception           => Escalate // Something weird? Let my parent handle it
  }

  val writer = context.actorOf(Props[DatabaseWriter], "writer")

  def receive: Receive = {
    case profile => writer ! profile
  }
}


By overriding supervisorStrategy, we've turned the manager into a conscious supervisor. If a ConstraintViolation occurs, we Stop the actor. This prevents the death spiral. In a real system, you'd likely combine this with a "Dead Letter" queue or a way to notify the user that their data was invalid, rather than just letting the actor vanish.

Deciding Between Resume and Restart

You might be wondering when to use Resume. I like to think of Resume as "ignore the glitch." When you resume, the actor keeps its current internal state and simply processes the next message in the mailbox.

Use Resume if the failure is completely irrelevant to the actor's state—for example, if a logging call failed but the actual business logic succeeded. Use Restart if you suspect the actor's internal state has been corrupted by the failure and you need a clean slate. Just remember: Restart will clear the actor's state, so if you're keeping important counters or caches in memory, you'll need to handle pre-restart snapshots or accept that the data is gone.




📋 Practical Task

Exercise: Implementing a Resilient Weather Data Aggregator

You are building a system that fetches weather data from multiple third-party APIs. You have a WeatherAggregator (parent) that manages several ApiFetcher (children) actors.

Your task is to implement a OneForOneStrategy in the WeatherAggregator that handles the following scenarios:

  • ApiTimeoutException: These are common. The actor should be restarted to try a fresh connection.
  • InvalidApiKeyException: This is a configuration error. Restarting won't fix it. The actor should be stopped immediately.
  • RateLimitException: This is a temporary pause. The actor should resume, skipping the current failed request but keeping its state to try the next one.
  • Any other exception: These are unknown. The error should be escalated to the aggregator's own supervisor.

Requirements:

  1. Create the three custom exception classes mentioned above.
  2. Implement the WeatherAggregator actor with the specific supervisorStrategy logic.
  3. Implement a simple ApiFetcher actor that randomly throws one of these exceptions when it receives a FetchData message so you can verify your strategy works.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.