AkkaでもActorの再起動を遅延したい

はじめに

Actorはエラー発生時に再起動させることが出来ますが、デフォルトの挙動では可能な限りすぐに再起動をさせようとするためDBやWebサービス等の障害が原因ですぐに復旧しない場合はActorの生成→停止を繰り返し死屍累々となることが予想出来ます。

という訳でBackoffSupervisorパターンを使って再起動を遅延させる方法を確認してみました。

Actor

今回テストで使用するActorの実装は以下の感じです。

メッセージとして例外を受け取ったらそれをスローします。

import akka.actor.Actor
import akka.event.Logging

class HogeActor extends Actor {
  val log = Logging(context.system, this)

  override def receive = {
    case e: Exception => {
      throw e
    }
    case message: String => {
      log.info(message)
    }
  }

  override def preStart() = {
    log.info("preStart")
  }
}

BackoffSupervisor

再起動を遅延させるためにはPropsを生成するときにBackoffSupervisorを挟んで生成するようです。

val supervisor = BackoffSupervisor.props(
  Backoff.onFailure(
    Props[HogeActor],
    childName = "hoge",
    minBackoff = 1 seconds,
    maxBackoff = 1 seconds,
    randomFactor = 0.02
  ).withSupervisorStrategy(
    OneForOneStrategy(maxNrOfRetries = 5, withinTimeRange = 1 minutes) {
      case _: NullPointerException => SupervisorStrategy.Restart
      case _: Exception => SupervisorStrategy.Resume
    }
  )
)

ここでは最小遅延が1秒、最大遅延が1秒でランダムファクターが2%で遅延させます。 ここでのランダムファクターとは、複数のActorが同時に再起動しようとして負荷が跳ね上がるのを防ぐために遅延をランダムに変動させるために使用される値です。

また、SupervisorStrategyとしてNullPointerExceptionの時は再起動、それ以外の時は再開させてます。また、1分以内に5回エラーを送出したら子Actorを停止させます

val hoge = system.actorOf(supervisor, "hoge")

hoge ! "hello"
hoge ! new NullPointerException()
hoge ! "fuga"
hoge ! "fuga"
hoge ! "fuga"

Thread.sleep(1500)

hoge ! new Exception()
hoge ! "piyo"

hoge ! new NullPointerException()
Thread.sleep(1500)
hoge ! new NullPointerException()
Thread.sleep(1500)
hoge ! new NullPointerException()
Thread.sleep(1500)
hoge ! new NullPointerException()
Thread.sleep(1500)
hoge ! new NullPointerException()
Thread.sleep(1500)
hoge ! "fugafuga"
[INFO] [01/08/2018 21:30:36.728] [backoff-akka.actor.default-dispatcher-5] [akka://backoff/user/hoge/hoge] preStart
[INFO] [01/08/2018 21:30:36.729] [backoff-akka.actor.default-dispatcher-5] [akka://backoff/user/hoge/hoge] hello
[ERROR] [01/08/2018 21:30:36.740] [backoff-akka.actor.default-dispatcher-3] [akka://backoff/user/hoge/hoge] null
java.lang.NullPointerException
    at jyuch.Hello$.delayedEndpoint$jyuch$Hello$1(Hello.scala:31)
    at jyuch.Hello$delayedInit$body.apply(Hello.scala:10)
    at scala.Function0.apply$mcV$sp(Function0.scala:34)
    at scala.Function0.apply$mcV$sp$(Function0.scala:34)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
    at scala.App.$anonfun$main$1$adapted(App.scala:76)
    at scala.collection.immutable.List.foreach(List.scala:389)
    at scala.App.main(App.scala:76)
    at scala.App.main$(App.scala:74)
    at jyuch.Hello$.main(Hello.scala:10)
    at jyuch.Hello.main(Hello.scala)

[INFO] [01/08/2018 21:30:36.740] [backoff-akka.actor.default-dispatcher-3] [akka://backoff/user/hoge/hoge] Message [java.lang.String] without sender to Actor[akka://backoff/user/hoge/hoge#1204086463] was not delivered. [1] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.
[INFO] [01/08/2018 21:30:36.740] [backoff-akka.actor.default-dispatcher-3] [akka://backoff/user/hoge/hoge] Message [java.lang.String] without sender to Actor[akka://backoff/user/hoge/hoge#1204086463] was not delivered. [2] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.
[INFO] [01/08/2018 21:30:36.740] [backoff-akka.actor.default-dispatcher-3] [akka://backoff/user/hoge/hoge] Message [java.lang.String] without sender to Actor[akka://backoff/user/hoge/hoge#1204086463] was not delivered. [3] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.
[INFO] [01/08/2018 21:30:37.776] [backoff-akka.actor.default-dispatcher-5] [akka://backoff/user/hoge/hoge] preStart
[WARN] [01/08/2018 21:30:38.215] [backoff-akka.actor.default-dispatcher-3] [akka://backoff/user/hoge/hoge] null
[INFO] [01/08/2018 21:30:38.215] [backoff-akka.actor.default-dispatcher-2] [akka://backoff/user/hoge/hoge] piyo
[ERROR] [01/08/2018 21:30:38.215] [backoff-akka.actor.default-dispatcher-3] [akka://backoff/user/hoge/hoge] null
java.lang.NullPointerException
    at jyuch.Hello$.delayedEndpoint$jyuch$Hello$1(Hello.scala:41)
    at jyuch.Hello$delayedInit$body.apply(Hello.scala:10)
    at scala.Function0.apply$mcV$sp(Function0.scala:34)
    at scala.Function0.apply$mcV$sp$(Function0.scala:34)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
    at scala.App.$anonfun$main$1$adapted(App.scala:76)
    at scala.collection.immutable.List.foreach(List.scala:389)
    at scala.App.main(App.scala:76)
    at scala.App.main$(App.scala:74)
    at jyuch.Hello$.main(Hello.scala:10)
    at jyuch.Hello.main(Hello.scala)

[INFO] [01/08/2018 21:30:39.249] [backoff-akka.actor.default-dispatcher-3] [akka://backoff/user/hoge/hoge] preStart
[ERROR] [01/08/2018 21:30:39.724] [backoff-akka.actor.default-dispatcher-5] [akka://backoff/user/hoge/hoge] null
java.lang.NullPointerException
    at jyuch.Hello$.delayedEndpoint$jyuch$Hello$1(Hello.scala:43)
    at jyuch.Hello$delayedInit$body.apply(Hello.scala:10)
    at scala.Function0.apply$mcV$sp(Function0.scala:34)
    at scala.Function0.apply$mcV$sp$(Function0.scala:34)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
    at scala.App.$anonfun$main$1$adapted(App.scala:76)
    at scala.collection.immutable.List.foreach(List.scala:389)
    at scala.App.main(App.scala:76)
    at scala.App.main$(App.scala:74)
    at jyuch.Hello$.main(Hello.scala:10)
    at jyuch.Hello.main(Hello.scala)

[INFO] [01/08/2018 21:30:40.757] [backoff-akka.actor.default-dispatcher-5] [akka://backoff/user/hoge/hoge] preStart
[ERROR] [01/08/2018 21:30:41.227] [backoff-akka.actor.default-dispatcher-2] [akka://backoff/user/hoge/hoge] null
java.lang.NullPointerException
    at jyuch.Hello$.delayedEndpoint$jyuch$Hello$1(Hello.scala:45)
    at jyuch.Hello$delayedInit$body.apply(Hello.scala:10)
    at scala.Function0.apply$mcV$sp(Function0.scala:34)
    at scala.Function0.apply$mcV$sp$(Function0.scala:34)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
    at scala.App.$anonfun$main$1$adapted(App.scala:76)
    at scala.collection.immutable.List.foreach(List.scala:389)
    at scala.App.main(App.scala:76)
    at scala.App.main$(App.scala:74)
    at jyuch.Hello$.main(Hello.scala:10)
    at jyuch.Hello.main(Hello.scala)

[INFO] [01/08/2018 21:30:42.265] [backoff-akka.actor.default-dispatcher-2] [akka://backoff/user/hoge/hoge] preStart
[ERROR] [01/08/2018 21:30:42.733] [backoff-akka.actor.default-dispatcher-5] [akka://backoff/user/hoge/hoge] null
java.lang.NullPointerException
    at jyuch.Hello$.delayedEndpoint$jyuch$Hello$1(Hello.scala:47)
    at jyuch.Hello$delayedInit$body.apply(Hello.scala:10)
    at scala.Function0.apply$mcV$sp(Function0.scala:34)
    at scala.Function0.apply$mcV$sp$(Function0.scala:34)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
    at scala.App.$anonfun$main$1$adapted(App.scala:76)
    at scala.collection.immutable.List.foreach(List.scala:389)
    at scala.App.main(App.scala:76)
    at scala.App.main$(App.scala:74)
    at jyuch.Hello$.main(Hello.scala:10)
    at jyuch.Hello.main(Hello.scala)

[INFO] [01/08/2018 21:30:43.777] [backoff-akka.actor.default-dispatcher-3] [akka://backoff/user/hoge/hoge] preStart
[ERROR] [01/08/2018 21:30:44.234] [backoff-akka.actor.default-dispatcher-5] [akka://backoff/user/hoge/hoge] null
java.lang.NullPointerException
    at jyuch.Hello$.delayedEndpoint$jyuch$Hello$1(Hello.scala:49)
    at jyuch.Hello$delayedInit$body.apply(Hello.scala:10)
    at scala.Function0.apply$mcV$sp(Function0.scala:34)
    at scala.Function0.apply$mcV$sp$(Function0.scala:34)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
    at scala.App.$anonfun$main$1$adapted(App.scala:76)
    at scala.collection.immutable.List.foreach(List.scala:389)
    at scala.App.main(App.scala:76)
    at scala.App.main$(App.scala:74)
    at jyuch.Hello$.main(Hello.scala:10)
    at jyuch.Hello.main(Hello.scala)

[INFO] [01/08/2018 21:30:45.741] [backoff-akka.actor.default-dispatcher-5] [akka://backoff/user/hoge] Message [java.lang.String] without sender to Actor[akka://backoff/user/hoge#324524509] was not delivered. [4] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.

Actorが再起動する前にメッセージボックスに到達したメッセージはdead-letters扱いになるようです。

また、5回エラーを送出された時点でActorの再起動を停止しています。

おわりに

github.com

おわり