我尝试使用akka
和futures
了解故障处理。
例如,我有父母和儿童演员。
儿童演员有两个失败案例:
消息处理时发生情况1)错误
案例2)错误发生在未来
在这两种情况下我都需要将错误传播给父级,但在第二种情况下,它不会发生。我做错了什么?
import akka.actor.SupervisorStrategy.{Decider, Stop}
import akka.actor.{Actor, ActorRef, ActorSystem, OneForOneStrategy, Props, SupervisorStrategy}
import akka.testkit.{TestKit, TestProbe}
import org.junit.{After, Before, Test}
import scala.concurrent.Future
import scala.util.{Failure, Success}
class Parent(_case: String, probe: ActorRef) extends Actor {
val child = context.actorOf(Props(new Child(_case)), "myLittleChild")
final val defaultStrategy: SupervisorStrategy = {
def defaultDecider: Decider = {
case ex: Exception =>
probe ! ex
Stop
}
OneForOneStrategy()(defaultDecider)
}
override def supervisorStrategy: SupervisorStrategy = defaultStrategy
override def receive: Receive = {
case msg => unhandled(msg)
}
}
class Child(_case: String) extends Actor {
implicit val ec = context.dispatcher
override def preStart(): Unit = {
self ! _case
}
override def receive: Receive = {
case "case1" => throw new RuntimeException("fail")
case "case2" => Future[String] {
throw new RuntimeException("fail")
}.onComplete {
case Success(s) => println(s)
case Failure(e) =>
throw e
}
case msg => unhandled(msg)
}
}
class TestExample {
protected implicit var system: ActorSystem = _
@Before
def setup(): Unit = {
system = ActorSystem.create("test")
}
@After
def tearDown(): Unit = {
TestKit.shutdownActorSystem(system)
}
@Test
def case1(): Unit = {
val testProbe = TestProbe()
system.actorOf(Props(new Parent("case1", testProbe.ref)))
testProbe expectMsgClass classOf[RuntimeException]
}
@Test
def case2(): Unit = {
val testProbe = TestProbe()
system.actorOf(Props(new Parent("case2", testProbe.ref)))
testProbe expectMsgClass classOf[RuntimeException]
}
}
答案 0 :(得分:1)
这不是父母和孩子之间沟通的方式。 正确的方法是定义包含失败的消息(而不是发送异常!)。 然后父母可以适当地处理消息。
此外,构建您在父级中执行的子actor不是首选,因为这使得测试actor非常困难。相反,应将child-actor-factory函数作为参数传递给父actor。在测试Parent actor时,可以很容易地用虚拟actor(例如TestActorRef或TestProbe)替换它。类似地,可以单独测试子actor以将正确的消息返回给父级。
此外,不建议在演员中使用“未来”。 actor已经在异步运行,当时只处理1条消息。当您开始在actor中使用Future时,您必须处理在Future尚未完成时接收其他消息的情况,因为在Future完成之前,actor可能处于不正确的状态。在演员中使用Future的方法可能是使用book 'Effective Akka'(Extra Pattern,Cameo Pattern)中描述的临时演员。
从Akka开始,'Effective Akka' book是一个很好的阅读。它包含一些最佳实践和要避免的事项。这是一本如此快速阅读的小书。
根据评论进行更新:
在这种情况下,您有两个选择:
其他解决方案是处理未来的onComplete并将成功或失败消息发送给父(或对结果感兴趣的行为者)。我个人不会抛出异常。
我会在父级演员中传递一个子演员工厂,而子级(或工作者)演员要么传入想要响应的演员,要么从“发件人”中传递它。
请注意,您必须在调用Future之前捕获'sender'。并且,使用不同的线程池,否则Future将使用与actor本身相同的线程池。要重用这个池,这也是你想要传递给子actor的东西,然后你也可以调整它来进行测试。
我不明白你为什么要重启演员。无论如何,这似乎是一个无国籍的演员,只是一个适用于databaseApi的适配器。
对于子actor的实现,您可以考虑使用extra / cameo模式。然后你肯定它没有收到任何其他消息(不要忘记在演员完成时停止演员)。但是,通过使它成为一个单独的actor,你最终可以决定创建一个这些actor的池(使用路由器)来控制并发db操作的数量。
答案 1 :(得分:0)
要让测试通过,您可以将异常发送给actor并从onComplete
回调之外重新抛出异常:
override def receive: Receive = {
case "case1" => throw new RuntimeException("fail")
case "case2" =>
Future[String] {
throw new RuntimeException("fail")
}.onComplete {
case Success(s) => println(s)
case Failure(e) =>
self ! e
}
case e: RuntimeException => throw e
case msg => unhandled(msg)
}
但是,如果必须在actor中使用Future
(例如,方法返回Future
的第三方库),则有更好的方法来处理异常。例如,使用您在评论(databaseApi.load(): Future[Rows]
)中提到的数据库API,父作者可以向孩子发送LoadDb
消息,孩子可以发送Rows
或错误消息回到父母。孩子的行为如下所示:
def receive = {
case LoadDb =>
val s = sender // capture the sender
databaseApi
.load
.onComplete {
case Success(rows) =>
s ! rows
case Failure(e) =>
s ! DbFailure(e)
}
case ...
}
一个重要的注意事项是,当孩子收到sender
邮件时,我们会制作LoadDb
引用的本地副本,以便从onComplete
内部引用正确的发件人打回来。如果我们刚刚在回调中调用sender
,则可能会产生错误的结果,因为sender
在执行回调时可能已经发生变化,如here所述。 (与sender
不同,self
是不可变的,因此在self
内使用onComplete
是安全的。)