如何将来将错误传播给父actor

时间:2017-07-25 10:47:32

标签: scala akka

我尝试使用akkafutures了解故障处理。 例如,我有父母和儿童演员。

儿童演员有两个失败案例:
消息处理时发生情况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]
  }

}

2 个答案:

答案 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是一个很好的阅读。它包含一些最佳实践和要避免的事项。这是一本如此快速阅读的小书。

根据评论进行更新:
在这种情况下,您有两个选择:

  • 由于actor已经在运行async,你可以决定让它在actor中同步。这会使它更简单,但需要阻止。
  • 其他解决方案是处理未来的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是安全的。)