Search code examples
scalaakkaactorakka-supervision

Does Akka automatically copy over variables when an actor fails


In Akka Cookbook by Héctor Veiga Ortiz, the reader is told that

When an actor throws an exception, it sends a message to the supervisor, and the supervisor handles the failure by restarting that actor. It clears out the accumulated state of the actor, and creates a fresh new actor, means, it then restores the last value assigned to the state of old actor to the preRestart value.

However, I tried testing the following code, which suggests that what the author says isn't true.

import akka.actor._
import akka.actor.SupervisorStrategy._
import akka.util.Timeout
import scala.concurrent.Await
import scala.concurrent.duration._
import akka.pattern.ask

case object Error
case class StopActor(actorRef: ActorRef)
case object Inc

class LifeCycleActor extends Actor {
  var sum = 1
  override def preRestart(reason: Throwable, message: Option[Any]):Unit =
    println(s"sum in preRestart is $sum")
  override def preStart(): Unit = println(s"sum in preStart is $sum")
  def receive = {
    case Inc => sum += 1
    case Error => throw new ArithmeticException()
    case _ => println("default msg")
  }
  override def postStop(): Unit =
    println(s"sum in postStop is ${sum * 3}")
  override def postRestart(reason: Throwable): Unit = {
    sum = sum * 2
    println(s"sum in postRestart is $sum")
  }
}

class Supervisor extends Actor {
  override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute){
    case _: ArithmeticException => Restart
    case t =>
      super.supervisorStrategy.decider.applyOrElse(t, (_:Any)=>Escalate)
  }
  def receive = {
    case (props: Props, name: String) => sender ! context.actorOf(props, name)
    case StopActor(actorRef) => context.stop(actorRef)
  }
}

object ActorLifeCycle extends App {
  implicit val timeout = Timeout(2 seconds)
  val actorSystem = ActorSystem("Supervision")
  val supervisor = actorSystem.actorOf(Props[Supervisor], "supervisor")
  val childFuture = supervisor ? (Props(new LifeCycleActor), "LifeCycleActor")
  val child = Await.result(childFuture.mapTo[ActorRef], 2 seconds)
  child ! Inc
  child ! Error
  Thread.sleep(1000)
  supervisor ! StopActor(child)
}

The output I get is as follows.

sbt:chpt2_ActorLifeCycle> runMain ActorLifeCycle
sum in preStart is 1
sum in preRestart is 2
[ERROR] [11/08/2018 20:06:01.423] [Supervision-akka.actor.default-dispatcher-4] [akka://Supervision/user/supervisor/LifeCycleActor] null
java.lang.ArithmeticException
sum in postRestart is 2
sum in postStop is 6

If what the author says is true, the final value should be double what it is.


Solution

  • First I guess you forget to add sum += 1 when receive message Inc in child actor when post the question, please modify. Otherwise, as I test, you cannot get your output.

    Next explain your code:

    From next diagram, you can see the preReStart is called on old instance, not on new instance.

    enter image description here

    There is also a desc about this, detail here

    1. The old actor is informed by calling preRestart with the exception which caused the restart and the message which triggered that exception; the latter may be None if the restart was not caused by processing a message, e.g. when a supervisor does not trap the exception and is restarted in turn by its supervisor, or if an actor is restarted due to a sibling’s failure. If the message is available, then that message’s sender is also accessible in the usual way (i.e. by calling sender). This method is the best place for cleaning up, preparing hand-over to the fresh actor instance, etc. By default it stops all children and calls postStop.
    2. The initial factory from the actorOf call is used to produce the fresh instance.
    3. The new actor’s postRestart method is invoked with the exception which caused the restart. By default the preStart is called, just as in the normal start-up case.

    So, for your example:

    • when call preRestart, it print the sum of old actor, that is 2, notice: you have inc it.
    • when call postRestart, it print the sum of new actor, that is the initial value 1 with the calculation sum = sum * 2, finally print 2, not 4. The Inc message just receive on old instance, not on new instance.

    Finally, the content of the book:

    When an actor throws an exception, it sends a message to the supervisor, and the supervisor handles the failure by restarting that actor. It clears out the accumulated state of the actor, and creates a fresh new actor, means, it then restores the last value assigned to the state of old actor to the preRestart value.

    I think what you concern is it then restores the last value assigned to the state of old actor to the preRestart value. I don't quite know what it means, if you just think it assign the last value of old actor to preRestart function, then it is correct, as it just run on old instance, otherwise, it seems conflict with akka official guide & experiment; And, if want to restore the value, we may had to use resume not restart. Anyway, I think we should use the akka offical document as a standard, and understand the correct logic.