以下更加深入地讲解错误处理的机制和可选的方法。
为了演示我们假设有这样的策略:
import akka.actor.OneForOneStrategy
import akka.actor.SupervisorStrategy._
import akka.util.duration._
override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
case _: ArithmeticException ⇒ Resume
case _: NullPointerException ⇒ Restart
case _: IllegalArgumentException ⇒ Stop
case _: Exception ⇒ Escalate
}
我选择了一种非常著名的异常类型来演示监管和监控 中描述的错误处理方式的使用. 首先,它是一个一对一的策略,意思是每一个子actor会被单独处理(多对一的策略与之相似,唯一的差别在于任何决策都应用于监管者的所有 子actor,而不仅仅是出错的那一个). 这里我们对重启的频率作了限制,最多每分钟能进行 10 次重启; 所有这样的设置都可以被忽略,也就是说,相应的限制并不被采用, 留下了设置重启频率的绝对上限值或让重启无限进行的可能性。
构成主体的 match 语句的类型是 Decider, 它是 PartialFunction[Throwable, Directive]. 这一部分将 子actor的失败类型映射到相应的指令 .
缺省的监管机制
如果定义的监管机制没有覆盖抛出的异常,将使用上溯 机制.
如果某个actor没有定义监管机制,下列异常将被缺省地处理:
ActorInitializationException 将终止出错的子 actor
ActorKilledException 将终止出错的子 actor
Exception 将重启出错的子 actor
其它的 Throwable 将被上溯传给父actor
如果异常一直被上溯到根监管者,在那儿也会用上述缺省方式进行处理。
测试应用
以下部分展示了实际中不同的指令的效果,为此我们需要创建一个测试环境。首先我们需要一个合适的监管者:
import akka.actor.Actor
class Supervisor extends Actor {
import akka.actor.OneForOneStrategy
import akka.actor.SupervisorStrategy._
import akka.util.duration._
override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
case _: ArithmeticException ⇒ Resume
case _: NullPointerException ⇒ Restart
case _: IllegalArgumentException ⇒ Stop
case _: Exception ⇒ Escalate
}
def receive = {
case p: Props ⇒ sender ! context.actorOf(p)
}
}
该监管者将用来创建一个我们用来做试验的子actor:
import akka.actor.Actor
class Child extends Actor {
var state = 0
def receive = {
case ex: Exception ⇒ throw ex
case x: Int ⇒ state = x
case "get" ⇒ sender ! state
}
}
这个测试可以用 测试 Actor 系统 (Scala) 中的工具来进行简化, 比如 AkkaSpec 是 TestKit with WordSpec with MustMatchers 的混合
import akka.testkit.{ AkkaSpec, ImplicitSender, EventFilter }
import akka.actor.{ ActorRef, Props, Terminated }
class FaultHandlingDocSpec extends AkkaSpec with ImplicitSender {
"A supervisor" must {
"apply the chosen strategy for its child" in {
// 在此添加代码
}
}
}
现在我们来创建 actor:
val supervisor = system.actorOf(Props[Supervisor], "supervisor")
supervisor ! Props[Child]
val child = expectMsgType[ActorRef] // 从TestKit的 testActor 中获取答案
第一个测试是为了演示 Resume 指令, 我们试着将actor设为非初始状态然后让它出错:
child ! 42 // 将状态设为 42
child ! "get"
expectMsg(42)
child ! new ArithmeticException // 让它崩溃
child ! "get"
expectMsg(42)
可以看到错误处理指令完后仍能得到42的值. 现在如果我们将错误换成更严重的 NullPointerException, 情况就不同了:
child ! new NullPointerException // 更严重的崩溃
child ! "get"
expectMsg(0)
而最后当致命的 IllegalArgumentException 发生时子actor将被其监管者终止:
watch(child) // 让 testActor 监视 “child”
child ! new IllegalArgumentException // 破坏它
expectMsg(Terminated(child))
child.isTerminated must be(true)
到目前为止监管者完全没有被子actor的错误所影响, 因为指令集确实处理了这些错误。而对于 Exception, 就不是这么回事了, 监管者会将失败上溯传递。
supervisor ! Props[Child] // 创建新的子actor
val child2 = expectMsgType[ActorRef]
watch(child2)
child2 ! "get" // 确认它还活着
expectMsg(0)
child2 ! new Exception("CRASH") // 上溯失败
expectMsg(Terminated(child2))
监管者自己是被 ActorSystem 的顶级actor所监管的。顶级actor的缺省策略是对所有的 Exception 情况 (注意 ActorInitializationException 和 ActorKilledException 是例外)进行重启. 由于缺省的重启指令会杀死所有的子actor,我们知道我们可怜的子actor最终无法从这个失败中幸免。
如果这不是我们希望的行为 (这取决于实际用例), 我们需要使用一个不同的监管者来覆盖这个行为。
class Supervisor2 extends Actor {
import akka.actor.OneForOneStrategy
import akka.actor.SupervisorStrategy._
import akka.util.duration._
override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
case _: ArithmeticException ⇒ Resume
case _: NullPointerException ⇒ Restart
case _: IllegalArgumentException ⇒ Stop
case _: Exception ⇒ Escalate
}
def receive = {
case p: Props ⇒ sender ! context.actorOf(p)
}
// 覆盖在重启时杀死所有子actor的缺省行为
override def preRestart(cause: Throwable, msg: Option[Any]) {}
}
在这个父actor之下,子actor在上溯的重启中得以幸免,如以下最后的测试:
val supervisor2 = system.actorOf(Props[Supervisor2], "supervisor2")
supervisor2 ! Props[Child]
val child3 = expectMsgType[ActorRef]
child3 ! 23
child3 ! "get"
expectMsg(23)
child3 ! new Exception("CRASH")
child3 ! "get"
expectMsg(0)