Skip to Content
Course content

149: Building Resilient Systems with Akka Supervision

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

A few years ago, I was mentoring a developer who was building a high-throughput payment gateway. He had wrapped almost every single line of his Actor's logic in massive try-catch blocks. He was terrified that if a single network timeout occurred, the Actor would die and the system would lose track of the transaction. The irony was that by trying to prevent the crash, he created a "zombie" actor. When an exception was caught and silenced, the actor's internal state became inconsistent, and it spent the next three hours processing payments with corrupted data. It was a nightmare to debug because the logs showed no errors, but the database was a mess.

That's the classic mistake when moving from traditional OOP to Akka. In a standard app, you catch exceptions to keep the thread alive. In Akka, we embrace the "Let it crash" philosophy. We don't try to anticipate every possible failure inside the actor itself; instead, we delegate the recovery logic to the actor's parent. This is the essence of supervision.

The Hierarchy of Failure

In Akka, every actor is created by another actor. This creates a parent-child hierarchy that mirrors a corporate org chart. If a child actor throws an exception, it doesn't just vanish into the void; it suspends itself and sends a failure signal to its parent. The parent then decides the fate of the child based on a defined SupervisorStrategy.

You have four primary directives to choose from when a child fails:

  • Resume: The parent tells the child to just keep going. The actor keeps its current state. This is useful for minor, transient errors that don't corrupt the internal state.
  • Restart: This is the most common choice. The actor is killed and a fresh instance is started. The internal state is wiped, but the actor keeps its original identity (mailbox and name). It's the software equivalent of "turning it off and on again."
  • Stop: The actor is terminated permanently. Use this if the failure is fatal and the actor can't possibly recover.
  • Escalate: The parent says, "I don't know how to handle this," and passes the failure up to its own parent.

Implementing a OneForOne Strategy

Most of the time, you'll want a OneForOneStrategy. This means the supervisor handles each failing child independently. If one child crashes, it doesn't affect its siblings. I generally prefer this because it limits the "blast radius" of a failure.

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

class ManagerActor extends Actor {
  override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
    case _: ArithmeticException    => Resume
    case _: NullPointerException   => Restart
    case _: Exception               => Escalate
  }

  def receive = {
    case "create-worker" => context.actorOf(Props[WorkerActor], "worker")
  }
}

class WorkerActor extends Actor {
  def receive = {
    case "fail" => throw new NullPointerException("Something went wrong!")
  }
}

Notice how the ManagerActor doesn't care how the WorkerActor does its job; it only cares about what happens when it fails. By separating the business logic (in the worker) from the fault-tolerance logic (in the manager), your system becomes significantly more resilient. You can change your recovery policy in one place without touching the complex logic inside your workers.

There is also an AllForOneStrategy, where if one child fails, the supervisor restarts all children. I'll warn you: use this sparingly. It's only appropriate when your children are so tightly coupled that one cannot function if another has failed. In 95% of the systems I've built, OneForOneStrategy is the right tool for the job.




📋 Practical Task

Exercise: Building a Resilient Weather Data Aggregator

You are tasked with building a system that fetches weather data from multiple unreliable external APIs. You need to implement a supervision hierarchy that prevents a single failing API call from crashing your entire aggregator.

Requirements:

  • Create a WeatherSupervisor actor.
  • Create a WeatherFetcher actor that simulates an API call. When it receives the message "fetch", it should randomly throw either a TimeoutException or a MalformedURLException.
  • Implement a OneForOneStrategy in the WeatherSupervisor:
    • If a TimeoutException occurs, the actor should Restart (assuming a fresh connection might work).
    • If a MalformedURLException occurs, the actor should Stop (since a bad URL won't fix itself by restarting).
    • Any other exception should be Escalated.
  • Start the system by having the WeatherSupervisor create three WeatherFetcher children and sending them "fetch" messages in a loop.

Goal: Verify through logs that TimeoutExceptions trigger a restart (you'll see the actor's preStart or constructor run again), while MalformedURLExceptions kill the specific actor without affecting the other fetchers.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.