Merge pull request #19424 from drewhk/wip-19338-fwd-port-drewhk
str #19338: NPE in ActorGraphInterpreter (fwd port)
This commit is contained in:
commit
e92429dab9
2 changed files with 79 additions and 3 deletions
|
|
@ -3,11 +3,13 @@
|
||||||
*/
|
*/
|
||||||
package akka.stream.impl.fusing
|
package akka.stream.impl.fusing
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
import akka.stream._
|
import akka.stream._
|
||||||
import akka.stream.scaladsl._
|
import akka.stream.scaladsl._
|
||||||
import akka.stream.stage.{ GraphStage, GraphStageLogic, InHandler, OutHandler }
|
import akka.stream.stage.{GraphStage, GraphStageLogic, InHandler, OutHandler}
|
||||||
import akka.stream.testkit.AkkaSpec
|
|
||||||
import akka.stream.testkit.Utils._
|
import akka.stream.testkit.Utils._
|
||||||
|
import akka.stream.testkit.{AkkaSpec, TestPublisher, TestSubscriber}
|
||||||
import akka.testkit.EventFilter
|
import akka.testkit.EventFilter
|
||||||
|
|
||||||
import scala.concurrent.Await
|
import scala.concurrent.Await
|
||||||
|
|
@ -256,5 +258,79 @@ class ActorGraphInterpreterSpec extends AkkaSpec {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"be able to properly handle case where a stage fails before subscription happens" in assertAllStagesStopped {
|
||||||
|
|
||||||
|
// Fuzzing needs to be off, so that the failure can propagate to the output boundary before the ExposedPublisher
|
||||||
|
// message.
|
||||||
|
val noFuzzMat = ActorMaterializer(ActorMaterializerSettings(system).withFuzzing(false))
|
||||||
|
|
||||||
|
val te = TE("Test failure in preStart")
|
||||||
|
|
||||||
|
val evilLatch = new CountDownLatch(1)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is a somewhat tricky test setup. We need the following conditions to be met:
|
||||||
|
* - the stage should fail its output port before the ExposedPublisher message is processed
|
||||||
|
* - the enclosing actor (and therefore the stage) should be kept alive until a stray SubscribePending arrives
|
||||||
|
* that has been enqueued after ExposedPublisher message has been enqueued, but before it has been processed
|
||||||
|
*
|
||||||
|
* To achieve keeping alive the stage for long enough, we use an extra input and output port and instead
|
||||||
|
* of failing the stage, we fail only the output port under test.
|
||||||
|
*
|
||||||
|
* To delay the startup long enough, so both ExposedPublisher and SubscribePending are enqueued, we use an evil
|
||||||
|
* latch to delay the preStart() (which in turn delays the enclosing actor's preStart).
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
val failyStage = new GraphStage[FanOutShape2[Int, Int, Int]] {
|
||||||
|
override val shape: FanOutShape2[Int, Int, Int] = new FanOutShape2(
|
||||||
|
Inlet[Int]("test.in"),
|
||||||
|
Outlet[Int]("test.out0"),
|
||||||
|
Outlet[Int]("test.out1"))
|
||||||
|
|
||||||
|
override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) {
|
||||||
|
|
||||||
|
override def preStart(): Unit = {
|
||||||
|
pull(shape.in)
|
||||||
|
evilLatch.await()
|
||||||
|
fail(shape.out0, te)
|
||||||
|
}
|
||||||
|
|
||||||
|
setHandler(shape.out0, ignoreTerminateOutput) //We fail in preStart anyway
|
||||||
|
setHandler(shape.out1, ignoreTerminateOutput) //We fail in preStart anyway
|
||||||
|
passAlong(shape.in, shape.out1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val downstream0 = TestSubscriber.probe[Int]()
|
||||||
|
val downstream1 = TestSubscriber.probe[Int]()
|
||||||
|
|
||||||
|
val upstream = TestPublisher.probe[Int]()
|
||||||
|
|
||||||
|
RunnableGraph.fromGraph(GraphDSL.create() { implicit b ⇒
|
||||||
|
import GraphDSL.Implicits._
|
||||||
|
val faily = b.add(failyStage)
|
||||||
|
|
||||||
|
Source.fromPublisher(upstream) ~> faily.in
|
||||||
|
faily.out0 ~> Sink.fromSubscriber(downstream0)
|
||||||
|
faily.out1 ~> Sink.fromSubscriber(downstream1)
|
||||||
|
|
||||||
|
ClosedShape
|
||||||
|
}).run()(noFuzzMat)
|
||||||
|
|
||||||
|
evilLatch.countDown()
|
||||||
|
downstream0.expectSubscriptionAndError(te)
|
||||||
|
|
||||||
|
// If an NPE would happen due to unset exposedPublisher (see #19338) this would receive a failure instead
|
||||||
|
// of the actual element
|
||||||
|
downstream1.request(1)
|
||||||
|
upstream.sendNext(42)
|
||||||
|
downstream1.expectNext(42)
|
||||||
|
|
||||||
|
upstream.sendComplete()
|
||||||
|
downstream1.expectComplete()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -269,12 +269,12 @@ private[stream] object ActorGraphInterpreter {
|
||||||
}
|
}
|
||||||
|
|
||||||
def exposedPublisher(publisher: ActorPublisher[Any]): Unit = {
|
def exposedPublisher(publisher: ActorPublisher[Any]): Unit = {
|
||||||
|
exposedPublisher = publisher
|
||||||
upstreamFailed match {
|
upstreamFailed match {
|
||||||
case _: Some[_] ⇒
|
case _: Some[_] ⇒
|
||||||
publisher.shutdown(upstreamFailed)
|
publisher.shutdown(upstreamFailed)
|
||||||
case _ ⇒
|
case _ ⇒
|
||||||
if (upstreamCompleted) publisher.shutdown(None)
|
if (upstreamCompleted) publisher.shutdown(None)
|
||||||
else exposedPublisher = publisher
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue