Akkaのエラーハンドリングストラテジを確認したい

はじめに

Akkaを実運用に突っ込もうと考えたときに一番最初に気になるのはActorがくたばった時の挙動ですよね。 知らない間にActorがくたばっていてメッセージだけが虚空の彼方に消えていったなんて事態になった日には次回の自身のボーナスも虚空に消えかねません。

ボーナスを死守するためにもエラーハンドリングは必須の知識として学ぶべきです。

サンプルコード

今回はこんな感じのサンプルコードを使用します。

package org.jyuch

import akka.actor.SupervisorStrategy.{Escalate, Restart, Resume, Stop}
import akka.actor.{Actor, ActorSystem, OneForOneStrategy, Props}

import scala.concurrent.duration._

import scala.language.postfixOps

object Hello {
  def main(args: Array[String]): Unit = {
    val system = ActorSystem()
    val calc = system.actorOf(CalcActor.props())

    calc ! Add(1, 2)
    calc ! Add(-1, 2)
    calc ! Add(1, 2)
    calc ! Subtract(3, 2)

    io.StdIn.readLine()
    system.terminate()
  }
}

trait OperandActor extends Actor {
  override def postRestart(reason: Throwable): Unit = {
    super.postRestart(reason)
    //println(reason.getClass.getTypeName)
  }
}

class CalcActor extends OperandActor {
  val add = context.actorOf(AddActor.props(), "add")
  val subtract = context.actorOf(SubtractActor.props(), "subtract")

  def receive = {
    case Result(v) =>{
      println(this + " : " + v)
    }
    case a: Add => add ! a
    case s: Subtract => subtract ! s
  }

  override val supervisorStrategy =
    OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
      case _: Exception => Stop
    }
}

object CalcActor {
  def props(): Props = Props[CalcActor]
}

class AddActor extends OperandActor {
  def receive = {
    case Add(x, y) => {
      println(this)
      if (x < 0 || y < 0) throw new Exception()
      sender() ! Result(x + y)
    }
  }
}

object AddActor {
  def props() = Props[AddActor]
}

class SubtractActor extends OperandActor {
  def receive = {
    case Subtract(x, y) => {
      println(this)
      if (x - y < 0) throw new Exception()
      sender() ! Result(x - y)
    }
  }
}

object SubtractActor {
  def props() = Props[SubtractActor]
}

sealed abstract class Operand

case class Add(x: Int, y: Int) extends Operand

case class Subtract(x: Int, y: Int) extends Operand

case class Result(value: Int)

子Actorの後始末

エラーハンドリングストラテジは親ActorのsupervisorStrategyで制御されますが、何も指定しない(デフォルト)とくたばったActorだけを再起動します。

supervisorStrategyで指定できるストラテジは以下の感じになります。

ストラテジ 意味
OneForOneStrategy 子Actorがくたばったらそいつだけ何とかする
AllForOneStrategy 子Actorがくたばったらすべての子Actorを何とかする

で、何とかする内容が以下の感じです。

ストラテジ 意味
Escalate 上位のActorに判断を委譲する
Restart 子Actorを再起動する
Resume エラーを気にせず続行する
Stop 子Actorを停止する

そして、ストラテジをこんな感じに指定します。

import akka.actor.OneForOneStrategy
import akka.actor.SupervisorStrategy._
import scala.concurrent.duration._

override val supervisorStrategy =
  OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
    case _: ArithmeticException      ⇒ Resume
    case _: NullPointerException     ⇒ Restart
    case _: IllegalArgumentException ⇒ Stop
    case _: Exception                ⇒ Escalate
  }

ガーディアンアクター

上記の指定で子がくたばった時の挙動を制御できるようになりました。じゃあ自身がくたばったらどうなるのさ?

自身がほかのActorの子だったらそいつが決めますが、自身がsystem.actorOfが生成されていた場合は自身の命運はガーディアンアクターが決めます。

doc.akka.io

Akka 2.1以降では設定ファイルのakka.actor.guardian-supervisor-strategyでガーディアンアクターのストラテジを設定出来ます。

doc.akka.io

  actor {

    # Either one of "local", "remote" or "cluster" or the
    # FQCN of the ActorRefProvider to be used; the below is the built-in default,
    # note that "remote" and "cluster" requires the akka-remote and akka-cluster
    # artifacts to be on the classpath.
    provider = "local"

    # The guardian "/user" will use this class to obtain its supervisorStrategy.
    # It needs to be a subclass of akka.actor.SupervisorStrategyConfigurator.
    # In addition to the default there is akka.actor.StoppingSupervisorStrategy.
    guardian-supervisor-strategy = "akka.actor.DefaultSupervisorStrategy"

現状ではakka.actor.StoppingSupervisorStrategyakka.actor.DefaultSupervisorStrategyが指定できるようです。

おわりに

これならActorがくたばったときにいい感じにActorを再起動できそうです。

これでボーナスを心配することなく安心して今夜も眠ることが出来そうです。

おわり

github.com

Scalaでも演算子のオーバーロードをしたい

はじめに

Scala演算子オーバーロードについてサクッと確認してみます。

オーバーロード

まぁ、とりあえず言語仕様を見ておけば安心だよねといった感じです。困ったら公式ドキュメントを読む。日本書紀にもそう書いてあります。

case class AltInt(val value: Int) {
  def +(x: AltInt) = plus(x)
  def plus(x: AltInt) = AltInt(value + x.value)
  def unary_~ = AltInt(value * -1)
  def +:(x: Int) = AltInt(value + x)
  def -:(x: AltInt) = { println(s"$value - ${x.value} = ${value - x.value}"); AltInt(value - x.value) }
  def *(x: AltInt) = AltInt(x.value * value)
  def ! = println(value)
}

中置演算子

Scalaでは引数を一つ取るメソッドはすべて中置演算子として扱えます。 逆に+のような演算子も普通*1のメソッドのように呼び出せます。

val a = AltInt(10)
val b = AltInt(20)

println(a.plus(b))
println(a plus b)
println(a.+(b))
println(a + b)

なお、優先順位はメソッドの先頭の文字によって規定されるようです。

先頭の文字 優先度
(all letters) 低い
|
^
&
= !
< >
:
+ -
* / %
(all other special characters) 高い
val a = AltInt(10)
val b = AltInt(20)
println(a * a + b) // 120

また、メソッドが:で終わる場合は引数とレシーバの位置関係が逆転します。

println(1 +: a)

また、通常演算子は左結合ですがこの場合は右結合になります。

println(b -: a -: a)
10 - 10 = 0
0 - 20 = -20
AltInt(-20)

後置演算子

中置演算子と同じように引数を取らないメソッドは後置演算子として呼び出せます。

println(a!)
println(a.!)

前置演算子

前置演算子unary_記号で定義できますが、記号に入ることができるのは+-!~に限られます。

println(~a)
println(a.unary_~)

おわりに

素直でいいですね。

*1:普通?普通ってなんだ?

implicitパラメータとは何ぞや

はじめに

PlayでコントローラからビューからFuture[T].mapからお前らimplicitパラメータ好きすぎだろってくらい多用されています。これはゆるふわっと使っていると後でヤバい目に遭うなと思ったので軽く調べてみました。

ゆるふわ愛されimplicit

言語仕様のimplicitsセクションを見ながら確認します。困ったときは公式ドキュメントを読む。古事記にもそう書いてあります。

とりあえず以下が一番シンプルな形です。

object Hello {
  def main(args: Array[String]): Unit = {
    implicit val hoge = "jyuch"

    greet("Hello")
  }

  def greet(message: String)(implicit who: String): Unit = {
    println(message + " " + who)
  }
}

Scalaコンパイラimplicitパラメータが明示的に与えられていないときはメソッドの呼び出し箇所からプレフィックス無しでアクセス可能なimplicit付き識別子を探し出してパラメータを適用します。 Scalaコンパイラ頑張りすぎてない?大丈夫?過労死しない?顔色悪いんじゃない?

また、プレフィックスなしでアクセス可能なら検索対象になりうるということは、何でもかんでもワイルドカードでインポートすると誤爆する可能性もあるってことですよね。

当たり前と言えば当たり前ですが、候補となるパラメータが複数ある場合はどちらを使えばいいか判断ができないためコンパイルエラーとなります。

  def main(args: Array[String]): Unit = {
    implicit val hoge = "jyuch"
    implicit val fuga = "hoge"

    // Error:(8, 10) ambiguous implicit values:
    // both value fuga of type String
    // and value hoge of type String
    // match expected type String
    // greet("Hello")
    greet("Hello")
  }

こちらも当たり前と言えば当たり前ですが、トレイトの実装クラスや抽象クラスの実装クラスも適用します。

object Hello {
  def main(args: Array[String]): Unit = {
    implicit val fuga = new Fuga()

    greet("Hello")
  }

  def greet(message: String)(implicit who: Hoge): Unit = {
    println(message + " " + who.who)
  }
}

trait Hoge {
  val who = "hoge"
}

class Fuga extends Hoge {
  override val who = "jyuch"
}

また、シングルトンオブジェクトでも出来るようです。(これは意味の無い例ですが)

object Hello {

  implicit object Hoge {
    val who = "jyuch"
  }

  def main(args: Array[String]): Unit = {
    greet("Hello")
  }

  def greet(message: String)(implicit who: Hoge.type): Unit = {
    println(message + " " + who.who)
  }
}

また、パターンとして

  • compound type(hoge: Hoge with Fugaみたいなアレ)
  • parameterized type(S[T1, T2...Tn]みたいなアレ)
  • type projection S#U(ネストクラスをいい感じにするアレ)
  • エイリアス
  • implicit val aaa: Hoge => Fuga = { x: Hoge => Fuga(x.name) }def piyo(implicit a: Hoge => Fuga)ってなるアレ(名前を知らない)

も大丈夫なようです。*1

で、一番気になるのが暗黙の型変換と暗黙のパラメータを組み合わせて適用可能なのかといったところです。これが可能ならば暗黒プログラミングが捗る可読性が下がってつらいですね。

object Hello {

  implicit def hoge2Fuga(hoge: Hoge): Fuga = hoge match {
    case Hoge(x) => Fuga(x)
  }

  def main(args: Array[String]): Unit = {
    implicit val name = Hoge("jyuch")

    greet("Hello")
  }

  def greet(message: String)(implicit who: Fuga): Unit = {
    println(message + " " + who.name)
  }

  def piyo(implicit a: Hoge => Fuga) {}
}

case class Hoge(name: String)

case class Fuga(name: String)
Error:(14, 10) could not find implicit value for parameter who: example.Fuga
    greet("Hello")
Error:(14, 10) not enough arguments for method greet: (implicit who: example.Fuga)Unit.
Unspecified value parameter who.
    greet("Hello")

暗黙の型変換をぶちかましつつ暗黙の引数に突っ込むまではしないようです。

おわってない

と、ここまでで仕様書のImplicit Parametersセクションの半分です。

残り半分くらいでネストされた場合のimplicitパラメータの挙動について述べていますが、力尽きました。

おわりに

Scalaむずかしい

*1:2.12の言語仕様ではもう一つ箇条書きの項目があるのですが、何を言っているのか弊社には全然わかりませんでした