在actor中实现图形

时间:2016-07-05 04:12:02

标签: scala akka actor akka-stream

我正在尝试在actor中实现图形。如果满足以下任一条件,这似乎有效:

  1. 该图表不包含广播(使用alsoTo创建)或
  2. 每次实现使用相同的ActorMaterializer,或
  3. 该图表在Actor
  4. 之外实现

    我已将其缩减为以下测试用例:

    import java.util.concurrent.{CountDownLatch, TimeUnit}
    
    import akka.NotUsed
    import akka.actor.{Actor, ActorSystem}
    import akka.stream.ActorMaterializer
    import akka.stream.scaladsl.{RunnableGraph, Sink, Source}
    import akka.testkit.{TestActorRef, TestKit}
    
    import org.scalatest.{FlatSpecLike, Matchers}
    
    class ActorFlowTest extends TestKit(ActorSystem("ActorFlowTest")) with Matchers with FlatSpecLike {
    
      def createGraph(withBroadcast: Boolean) = {
        if (withBroadcast) Source.empty.alsoTo(Sink.ignore).to(Sink.ignore)
        else Source.empty.to(Sink.ignore)
      }
    
      case object Bomb
    
      class FlowActor(
        graph: RunnableGraph[NotUsed],
        latch: CountDownLatch,
        materializer: (ActorSystem) => ActorMaterializer
      ) extends Actor {
    
        override def preStart(): Unit = {
          graph.run()(materializer(context.system))
          latch.countDown()
        }
    
        override def receive: Receive = {
          case Bomb => throw new RuntimeException
        }
      }
    
      "Without an actor" should "be able to materialize twice" in {
        val graph = Source.empty.alsoTo(Sink.ignore).to(Sink.ignore)
        val materializer1 = ActorMaterializer()(system)
        val materializer2 = ActorMaterializer()(system)
        graph.run()(materializer1)
        graph.run()(materializer2) // Pass
      }
    
      "With a the same materializer" should "be able to materialize twice" in {
        val graph = createGraph(withBroadcast = true)
        val latch = new CountDownLatch(2)
        val materializer = ActorMaterializer()(system)
        val actorRef = TestActorRef(new FlowActor(graph, latch, _ => materializer))
        verify(actorRef, latch) should be(true) // Pass
      }
    
      "With a new materializer but no broadcast" should "be able to materialize twice" in {
        val graph = createGraph(withBroadcast = false)
        val latch = new CountDownLatch(2)
        def materializer(system: ActorSystem) = ActorMaterializer()(system)
        val actorRef = TestActorRef(new FlowActor(graph, latch, materializer))
        verify(actorRef, latch) should be(true) // Pass
      }
    
      "With a new materializer and a broadcast" should "be able to materialize twice" in {
        val graph = createGraph(withBroadcast = true)
        val latch = new CountDownLatch(2)
        def materializer(system: ActorSystem) = ActorMaterializer()(system)
        val actorRef = TestActorRef(new FlowActor(graph, latch, materializer))
        verify(actorRef, latch) should be(true) // Fail
      }
    
      def verify(actorRef: TestActorRef[_], latch: CountDownLatch): Boolean = {
        actorRef.start()
        actorRef ! Bomb
        latch.await(25, TimeUnit.SECONDS)
      }
    }
    

    似乎最后的情况总是超时,日志中出现以下错误:

    [ERROR] [07/05/2016 16:06:30.625] [ActorFlowTest-akka.actor.default-dispatcher-6] [akka://ActorFlowTest/user/$$c] Futures timed out after [20000 milliseconds]
    akka.actor.PostRestartException: akka://ActorFlowTest/user/$$c: exception post restart (class java.lang.RuntimeException)
        at akka.actor.dungeon.FaultHandling$$anonfun$6.apply(FaultHandling.scala:250)
        at akka.actor.dungeon.FaultHandling$$anonfun$6.apply(FaultHandling.scala:248)
        at akka.actor.dungeon.FaultHandling$$anonfun$handleNonFatalOrInterruptedException$1.applyOrElse(FaultHandling.scala:303)
        at akka.actor.dungeon.FaultHandling$$anonfun$handleNonFatalOrInterruptedException$1.applyOrElse(FaultHandling.scala:298)
        at scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:36)
        at akka.actor.dungeon.FaultHandling$class.finishRecreate(FaultHandling.scala:248)
        at akka.actor.dungeon.FaultHandling$class.faultRecreate(FaultHandling.scala:76)
        at akka.actor.ActorCell.faultRecreate(ActorCell.scala:374)
        at akka.actor.ActorCell.invokeAll$1(ActorCell.scala:464)
        at akka.actor.ActorCell.systemInvoke(ActorCell.scala:483)
        at akka.dispatch.Mailbox.processAllSystemMessages(Mailbox.scala:282)
        at akka.testkit.CallingThreadDispatcher.process$1(CallingThreadDispatcher.scala:243)
        at akka.testkit.CallingThreadDispatcher.runQueue(CallingThreadDispatcher.scala:283)
        at akka.testkit.CallingThreadDispatcher.systemDispatch(CallingThreadDispatcher.scala:191)
        at akka.actor.dungeon.Dispatch$class.restart(Dispatch.scala:119)
        at akka.actor.ActorCell.restart(ActorCell.scala:374)
        at akka.actor.LocalActorRef.restart(ActorRef.scala:406)
        at akka.actor.SupervisorStrategy.restartChild(FaultHandling.scala:365)
        at akka.actor.OneForOneStrategy.processFailure(FaultHandling.scala:518)
        at akka.actor.SupervisorStrategy.handleFailure(FaultHandling.scala:303)
        at akka.actor.dungeon.FaultHandling$class.handleFailure(FaultHandling.scala:263)
        at akka.actor.ActorCell.handleFailure(ActorCell.scala:374)
        at akka.actor.ActorCell.invokeAll$1(ActorCell.scala:459)
        at akka.actor.ActorCell.systemInvoke(ActorCell.scala:483)
        at akka.dispatch.Mailbox.processAllSystemMessages(Mailbox.scala:282)
        at akka.dispatch.Mailbox.run(Mailbox.scala:223)
        at akka.dispatch.Mailbox.exec(Mailbox.scala:234)
        at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
        at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
        at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
        at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)
    Caused by: java.util.concurrent.TimeoutException: Futures timed out after [20000 milliseconds]
        at scala.concurrent.impl.Promise$DefaultPromise.ready(Promise.scala:219)
        at scala.concurrent.impl.Promise$DefaultPromise.result(Promise.scala:223)
        at scala.concurrent.Await$$anonfun$result$1.apply(package.scala:190)
        at akka.dispatch.MonitorableThreadFactory$AkkaForkJoinWorkerThread$$anon$3.block(ThreadPoolBuilder.scala:167)
        at scala.concurrent.forkjoin.ForkJoinPool.managedBlock(ForkJoinPool.java:3640)
        at akka.dispatch.MonitorableThreadFactory$AkkaForkJoinWorkerThread.blockOn(ThreadPoolBuilder.scala:165)
        at scala.concurrent.Await$.result(package.scala:190)
        at akka.stream.impl.ActorMaterializerImpl.actorOf(ActorMaterializerImpl.scala:207)
        at akka.stream.impl.ActorMaterializerImpl$$anon$2.matGraph(ActorMaterializerImpl.scala:166)
        at akka.stream.impl.ActorMaterializerImpl$$anon$2.materializeAtomic(ActorMaterializerImpl.scala:150)
        at akka.stream.impl.MaterializerSession$$anonfun$materializeModule$1.apply(StreamLayout.scala:919)
        at akka.stream.impl.MaterializerSession$$anonfun$materializeModule$1.apply(StreamLayout.scala:915)
        at scala.collection.immutable.Set$Set1.foreach(Set.scala:94)
        at akka.stream.impl.MaterializerSession.materializeModule(StreamLayout.scala:915)
        at akka.stream.impl.MaterializerSession$$anonfun$materializeModule$1.apply(StreamLayout.scala:922)
        at akka.stream.impl.MaterializerSession$$anonfun$materializeModule$1.apply(StreamLayout.scala:915)
        at scala.collection.immutable.Set$Set4.foreach(Set.scala:200)
        at akka.stream.impl.MaterializerSession.materializeModule(StreamLayout.scala:915)
        at akka.stream.impl.MaterializerSession.materialize(StreamLayout.scala:882)
        at akka.stream.impl.ActorMaterializerImpl.materialize(ActorMaterializerImpl.scala:182)
        at akka.stream.impl.ActorMaterializerImpl.materialize(ActorMaterializerImpl.scala:80)
        at akka.stream.scaladsl.RunnableGraph.run(Flow.scala:351)
        at ActorFlowTest$FlowActor.preStart(ActorFlowTest.scala:40)
        at akka.actor.Actor$class.postRestart(Actor.scala:566)
        at ActorFlowTest$FlowActor.postRestart(ActorFlowTest.scala:33)
        at akka.actor.Actor$class.aroundPostRestart(Actor.scala:504)
        at ActorFlowTest$FlowActor.aroundPostRestart(ActorFlowTest.scala:33)
        at akka.actor.dungeon.FaultHandling$class.finishRecreate(FaultHandling.scala:239)
        ... 25 more
    

    我已尝试明确终止ActorMaterializers,但这不会重现问题。

    解决方法是围绕ActorMaterializer中的Props创建一个闭包,但如果这也来自另一个Actor,我担心我最终会遇到类似的问题。

    知道为什么会这样吗?显然它与ActorMaterializer有关,但有趣的是如何删除广播也解决了它(即使有更复杂的图形)。

2 个答案:

答案 0 :(得分:1)

这似乎与(或至少通过适当的)监督有关。我创建了一个额外的Supervisor - Actor,出于演示目的,只需在其FlowActor函数中启动单个preStart并将Bomb消息转发给它。以下测试成功执行,没有任何超时异常:

import java.util.concurrent.{CountDownLatch, TimeUnit}

import akka.NotUsed
import akka.actor.Actor.Receive
import akka.actor.SupervisorStrategy._
import akka.actor.{Actor, ActorRef, ActorSystem, OneForOneStrategy, Props}
import akka.stream.ActorMaterializer
import akka.stream.scaladsl.{RunnableGraph, Sink, Source}
import akka.testkit.{TestActorRef, TestKit}
import org.scalatest.{FlatSpecLike, Matchers}

import scala.concurrent.duration._

class ActorFlowTest extends TestKit(ActorSystem("TrikloSystem")) with Matchers with FlatSpecLike {

  def createGraph(withBroadcast: Boolean) = {
    if (withBroadcast) Source.empty.alsoTo(Sink.ignore).to(Sink.ignore)
    else Source.empty.to(Sink.ignore)
  }

  case object Bomb

  class Supervisor( graph: RunnableGraph[NotUsed],
                    latch: CountDownLatch,
                    materializer: (ActorSystem) => ActorMaterializer) extends Actor {

    var actorRef: Option[ActorRef] = None

    override def preStart(): Unit = {
      actorRef = Some(context.actorOf(Props( new FlowActor(graph, latch, materializer))))
    }

    override def receive: Receive = {
      case Bomb => actorRef.map( _ ! Bomb )
    }
  }

  class FlowActor(
                   graph: RunnableGraph[NotUsed],
                   latch: CountDownLatch,
                   materializer: (ActorSystem) => ActorMaterializer
                 ) extends Actor {

    override def preStart(): Unit = {
      graph.run()(materializer(context.system))
      latch.countDown()
    }

    override def receive: Receive = {
      case Bomb =>
        throw new RuntimeException
    }
  }

  "Without an actor" should "be able to materialize twice" in {
    val graph = Source.empty.alsoTo(Sink.ignore).to(Sink.ignore)
    val materializer1 = ActorMaterializer()(system)
    val materializer2 = ActorMaterializer()(system)
    graph.run()(materializer1)
    graph.run()(materializer2) // Pass
  }

  "With a the same materializer" should "be able to materialize twice" in {
    val graph = createGraph(withBroadcast = true)
    val latch = new CountDownLatch(2)
    val materializer = ActorMaterializer()(system)
    val actorRef = TestActorRef(new Supervisor(graph, latch, _ => materializer))
    verify(actorRef, latch) should be(true) // Pass
  }

  "With a new materializer but no broadcast" should "be able to materialize twice" in {
    val graph = createGraph(withBroadcast = false)
    val latch = new CountDownLatch(2)
    def materializer(system: ActorSystem) = ActorMaterializer()(system)
    val actorRef = TestActorRef(new Supervisor(graph, latch, materializer))
    verify(actorRef, latch) should be(true) // Pass
  }

  "With a new materializer and a broadcast" should "be able to materialize twice" in {
    val graph = createGraph(withBroadcast = true)
    val latch = new CountDownLatch(2)
    def materializer(system: ActorSystem) = ActorMaterializer()(system)
    val actorRef = TestActorRef(new Supervisor(graph, latch, materializer))
    verify(actorRef, latch) should be(true) // Fail
  }

  def verify(actorRef: TestActorRef[_], latch: CountDownLatch): Boolean = {
    actorRef.start()
    actorRef ! Bomb
    latch.await(25, TimeUnit.SECONDS)
  }
}

答案 1 :(得分:0)

在此测试中,Akka TestKit存在一些误用。

TestActorRef是一个非常特殊的测试构造,它将在调用线程(CallingThreadDispatcher)上执行,以便进行简单的同步单元测试。在同步测试中使用CountDownLatch很奇怪,因为任何操作都在同一个线程上,因此不需要进行线程间通信。

当您创建TestActorRef的实例时,它会在同一个调用中启动(您可以通过例如从构造函数或preStart抛出异常来查看它,并看到它最终出现在您的测试用例中)。

ActorRef上调用start绝对不是你应该做的事情,TestActorRef的特殊性质让你可以访问它,但你基本上是在一个空的shell actor上调用start,而不是actor你认为你正在与之互动(如果是那个演员,那么在它上面调用start()是不对的。)

一个合适的(但不是非常有用,因为无论上下文或物化器无法实现图形两次都没有问题)对你打算重复测试的测试将没有闩锁,看起来像这样:

class FlowActor(graph: RunnableGraph[NotUsed], materializer: (ActorSystem) => ActorMaterializer) extends Actor {
  override def preStart(): Unit = {
    graph.run()(materializer(context.system))
  }
  override def receive: Receive = Actor.emptyBehavior
}

"With a new materializer and a broadcast" should "be able to materialize twice" in {
  val graph = Source.empty.alsoTo(Sink.ignore).to(Sink.ignore)
  def materializer(system: ActorSystem) = ActorMaterializer()(system)
  val actorRef1 = TestActorRef(new FlowActor(graph, materializer))
  val actorRef2 = TestActorRef(new FlowActor(graph, materializer))
  // we'd get an exception here if it was not possible to materialize
  // since pre-start is run on the calling thread - the same thread
  // that is executing the test case
}

我只是让这个特定的怪异而不是深入挖掘TestActorRef中的魔力,它将是难以获得的见解,并且它们在许多情况下不适用于这个特定的情况。