scalaakkaakka-supervision

Akka Supervisor Strategy - Correct Use Case


I have been using Akka Supervisor Strategy to handle business logic exceptions.

Reading one of the most famous Scala blog series Neophyte, I found him giving a different purpose for what I have always been doing.

Example:

Let's say I have an HttpActor that should contact an external resource and in case it's down, I will throw an Exception, for now a ResourceUnavailableException.

In case my Supervisor catches that, I will call a Restart on my HttpActor, and in my HttpActor preRestart method, I will call do a schedulerOnce to retry that.

The actor:

class HttpActor extends Actor with ActorLogging {

  implicit val system = context.system

  override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
    log.info(s"Restarting Actor due: ${reason.getCause}")
    message foreach { msg =>
      context.system.scheduler.scheduleOnce(10.seconds, self, msg)
    }
  }

  def receive = LoggingReceive {

    case g: GetRequest =>
      doRequest(http.doGet(g), g.httpManager.url, sender())
  }

A Supervisor:

class HttpSupervisor extends Actor with ActorLogging with RouterHelper {

  override val supervisorStrategy =
    OneForOneStrategy(maxNrOfRetries = 5) {
      case _: ResourceUnavailableException   => Restart
      case _: Exception                      => Escalate
    }

  var router = makeRouter[HttpActor](5)

  def receive = LoggingReceive {
    case g: GetRequest =>
      router.route(g, sender())

    case Terminated(a) =>
      router = router.removeRoutee(a)
      val r = context.actorOf(Props[HttpActor])
      context watch r
      router = router.addRoutee(r)
  }
}

What's the point here?

In case my doRequest method throws the ResourceUnavailableException, the supervisor will get that and restart the actor, forcing it to resend the message after some time, according to the scheduler. The advantages I see is the fact I get for free the number of retries and a nice way to handle the exception itself.

Now looking at the blog, he shows a different approach in case you need a retry stuff, just sending messages like this:

def receive = {
  case EspressoRequest =>
    val receipt = register ? Transaction(Espresso)
    receipt.map((EspressoCup(Filled), _)).recover {
      case _: AskTimeoutException => ComebackLater
    } pipeTo(sender)

  case ClosingTime => context.system.shutdown()
}

Here in case of AskTimeoutException of the Future, he pipes the result as a ComebackLater object, which he will handle doing this:

case ComebackLater =>
      log.info("grumble, grumble")
      context.system.scheduler.scheduleOnce(300.millis) {
        coffeeSource ! EspressoRequest
      }

For me this is pretty much what you can do with the strategy supervisor, but in a manually way, with no built in number of retries logic.

So what is the best approach here and why? Is my concept of using akka supervisor strategy completely wrong?


Solution

  • You can use BackoffSupervisor:

    Provided as a built-in pattern the akka.pattern.BackoffSupervisor implements the so-called exponential backoff supervision strategy, starting a child actor again when it fails, each time with a growing time delay between restarts.

    val supervisor = BackoffSupervisor.props(
      Backoff.onFailure(
        childProps,
        childName = "myEcho",
        minBackoff = 3.seconds,
        maxBackoff = 30.seconds,
        randomFactor = 0.2 // adds 20% "noise" to vary the intervals slightly 
      ).withAutoReset(10.seconds) // the child must send BackoffSupervisor.Reset to its parent 
      .withSupervisorStrategy(
        OneForOneStrategy() {
          case _: MyException => SupervisorStrategy.Restart
          case _ => SupervisorStrategy.Escalate
        }))