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

时间:2014-10-17 21:10:58

标签: scala testing akka akka-testkit akka-monitoring

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

  1. watchee(我使用TestProbe
  2. watcherWatcher包含在TestActorRef中,以便在我的测试中公开一些内部state跟踪)
  3. 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")
      }
    
    }
    

    现在,由于所有参与的演员使用CallingThreadDispatcher(所有Akka的测试助手都使用带有.withDispatcher(CallingThreadDispatcher.Id)的道具构建)我可以安全地假设当此语句返回时:

    watcher ! "start"
    

    ..."开始"消息已由WatchingActor处理,因此我可以根据watcher.underlyingActor.state

    进行断言

    但是,根据我的观察,当我使用watchee停止system.stop或向其发送Kill时,Terminated消息产生{{1}的副作用死亡在另一个线程中以异步方式执行。

    非解决方案是停止watchee,阻止线程一段时间并在此之后验证watchee状态,但我想知道如何以正确的方式执行此操作(即如何确定杀死演员之后它的观察者收到并处理 Watcher消息表明它的死亡)?

2 个答案:

答案 0 :(得分: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")

所以你可以看到我已经引入了附加观察者的行:

val probeWatcher = TestProbe()
probeWatcher watch watchee.ref

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

probeWatcher.expectTerminated(watchee.ref)

当代码移过这一行时,我可以确定被测试的watcher也已收到其终止的消息,并且接下来的断言将通过。

修改

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

watcher.underlyingActor.context.stop(watchee.ref)

使用context的{​​{1}}我相信TestActorRef将通过Terminated全部传递,因此完全同步。我在一个循环中测试了这个,它对我来说超过1000次迭代。

现在我想这可能是因为我正在使用期望CallingThreadDispatcher的同一个演员执行stop,可能有优化将Terminated传递给我自己的那个,所以我也用完全不同的Terminated进行了测试,如下所示:

Actor

然后在测试代码中:

class FooActor extends Actor{
  def receive = {
    case _ =>
  }

停止时:

val foo = TestActorRef(new FooActor)

这也符合预期。

答案 1 :(得分:3)

编辑: 在使用OP进行讨论和测试之后,我们发现将PoisonPill作为终止受监视的actor的手段可以实现所需的行为,因为从PPill中终止同步处理,而来自stop或kill的那些是异步处理的。

虽然我们不确定原因,但我们最好的选择是,这是因为杀死一名演员引发了异常,而PP则没有。

显然,这与根据我的原始建议使用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)

这样做是将PoisonPill发送给受害者(注意:您可以使用自定义消息),他将回复受害者死亡后完成的未来。使用Await.result可以保证同步。

不幸的是,这只有在以下情况下才可用:a)您正在积极地杀死受害者,而您却不想回应外部原因b)您可以在代码中使用超时和阻止。但也许你可以根据自己的情况调整这种模式。