ましめも

技術系メモ

お手軽Akka Schedulerとハマりポイント

定期実行処理を簡単に行えるAkka Schedulerというものがある。Akkaはplay framework上から簡単に使うことができるので、バッチ処理等を行うのにとても便利。
Schedulerには次のようなメソッドが定義されている*1

  • def schedule(initialDelay: FiniteDuration, interval: FiniteDuration)(f: => Unit)
    • initialDelay後, intervalの間隔で関数fを実行する
  • def schedule(initialDelay: FiniteDuration, interval: FiniteDuration, receiver: ActorRef, message: Any)
    • initialDelay後, intervalの間隔で receiver(Actor)に message を送る

「ちょっと定期実行したいだけだし&Actor定義するのめんどくさいし前者を使おう」と思って使ったら1つ問題点に気づいた。

例外発生時、定期実行されなくなってしまう

この方法の問題点は、scheduleに渡した関数内で例外が発生すると定期実行されなくなってしまうということだ*2。この方法で確実に定期実行するなら決して例外が発生しないようにしないといけない。

    var i = 0
    Akka.system.scheduler.schedule(5 seconds, 1 seconds) {
      i = i + 1
      Logger.info(s"$i 回目 実行します")
      if (i > 5) {
        throw new IllegalArgumentException("ごめん")
      }
      Logger.info("実行に成功しました!")
    }

実行結果(6回で実行が止まってしまう)

[info] application - 1 回目 実行します
[info] application - 実行に成功しました!
[info] application - 2 回目 実行します
[info] application - 実行に成功しました!
[info] application - 3 回目 実行します
[info] application - 実行に成功しました!
[info] application - 4 回目 実行します
[info] application - 実行に成功しました!
[info] application - 5 回目 実行します
[info] application - 実行に成功しました!
[info] application - 6 回目 実行します
java.lang.IllegalArgumentException: ごめん
	at Global$$anonfun$autoCleanUpFileStorage$1.apply$mcV$sp(Global.scala:35)
...
※以降何も出力されない

Actorを使った場合は例外が発生しても止まらない

一方でActorを定義しActorに定期的にメッセージを送るように設定した場合は、処理中に例外が発生しても止まることはない*3

  var j = 0
  class BatchActor extends Actor {
    def receive: Actor.Receive = {
      case e: String =>
        j = j + 1
        Logger.info(s"$j 回目 実行します")
        if (j > 5) {
          throw new IllegalArgumentException("ごめん")
        }
        Logger.info("実行に成功しました!")
    }
  }
// 1秒毎にBatchActorにメッセージ"はい"を送る
Akka.system.scheduler.schedule(5 seconds, 1 seconds, Akka.system.actorOf(Props[BatchActor]), "はい")

実行結果(6回目以降もずっと呼ばれ続けている)

[info] application - 1 回目 実行します
[info] application - 実行に成功しました!
[info] application - 2 回目 実行します
[info] application - 実行に成功しました!
[info] application - 3 回目 実行します
[info] application - 実行に成功しました!
[info] application - 4 回目 実行します
[info] application - 実行に成功しました!
[info] application - 5 回目 実行します
[info] application - 実行に成功しました!
[info] application - 6 回目 実行します
[ERROR] [02/11/2014 20:26:07.746] [application-akka.actor.default-dispatcher-5] [akka://application/user/$a] ごめん
java.lang.IllegalArgumentException: ごめん
	at Global$BatchActor$$anonfun$receive$1.applyOrElse(Global.scala:37)
..

[info] application - 7 回目 実行します
[ERROR] [02/11/2014 20:26:08.741] [application-akka.actor.default-dispatcher-5] [akka://application/user/$a] ごめん
java.lang.IllegalArgumentException: ごめん
	at Global$BatchActor$$anonfun$receive$1.applyOrElse(Global.scala:37)
..
[info] application - 8 回目 実行します
[ERROR] [02/11/2014 20:26:09.741] [application-akka.actor.default-dispatcher-2] [akka://application/user/$a] ごめん
..

この仕様を知らずに前者の関数を渡すschedule関数を使っている&例外処理をちゃんとしてないと、「あれれー?気づいたら定期実行されなくなってるぞー」ということになるので注意。

*1:http://doc.akka.io/docs/akka/2.1.0/scala/scheduler.html

*2:https://github.com/akka/akka/blob/v2.2.0/akka-actor/src/main/scala/akka/actor/Scheduler.scala#L527 例外が発生した場合は次の実行のためのスケジューリングがされない

*3:https://github.com/akka/akka/blob/v2.2.0/akka-actor/src/main/scala/akka/actor/Scheduler.scala#L62 メッセージの送信自体で例外が発生した場合やActorが終了している場合は止まる