529 lines
17 KiB
Scala
529 lines
17 KiB
Scala
/*
|
|
* Copyright (C) 2020-2021 Lightbend Inc. <https://www.lightbend.com>
|
|
*/
|
|
|
|
package akka.stream.scaladsl
|
|
|
|
import akka.NotUsed
|
|
import akka.stream.SubscriptionWithCancelException.NonFailureCancellation
|
|
import akka.stream.{ AbruptStageTerminationException, Attributes, Materializer, NeverMaterializedException }
|
|
import akka.stream.testkit.StreamSpec
|
|
import akka.stream.testkit.Utils.TE
|
|
import akka.stream.testkit.scaladsl.StreamTestKit.assertAllStagesStopped
|
|
|
|
import scala.concurrent.{ Future, Promise }
|
|
|
|
class FlowFutureFlowSpec extends StreamSpec {
|
|
def src10(i: Int = 0) = Source(i until (i + 10))
|
|
def src10WithFailure(i: Int = 0)(failOn: Int) = src10(i).map {
|
|
case `failOn` => throw TE(s"fail on $failOn")
|
|
case x => x
|
|
}
|
|
|
|
//this stage's behaviour in case of an 'early' downstream cancellation is governed by an attribute
|
|
//so we run all tests cases using both modes of the attributes.
|
|
//please notice most of the cases don't exhibit any difference in behaviour between the two modes
|
|
for {
|
|
(att, name) <- List(
|
|
(Attributes.NestedMaterializationCancellationPolicy.EagerCancellation, "EagerCancellation"),
|
|
(Attributes.NestedMaterializationCancellationPolicy.PropagateToNested, "PropagateToNested"))
|
|
delayDownstreamCancellation = att.propagateToNestedMaterialization
|
|
attributes = Attributes(att)
|
|
} {
|
|
|
|
s"a futureFlow with $name (delayDownstreamCancellation=$delayDownstreamCancellation)" must {
|
|
"work in the simple case with a completed future" in assertAllStagesStopped {
|
|
val (fNotUsed, fSeq) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow {
|
|
Future.successful(Flow[Int])
|
|
}
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.futureValue should be(NotUsed)
|
|
fSeq.futureValue should equal(0 until 10)
|
|
}
|
|
|
|
"work in the simple case with a late future" in assertAllStagesStopped {
|
|
val prFlow = Promise[Flow[Int, Int, NotUsed]]()
|
|
val (fNotUsed, fSeq) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow(prFlow.future)
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.value should be(empty)
|
|
fSeq.value should be(empty)
|
|
|
|
prFlow.success(Flow[Int])
|
|
|
|
fNotUsed.futureValue should be(NotUsed)
|
|
fSeq.futureValue should equal(0 until 10)
|
|
}
|
|
|
|
"fail properly when future is a completed failed future" in assertAllStagesStopped {
|
|
val (fNotUsed, fSeq) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow {
|
|
Future.failed[Flow[Int, Int, NotUsed]](TE("damn!"))
|
|
}
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.failed.futureValue should be(a[NeverMaterializedException])
|
|
fNotUsed.failed.futureValue.getCause should equal(TE("damn!"))
|
|
|
|
fSeq.failed.futureValue should equal(TE("damn!"))
|
|
|
|
}
|
|
|
|
"fail properly when future is late completed failed future" in assertAllStagesStopped {
|
|
val prFlow = Promise[Flow[Int, Int, NotUsed]]()
|
|
val (fNotUsed, fSeq) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow(prFlow.future)
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.value should be(empty)
|
|
fSeq.value should be(empty)
|
|
|
|
prFlow.failure(TE("damn!"))
|
|
|
|
fNotUsed.failed.futureValue should be(a[NeverMaterializedException])
|
|
fNotUsed.failed.futureValue.getCause should equal(TE("damn!"))
|
|
|
|
fSeq.failed.futureValue should equal(TE("damn!"))
|
|
|
|
}
|
|
|
|
"handle upstream failure when future is pre-completed" in assertAllStagesStopped {
|
|
val (fNotUsed, fSeq) = src10WithFailure()(5)
|
|
.viaMat {
|
|
Flow.futureFlow {
|
|
Future.successful {
|
|
Flow[Int].recover {
|
|
case TE("fail on 5") => 99
|
|
}
|
|
}
|
|
}
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.futureValue should be(NotUsed)
|
|
fSeq.futureValue should equal(List(0, 1, 2, 3, 4, 99))
|
|
}
|
|
|
|
"handle upstream failure when future is late-completed" in assertAllStagesStopped {
|
|
val prFlow = Promise[Flow[Int, Int, NotUsed]]()
|
|
val (fNotUsed, fSeq) = src10WithFailure()(5)
|
|
.viaMat {
|
|
Flow.futureFlow(prFlow.future)
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.value should be(empty)
|
|
fSeq.value should be(empty)
|
|
|
|
prFlow.success {
|
|
Flow[Int].recover {
|
|
case TE("fail on 5") => 99
|
|
}
|
|
}
|
|
|
|
fNotUsed.futureValue should be(NotUsed)
|
|
fSeq.futureValue should equal(List(0, 1, 2, 3, 4, 99))
|
|
}
|
|
|
|
"propagate upstream failure when future is pre-completed" in assertAllStagesStopped {
|
|
val (fNotUsed, fSeq) = src10WithFailure()(5)
|
|
.viaMat {
|
|
Flow.futureFlow {
|
|
Future.successful {
|
|
Flow[Int]
|
|
}
|
|
}
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.futureValue should be(NotUsed)
|
|
fSeq.failed.futureValue should equal(TE("fail on 5"))
|
|
}
|
|
|
|
"propagate upstream failure when future is late-completed" in assertAllStagesStopped {
|
|
val prFlow = Promise[Flow[Int, Int, NotUsed]]()
|
|
val (fNotUsed, fSeq) = src10WithFailure()(5)
|
|
.viaMat {
|
|
Flow.futureFlow(prFlow.future)
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.value should be(empty)
|
|
fSeq.value should be(empty)
|
|
|
|
prFlow.success {
|
|
Flow[Int]
|
|
}
|
|
|
|
fNotUsed.futureValue should be(NotUsed)
|
|
fSeq.failed.futureValue should equal(TE("fail on 5"))
|
|
}
|
|
|
|
"handle early upstream error when flow future is pre-completed" in assertAllStagesStopped {
|
|
val (fNotUsed, fSeq) = Source
|
|
.failed(TE("not today my friend"))
|
|
.viaMat {
|
|
Flow.futureFlow {
|
|
Future.successful {
|
|
Flow[Int]
|
|
.recover {
|
|
case TE("not today my friend") => 99
|
|
}
|
|
.concat(src10())
|
|
}
|
|
}
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.futureValue should be(NotUsed)
|
|
fSeq.futureValue should equal(99 +: (0 until 10))
|
|
|
|
}
|
|
|
|
"handle early upstream error when flow future is late-completed" in assertAllStagesStopped {
|
|
val prFlow = Promise[Flow[Int, Int, NotUsed]]()
|
|
val (fNotUsed, fSeq) = Source
|
|
.failed(TE("not today my friend"))
|
|
.viaMat {
|
|
Flow.futureFlow(prFlow.future)
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.value should be(empty)
|
|
fSeq.value should be(empty)
|
|
|
|
prFlow.success {
|
|
Flow[Int]
|
|
.recover {
|
|
case TE("not today my friend") => 99
|
|
}
|
|
.concat(src10())
|
|
}
|
|
|
|
fNotUsed.futureValue should be(NotUsed)
|
|
fSeq.futureValue should equal(99 +: (0 until 10))
|
|
|
|
}
|
|
|
|
"handle closed downstream when flow future is pre completed" in assertAllStagesStopped {
|
|
val (fSeq1, fSeq2) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow {
|
|
Future.successful {
|
|
Flow[Int].alsoToMat(Sink.seq)(Keep.right)
|
|
}
|
|
}
|
|
}(Keep.right)
|
|
.mapMaterializedValue(_.flatten)
|
|
.take(0)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fSeq1.futureValue should be(empty)
|
|
fSeq2.futureValue should be(empty)
|
|
|
|
}
|
|
|
|
"handle closed downstream when flow future is completed after downstream cancel" in assertAllStagesStopped {
|
|
val prFlow = Promise[Flow[Int, Int, Future[collection.immutable.Seq[Int]]]]()
|
|
val (fNestedFlowMatVal, fSinkCompletion) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow(prFlow.future)
|
|
}(Keep.right)
|
|
.mapMaterializedValue(_.flatten)
|
|
.take(0) // cancel asap
|
|
.toMat(Sink.ignore)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fSinkCompletion.futureValue // downstream has completed/cancelled
|
|
if (delayDownstreamCancellation) {
|
|
fNestedFlowMatVal.value should be(empty)
|
|
|
|
prFlow.success {
|
|
Flow[Int].alsoToMat(Sink.seq)(Keep.right)
|
|
}
|
|
|
|
// was materialized but cancelled
|
|
fNestedFlowMatVal.futureValue should be(empty)
|
|
} else {
|
|
// was never materialized
|
|
fNestedFlowMatVal.failed.futureValue should be(a[NeverMaterializedException])
|
|
fNestedFlowMatVal.failed.futureValue.getCause should be(a[NonFailureCancellation])
|
|
}
|
|
}
|
|
|
|
"handle early downstream failure when flow future is pre-completed" in assertAllStagesStopped {
|
|
val (fSeq1, fSeq2) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow {
|
|
Future.successful {
|
|
Flow[Int].alsoToMat(Sink.seq)(Keep.right)
|
|
}
|
|
}
|
|
}(Keep.right)
|
|
.mapMaterializedValue(_.flatten)
|
|
.prepend(Source.failed(TE("damn!")))
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fSeq1.failed.futureValue should equal(TE("damn!"))
|
|
fSeq2.failed.futureValue should equal(TE("damn!"))
|
|
}
|
|
|
|
"handle early downstream failure when flow future is late completed" in assertAllStagesStopped {
|
|
val prFlow = Promise[Flow[Int, Int, Future[collection.immutable.Seq[Int]]]]()
|
|
val (fSeq1, fSeq2) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow(prFlow.future)
|
|
}(Keep.right)
|
|
.mapMaterializedValue(_.flatten)
|
|
.prepend(Source.failed(TE("damn!")))
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
if (delayDownstreamCancellation) {
|
|
fSeq2.failed.futureValue should equal(TE("damn!"))
|
|
fSeq1.value should be(empty)
|
|
|
|
prFlow.success {
|
|
Flow[Int].alsoToMat(Sink.seq)(Keep.right)
|
|
}
|
|
|
|
fSeq1.failed.futureValue should equal(TE("damn!"))
|
|
} else {
|
|
fSeq1.failed.futureValue should be(a[NeverMaterializedException])
|
|
fSeq1.failed.futureValue.getCause should equal(TE("damn!"))
|
|
fSeq2.failed.futureValue should equal(TE("damn!"))
|
|
}
|
|
}
|
|
|
|
"handle early upstream completion when flow future is pre-completed" in assertAllStagesStopped {
|
|
val (fNotUsed, fSeq) = Source
|
|
.empty[Int]
|
|
.viaMat {
|
|
Flow.futureFlow {
|
|
Future.successful {
|
|
Flow[Int].orElse(Source.single(99))
|
|
}
|
|
}
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.futureValue should be(NotUsed)
|
|
fSeq.futureValue should equal(99 :: Nil)
|
|
}
|
|
|
|
"handle early upstream completion when flow future is late-completed" in assertAllStagesStopped {
|
|
val prFlow = Promise[Flow[Int, Int, NotUsed]]()
|
|
val (fNotUsed, fSeq) = Source
|
|
.empty[Int]
|
|
.viaMat {
|
|
Flow.futureFlow(prFlow.future)
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.value should be(empty)
|
|
fSeq.value should be(empty)
|
|
|
|
prFlow.success {
|
|
Flow[Int].orElse(Source.single(99))
|
|
}
|
|
|
|
fNotUsed.futureValue should be(NotUsed)
|
|
fSeq.futureValue should equal(99 :: Nil)
|
|
}
|
|
|
|
"fails properly on materialization failure with a completed future" in assertAllStagesStopped {
|
|
val (fNotUsed, fSeq) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow {
|
|
Future.successful(Flow[Int].mapMaterializedValue[NotUsed](_ => throw TE("BBOM!")))
|
|
}
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.failed.futureValue should be(a[NeverMaterializedException])
|
|
fNotUsed.failed.futureValue.getCause should equal(TE("BBOM!"))
|
|
fSeq.failed.futureValue should equal(TE("BBOM!"))
|
|
}
|
|
|
|
"fails properly on materialization failure with a late future" in assertAllStagesStopped {
|
|
val prFlow = Promise[Flow[Int, Int, NotUsed]]()
|
|
val (fNotUsed, fSeq) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow(prFlow.future)
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.value should be(empty)
|
|
fSeq.value should be(empty)
|
|
|
|
prFlow.success(Flow[Int].mapMaterializedValue[NotUsed](_ => throw TE("BBOM!")))
|
|
|
|
fNotUsed.failed.futureValue should be(a[NeverMaterializedException])
|
|
fNotUsed.failed.futureValue.getCause should equal(TE("BBOM!"))
|
|
fSeq.failed.futureValue should equal(TE("BBOM!"))
|
|
}
|
|
|
|
"propagate flow failures with a completed future" in assertAllStagesStopped {
|
|
val (fNotUsed, fSeq) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow {
|
|
Future.successful {
|
|
Flow[Int].map {
|
|
case 5 => throw TE("fail on 5")
|
|
case x => x
|
|
}
|
|
}
|
|
}
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.futureValue should be(NotUsed)
|
|
fSeq.failed.futureValue should equal(TE("fail on 5"))
|
|
}
|
|
|
|
"propagate flow failures with a late future" in assertAllStagesStopped {
|
|
val prFlow = Promise[Flow[Int, Int, NotUsed]]()
|
|
val (fNotUsed, fSeq) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow(prFlow.future)
|
|
}(Keep.right)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fNotUsed.value should be(empty)
|
|
fSeq.value should be(empty)
|
|
|
|
prFlow.success {
|
|
Flow[Int].map {
|
|
case 5 => throw TE("fail on 5")
|
|
case x => x
|
|
}
|
|
}
|
|
|
|
fNotUsed.futureValue should be(NotUsed)
|
|
fSeq.failed.futureValue should equal(TE("fail on 5"))
|
|
}
|
|
|
|
"allow flow to handle downstream completion with a completed future" in assertAllStagesStopped {
|
|
val (fSeq1, fSeq2) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow {
|
|
Future.successful {
|
|
Flow.fromSinkAndSourceMat(Sink.seq[Int], src10(10))(Keep.left)
|
|
}
|
|
}
|
|
}(Keep.right)
|
|
.take(5)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fSeq1.flatten.futureValue should be(0 until 10)
|
|
fSeq2.futureValue should equal(10 until 15)
|
|
}
|
|
|
|
"allow flow to handle downstream completion with a late future" in assertAllStagesStopped {
|
|
val pr = Promise[Flow[Int, Int, Future[Seq[Int]]]]()
|
|
val (fSeq1, fSeq2) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow(pr.future)
|
|
}(Keep.right)
|
|
.take(5)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()
|
|
|
|
fSeq1.value should be(empty)
|
|
fSeq2.value should be(empty)
|
|
|
|
pr.success {
|
|
Flow.fromSinkAndSourceMat(Sink.seq[Int], src10(10))(Keep.left)
|
|
}
|
|
|
|
fSeq1.flatten.futureValue should be(0 until 10)
|
|
fSeq2.futureValue should equal(10 until 15)
|
|
}
|
|
|
|
"abrupt termination before future completion" in assertAllStagesStopped {
|
|
val mat = Materializer(system)
|
|
val prFlow = Promise[Flow[Int, Int, Future[collection.immutable.Seq[Int]]]]()
|
|
val (fSeq1, fSeq2) = src10()
|
|
.viaMat {
|
|
Flow.futureFlow(prFlow.future)
|
|
}(Keep.right)
|
|
.take(5)
|
|
.toMat(Sink.seq)(Keep.both)
|
|
.withAttributes(attributes)
|
|
.run()(mat)
|
|
|
|
fSeq1.value should be(empty)
|
|
fSeq2.value should be(empty)
|
|
|
|
mat.shutdown()
|
|
|
|
fSeq1.failed.futureValue should be(a[AbruptStageTerminationException])
|
|
fSeq2.failed.futureValue should be(a[AbruptStageTerminationException])
|
|
}
|
|
}
|
|
}
|
|
|
|
"NestedMaterializationCancellationPolicy" must {
|
|
"default to false" in assertAllStagesStopped {
|
|
val fl = Flow.fromMaterializer {
|
|
case (_, attributes) =>
|
|
val att = attributes.mandatoryAttribute[Attributes.NestedMaterializationCancellationPolicy]
|
|
att.propagateToNestedMaterialization should be(false)
|
|
Flow[Any]
|
|
}
|
|
Source.empty.via(fl).runWith(Sink.headOption).futureValue should be(empty)
|
|
}
|
|
}
|
|
}
|