阿卡:测试监控\死亡手表

Eug*_*Loy 5 testing scala akka akka-testkit akka-monitoring

在我的场景中,我有2个演员:

  1. watchee(我用TestProbe)
  2. watcher(Watcher包裹在我的测试中TestActorRef暴露一些内部stateI轨道)

守望者应该在watchee死亡时采取一些行动.

这是我到目前为止编写的完整测试用例:

class TempTest(_system: ActorSystem) extends TestKit(_system) with ImplicitSender with FunSuiteLike with Matchers with BeforeAndAfterAll {

  def this() = this(ActorSystem("TempTest"))

  override def afterAll {
    TestKit.shutdownActorSystem(system)
  }

  class WatcherActor(watchee: ActorRef) extends Actor {

    var state = "initial"
    context.watch(watchee)

    override def receive: Receive = {
      case "start" =>
        state = "start"
      case _: Terminated =>
        state = "terminated"
    }

  }

  test("example") {
    val watchee = TestProbe()
    val watcher = TestActorRef[WatcherActor](Props(new WatcherActor(watchee.ref)))

    assert(watcher.underlyingActor.state === "initial")

    watcher ! "start" // "start" will be sent and handled by watcher synchronously
    assert(watcher.underlyingActor.state === "start")

    system.stop(watchee.ref) // will cause Terminated to be sent and handled asynchronously by watcher
    Thread.sleep(100) // what is the best way to avoid blocking here?
    assert(watcher.underlyingActor.state === "terminated")
  }

}
Run Code Online (Sandbox Code Playgroud)

现在,因为所有涉及的actor都使用CallingThreadDispatcher(所有Akka的测试助手都使用props构造.withDispatcher(CallingThreadDispatcher.Id))我可以安全地假设当这个语句返回时:

watcher ! "start"
Run Code Online (Sandbox Code Playgroud)

..."开始"消息已经被处理WatchingActor,因此我可以根据该消息进行断言watcher.underlyingActor.state

但是,根据我的观察,当我停止watchee使用system.stop或发送Kill给它时,Terminated作为watchee死亡的副作用产生的消息在另一个线程中异步执行.

不,一个解决方案是停止watchee,块线程一段时间,验证Watcher之后的状态,但我想知道我该怎么做正确的方式(即如何确保杀死男主角后,它的观察者接收和处理 Terminated消息表明它死了)?

cmb*_*ter 5

解决此问题的一种方法是在您的测试中引入另一个观察者,同时观看watchee.这个其他观察者是一个TestProbe允许我们对它执行断言的东西,它将摆脱你所看到的时间问题.一,修改后的测试代码:

 val watchee = TestProbe()
 val watcher = TestActorRef[WatcherActor](Props(new WatcherActor(watchee.ref)))
 val probeWatcher = TestProbe()
 probeWatcher watch watchee.ref

 assert(watcher.underlyingActor.state === "initial")

 watcher ! "start" // "start" will be sent and handled by watcher synchronously
 assert(watcher.underlyingActor.state === "start")

 system.stop(watchee.ref) // will cause Terminated to be sent and handled asynchronously by watcher
 probeWatcher.expectTerminated(watchee.ref)
 assert(watcher.underlyingActor.state === "terminated")
Run Code Online (Sandbox Code Playgroud)

所以你可以看到我已经引入了额外的观察者:

val probeWatcher = TestProbe()
probeWatcher watch watchee.ref
Run Code Online (Sandbox Code Playgroud)

然后,在代码的后面,在你失败的最终断言之前,我使用另一个断言让我知道Terminated已停止的actor 的消息已经正确分发:

probeWatcher.expectTerminated(watchee.ref)

当代码移过这一行时,我可以确定watcher欠测试也收到了它终止的消息,并且跟随的断言将通过.

编辑

如OP所述,此代码存在一定程度的非确定性.另一种可能的解决方案是更改测试代码中停止actor的行:

watcher.underlyingActor.context.stop(watchee.ref)
Run Code Online (Sandbox Code Playgroud)

通过使用contextTestActorRef相信Terminated都将通过交付CallingThreadDispatcher,因此是完全同步的.我在一个循环中测试了这个,它对我来说超过1000次迭代.

现在我想,也许是因为我正在stop使用相同的演员,期待Terminated可能有一个优化来Terminated为该scanario 提供自我,所以我也测试了一个完全不同Actor的如下:

class FooActor extends Actor{
  def receive = {
    case _ =>
  }
Run Code Online (Sandbox Code Playgroud)

然后在测试代码中:

val foo = TestActorRef(new FooActor)
Run Code Online (Sandbox Code Playgroud)

并在停止:

foo.underlyingActor.context.stop(watchee.ref)
Run Code Online (Sandbox Code Playgroud)

这也按预期工作.


Die*_*oia 4

编辑:经过与 OP 的讨论和测试,我们发现发送 PoisonPill 作为终止所监视的 Actor 的方法可以实现所需的行为,因为 PPill 的终止是同步处理的,而 stop 或终止的终止是异步处理的。

虽然我们不确定原因,但我们最好的猜测是,这是因为杀死演员会引发异常,而 PPilling 则不会。

显然,这与按照我最初的建议使用 GracefulStop 模式无关,我将在下面报告该建议。

总之,OP问题的解决方案只是终止被监视的actor发送PPill而不是Kill消息或通过执行system.stop。


旧答案从这里开始:

我可能会提出一些不相关的建议,但我觉得它可能适用。

如果我理解正确的话,你想要做的基本上是同步终止演员,即只在演员正式死亡并且其死亡已被记录(在你的情况下由观察者记录)后才返回。

一般来说,死亡通知以及 akka 中的大多数其他通知都是异步的。尽管如此,使用 GracefulStop 模式 (akka.pattern.gracefulStop) 可以获得同步死亡确认。

为此,代码应类似于:

val timeout = 5.seconds
val killResultFuture = gracefulStop(victimRef, timeout, PoisonPill)
Await.result(killResultFuture, timeout)
Run Code Online (Sandbox Code Playgroud)

其作用是向受害者发送 PoisonPill(注意:您可以使用自定义消息),受害者将回复一个在受害者死亡后完成的 future。使用 Await.result 保证你是同步的。

不幸的是,这仅在以下情况下可用:a)您正在主动杀死受害者,并且您不想响应外部死亡原因 b)您可以在代码中使用超时和阻塞。但也许您可以根据您的情况调整此模式。