Liam Haworth
Liam Haworth

Reputation: 858

Handling exceptions in preStart in actors

I have a service that has a supervisor responsible for build children actors and handling their exceptions.

ServiceMain -> Supervisor -> DiscoveryActor

To build the DiscoveryActor I call the following

Await.result(supervisor ? (Props[Discovery], "Discovery"), Duration.create(60, SECONDS)) match {
    case actor: ActorRef =>
      discoveryActor = actor

    case ex: Exception =>
      logger.error("Failed to initialize discovery actor", ex)
      sys.exit(1)
}

Which gets handled by this code in Supervisor

def receive = {
  case p: Props => sender() ! context.actorOf(p)

  case (p: Props, s: String) => sender() ! context.actorOf(p, s)
}

It is expected of the DiscoveryActor to throw an exception in preStart() if it can't research a host as configured. The exception ApiConnectionException is thrown by the actor and is captured by akka and turned into a ActorInitializationException.

I have tired catching this exception in the Await and with the superviseStrategy as below

override val supervisorStrategy =
  AllForOneStrategy() {
    case _: Exception                =>       Escalate
  }

But neither of these have managed to catch it, what I am attempting to do is catch such exception and exit the application.

If anyone can point out where I am going wrong or what I have missed then I shall be very grateful!

Upvotes: 0

Views: 1564

Answers (1)

Maciej Falski
Maciej Falski

Reputation: 249

I simplified your code a bit just to get straight to the root of the problem. You can copy it and paste it into your editor. It uses ScalaTest suite.

SupervisorStrategy defined in Supervisor actor does catch an exception thrown by Discovery actor in preStart method. You might want to have a closer look at your own code.

Your Await block is trying to catch an exception, but it's not possible in this context. The Exception is thrown by Discovery actor, not send as a message. Ask pattern (?) you use simply awaits for a message to arrive though. Only use of SupervisorStrategy can get you back a thrown exception. Instead of escalating exception in your Supervisor you could send a message to your application Guardian actor saying that initialization failed so the application should exit. Or do it directly in your Supervisor.

import java.util.concurrent.TimeUnit    
import akka.actor.SupervisorStrategy.Escalate
import akka.actor._
import akka.pattern.ask
import akka.testkit.{ImplicitSender, TestKit}
import akka.util.Timeout
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike, Matchers}

import scala.concurrent.Await

abstract class ActorSuite(systemName: String)
  extends TestKit(ActorSystem(systemName))
  with FunSuiteLike
  with ImplicitSender
  with Matchers
  with BeforeAndAfterAll {

  override def afterAll {
    TestKit.shutdownActorSystem(system)
  }
}

class FailingActorInitializationSuite extends ActorSuite("failing-system") {


  test("run it") {

    val supervisor = system.actorOf(Props[Supervisor])
    var discoveryActor: ActorRef = null

    implicit val timeout = Timeout(60, TimeUnit.SECONDS)

    Await.result(
      supervisor ?(Props[Discovery], "Discovery"), timeout.duration) match {
      case actor: ActorRef =>
        discoveryActor = actor
    }
  }
}

class Supervisor extends Actor with ActorLogging {

  override val supervisorStrategy =
    AllForOneStrategy() {
      case e: Exception =>
        log.error(s"Caught an exception [${e.getCause.getMessage}] and escalating")
        Escalate
    }

  override def receive: Receive = {
    case (p: Props, s: String) => sender() ! context.actorOf(p, s)
  }
}

class Discovery extends Actor {

  override def preStart(): Unit = {
    super.preStart()
    throw new RuntimeException("Can't create")
  }

  override def receive: Actor.Receive = {
    case _ =>
  }
}

Upvotes: 3

Related Questions