Тестирование жизненного цикла актора - ActorLifeCycleSpec

Тесткейс ActorLifeCycleSpec содержит несколько тестов, описывающих жизненный цикл актора. Рассмотрим 1-й, тестирующий хуки на запуск и останов актора. Вообще тесткейс устроен следующим образом - в нем создаются актор-супервизор (Supervisor), и тестовый актор LifeCycleTestActor. Супервизор устроен очень просто:

class Supervisor(override val supervisorStrategy: SupervisorStrategy) extends Actor {

  def receive = {
    case x: Props  sender() ! context.actorOf(x)
  }

  override def preRestart(cause: Throwable, msg: Option[Any]) {}
}

Т.е. для него будет переопределена supervisorStrategy - см. ниже; preRestart переопределен для того, чтобы предотвратить дефолтное поведение, которое заключается в том, что все child акторы останавливаются при перезапуске родителя. Все, что делает этот актор - принимает на вход конфигурационный объект Props, и возвращает child актор, с которым уже можно поэкспериментировать.

class LifeCycleTestActor(testActor: ActorRef, id: String, generationProvider: AtomicInteger) extends Actor {

  def report(msg: Any) = testActor ! message(msg)

  def message(msg: Any): Tuple3[Any, String, Int] = (msg, id, currentGen)

  val currentGen = generationProvider.getAndIncrement()

  override def preStart() { report("preStart") }

  override def postStop() { report("postStop") }

  def receive = { case "status"  sender() ! message("OK") }
}

testActor - это экземпляр специального актора TestActor из TestKit, который предназначен для верификации сообщений. id - уникальный идентификатор актора, и gen - так сказать, “номер поколения”. ’LifeCycleTestActor’ отправляет сообщения testActorу, которые мы можем верифицировать в тесте.

Вначале создается супервизор. Здесь стоит обратить внимание на параметр maxNrOfRetries. В некоторых случаях мы хотели бы сказать супервизору, чтобы он после нескольких перезапусков больше не пытался перезапустить актор:

val id = newUuid.toString
val supervisor = system.actorOf(Props(classOf[Supervisor], OneForOneStrategy(maxNrOfRetries = 3)(List(classOf[Exception]))))

Затем конфигурационный объект для child актора, который является экземпляром LifeCycleTestActor:

val gen = new AtomicInteger(0)
val restarterProps = Props(new LifeCycleTestActor(testActor, id, gen) {

  override def preRestart(reason: Throwable, message: Option[Any]) {
    report("preRestart")
  }

  override def postRestart(reason: Throwable) {
    report("postRestart")
  }
}).withDeploy(Deploy.local)

И наконец, сам актор создается с помощью супервизора:

val restarter = Await.result((supervisor ? restarterProps).mapTo[ActorRef], timeout.duration)

И первое же сообщение, которое мы получим, должно быть, разумеется, "preStart":

expectMsg(("preStart", id, 0))

Теперь, мы отсылаем сообщение Kill, и у нас должна возникнуть новая “реинкарнация” актора:

restarter ! Kill
expectMsg(("preRestart", id, 0))
expectMsg(("postRestart", id, 1))
restarter ! "status"
expectMsg(("OK", id, 1))

То же самое происходит и второй, и третий раз.

restarter ! Kill
expectMsg(("preRestart", id, 1))
expectMsg(("postRestart", id, 2))
restarter ! "status"
expectMsg(("OK", id, 2))
restarter ! Kill
expectMsg(("preRestart", id, 2))
expectMsg(("postRestart", id, 3))
restarter ! "status"
expectMsg(("OK", id, 3))

На четвертый же раз, поскольку мы указали maxNrOfRetries равным 3, то child актор restarter более не перезапустится.

restarter ! Kill
expectMsg(("postStop", id, 3))
expectNoMsg(1 seconds)