!str,htc replace and remove OneBoundedInterpreter
main work by @drewhk with contributions from @2m and @rkuhn This work uncovered many well-hidden bugs in existing Stages, in particular StatefulStage. These were hidden by the behavior of OneBoundedInterpreter that normally behaves more orderly than it guarantees in general, especially with respect to the timeliness of delivery of upstream termination signals; the bugs were then that internal state was not flushed when onComplete arrived “too early”.
This commit is contained in:
parent
20f54435f1
commit
556012b7ee
107 changed files with 2456 additions and 3061 deletions
|
|
@ -4,17 +4,24 @@
|
|||
package akka.stream.impl.fusing
|
||||
|
||||
import akka.stream.stage._
|
||||
import akka.stream.testkit.AkkaSpec
|
||||
import akka.testkit.EventFilter
|
||||
|
||||
import scala.util.control.NoStackTrace
|
||||
import akka.stream.Supervision
|
||||
|
||||
class InterpreterSpec extends InterpreterSpecKit {
|
||||
class InterpreterSpec extends AkkaSpec with GraphInterpreterSpecKit {
|
||||
import Supervision.stoppingDecider
|
||||
|
||||
/*
|
||||
* These tests were writtern for the previous veryion of the interpreter, the so called OneBoundedInterpreter.
|
||||
* These stages are now properly emulated by the GraphInterpreter and many of the edge cases were relevant to
|
||||
* the execution model of the old one. Still, these tests are very valuable, so please do not remove.
|
||||
*/
|
||||
|
||||
"Interpreter" must {
|
||||
|
||||
"implement map correctly" in new TestSetup(Seq(Map((x: Int) ⇒ x + 1, stoppingDecider))) {
|
||||
"implement map correctly" in new OneBoundedSetup[Int](Seq(Map((x: Int) ⇒ x + 1, stoppingDecider))) {
|
||||
lastEvents() should be(Set.empty)
|
||||
|
||||
downstream.requestOne()
|
||||
|
|
@ -33,7 +40,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(OnComplete))
|
||||
}
|
||||
|
||||
"implement chain of maps correctly" in new TestSetup(Seq(
|
||||
"implement chain of maps correctly" in new OneBoundedSetup[Int](Seq(
|
||||
Map((x: Int) ⇒ x + 1, stoppingDecider),
|
||||
Map((x: Int) ⇒ x * 2, stoppingDecider),
|
||||
Map((x: Int) ⇒ x + 1, stoppingDecider))) {
|
||||
|
|
@ -56,7 +63,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(Cancel))
|
||||
}
|
||||
|
||||
"work with only boundary ops" in new TestSetup(Seq.empty) {
|
||||
"work with only boundary ops" in new OneBoundedSetup[Int](Seq.empty) {
|
||||
lastEvents() should be(Set.empty)
|
||||
|
||||
downstream.requestOne()
|
||||
|
|
@ -69,7 +76,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(OnComplete))
|
||||
}
|
||||
|
||||
"implement one-to-many many-to-one chain correctly" in new TestSetup(Seq(
|
||||
"implement one-to-many many-to-one chain correctly" in new OneBoundedSetup[Int](Seq(
|
||||
Doubler(),
|
||||
Filter((x: Int) ⇒ x != 0, stoppingDecider))) {
|
||||
|
||||
|
|
@ -94,7 +101,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(OnComplete))
|
||||
}
|
||||
|
||||
"implement many-to-one one-to-many chain correctly" in new TestSetup(Seq(
|
||||
"implement many-to-one one-to-many chain correctly" in new OneBoundedSetup[Int](Seq(
|
||||
Filter((x: Int) ⇒ x != 0, stoppingDecider),
|
||||
Doubler())) {
|
||||
|
||||
|
|
@ -119,7 +126,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(Cancel))
|
||||
}
|
||||
|
||||
"implement take" in new TestSetup(Seq(Take(2))) {
|
||||
"implement take" in new OneBoundedSetup[Int](Seq(Take(2))) {
|
||||
|
||||
lastEvents() should be(Set.empty)
|
||||
|
||||
|
|
@ -136,7 +143,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(OnNext(1), Cancel, OnComplete))
|
||||
}
|
||||
|
||||
"implement take inside a chain" in new TestSetup(Seq(
|
||||
"implement take inside a chain" in new OneBoundedSetup[Int](Seq(
|
||||
Filter((x: Int) ⇒ x != 0, stoppingDecider),
|
||||
Take(2),
|
||||
Map((x: Int) ⇒ x + 1, stoppingDecider))) {
|
||||
|
|
@ -159,7 +166,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(Cancel, OnComplete, OnNext(3)))
|
||||
}
|
||||
|
||||
"implement fold" in new TestSetup(Seq(Fold(0, (agg: Int, x: Int) ⇒ agg + x, stoppingDecider))) {
|
||||
"implement fold" in new OneBoundedSetup[Int](Seq(Fold(0, (agg: Int, x: Int) ⇒ agg + x, stoppingDecider))) {
|
||||
lastEvents() should be(Set.empty)
|
||||
|
||||
downstream.requestOne()
|
||||
|
|
@ -178,7 +185,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(OnNext(3), OnComplete))
|
||||
}
|
||||
|
||||
"implement fold with proper cancel" in new TestSetup(Seq(Fold(0, (agg: Int, x: Int) ⇒ agg + x, stoppingDecider))) {
|
||||
"implement fold with proper cancel" in new OneBoundedSetup[Int](Seq(Fold(0, (agg: Int, x: Int) ⇒ agg + x, stoppingDecider))) {
|
||||
|
||||
lastEvents() should be(Set.empty)
|
||||
|
||||
|
|
@ -198,7 +205,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(Cancel))
|
||||
}
|
||||
|
||||
"work if fold completes while not in a push position" in new TestSetup(Seq(Fold(0, (agg: Int, x: Int) ⇒ agg + x, stoppingDecider))) {
|
||||
"work if fold completes while not in a push position" in new OneBoundedSetup[Int](Seq(Fold(0, (agg: Int, x: Int) ⇒ agg + x, stoppingDecider))) {
|
||||
|
||||
lastEvents() should be(Set.empty)
|
||||
|
||||
|
|
@ -209,7 +216,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(OnComplete, OnNext(0)))
|
||||
}
|
||||
|
||||
"implement grouped" in new TestSetup(Seq(Grouped(3))) {
|
||||
"implement grouped" in new OneBoundedSetup[Int](Seq(Grouped(3))) {
|
||||
lastEvents() should be(Set.empty)
|
||||
|
||||
downstream.requestOne()
|
||||
|
|
@ -234,7 +241,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(OnNext(Vector(3)), OnComplete))
|
||||
}
|
||||
|
||||
"implement conflate" in new TestSetup(Seq(Conflate(
|
||||
"implement conflate" in new OneBoundedSetup[Int](Seq(Conflate(
|
||||
(in: Int) ⇒ in,
|
||||
(agg: Int, x: Int) ⇒ agg + x,
|
||||
stoppingDecider))) {
|
||||
|
|
@ -266,7 +273,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(Cancel))
|
||||
}
|
||||
|
||||
"implement expand" in new TestSetup(Seq(Expand(
|
||||
"implement expand" in new OneBoundedSetup[Int](Seq(Expand(
|
||||
(in: Int) ⇒ in,
|
||||
(agg: Int) ⇒ (agg, agg)))) {
|
||||
|
||||
|
|
@ -294,7 +301,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(OnComplete))
|
||||
}
|
||||
|
||||
"work with conflate-conflate" in new TestSetup(Seq(
|
||||
"work with conflate-conflate" in new OneBoundedSetup[Int](Seq(
|
||||
Conflate(
|
||||
(in: Int) ⇒ in,
|
||||
(agg: Int, x: Int) ⇒ agg + x,
|
||||
|
|
@ -332,7 +339,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
|
||||
}
|
||||
|
||||
"work with expand-expand" in new TestSetup(Seq(
|
||||
"work with expand-expand" in new OneBoundedSetup[Int](Seq(
|
||||
Expand(
|
||||
(in: Int) ⇒ in,
|
||||
(agg: Int) ⇒ (agg, agg + 1)),
|
||||
|
|
@ -369,7 +376,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(OnComplete, OnNext(12)))
|
||||
}
|
||||
|
||||
"implement conflate-expand" in new TestSetup(Seq(
|
||||
"implement conflate-expand" in new OneBoundedSetup[Int](Seq(
|
||||
Conflate(
|
||||
(in: Int) ⇒ in,
|
||||
(agg: Int, x: Int) ⇒ agg + x,
|
||||
|
|
@ -405,12 +412,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
lastEvents() should be(Set(Cancel))
|
||||
}
|
||||
|
||||
"implement expand-conflate" in {
|
||||
pending
|
||||
// Needs to detect divergent loops
|
||||
}
|
||||
|
||||
"implement doubler-conflate" in new TestSetup(Seq(
|
||||
"implement doubler-conflate" in new OneBoundedSetup[Int](Seq(
|
||||
Doubler(),
|
||||
Conflate(
|
||||
(in: Int) ⇒ in,
|
||||
|
|
@ -429,7 +431,8 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
|
||||
}
|
||||
|
||||
"work with jumpback table and completed elements" in new TestSetup(Seq(
|
||||
// Note, the new interpreter has no jumpback table, still did not want to remove the test
|
||||
"work with jumpback table and completed elements" in new OneBoundedSetup[Int](Seq(
|
||||
Map((x: Int) ⇒ x, stoppingDecider),
|
||||
Map((x: Int) ⇒ x, stoppingDecider),
|
||||
KeepGoing(),
|
||||
|
|
@ -461,7 +464,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
|
||||
}
|
||||
|
||||
"work with pushAndFinish if upstream completes with pushAndFinish" in new TestSetup(Seq(
|
||||
"work with pushAndFinish if upstream completes with pushAndFinish" in new OneBoundedSetup[Int](Seq(
|
||||
new PushFinishStage)) {
|
||||
|
||||
lastEvents() should be(Set.empty)
|
||||
|
|
@ -469,11 +472,11 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
downstream.requestOne()
|
||||
lastEvents() should be(Set(RequestOne))
|
||||
|
||||
upstream.onNextAndComplete("foo")
|
||||
lastEvents() should be(Set(OnNext("foo"), OnComplete))
|
||||
upstream.onNextAndComplete(0)
|
||||
lastEvents() should be(Set(OnNext(0), OnComplete))
|
||||
}
|
||||
|
||||
"work with pushAndFinish if indirect upstream completes with pushAndFinish" in new TestSetup(Seq(
|
||||
"work with pushAndFinish if indirect upstream completes with pushAndFinish" in new OneBoundedSetup[Int](Seq(
|
||||
Map((x: Any) ⇒ x, stoppingDecider),
|
||||
new PushFinishStage,
|
||||
Map((x: Any) ⇒ x, stoppingDecider))) {
|
||||
|
|
@ -483,24 +486,24 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
downstream.requestOne()
|
||||
lastEvents() should be(Set(RequestOne))
|
||||
|
||||
upstream.onNextAndComplete("foo")
|
||||
lastEvents() should be(Set(OnNext("foo"), OnComplete))
|
||||
upstream.onNextAndComplete(1)
|
||||
lastEvents() should be(Set(OnNext(1), OnComplete))
|
||||
}
|
||||
|
||||
"work with pushAndFinish if upstream completes with pushAndFinish and downstream immediately pulls" in new TestSetup(Seq(
|
||||
"work with pushAndFinish if upstream completes with pushAndFinish and downstream immediately pulls" in new OneBoundedSetup[Int](Seq(
|
||||
new PushFinishStage,
|
||||
Fold("", (x: String, y: String) ⇒ x + y, stoppingDecider))) {
|
||||
Fold(0, (x: Int, y: Int) ⇒ x + y, stoppingDecider))) {
|
||||
|
||||
lastEvents() should be(Set.empty)
|
||||
|
||||
downstream.requestOne()
|
||||
lastEvents() should be(Set(RequestOne))
|
||||
|
||||
upstream.onNextAndComplete("foo")
|
||||
lastEvents() should be(Set(OnNext("foo"), OnComplete))
|
||||
upstream.onNextAndComplete(1)
|
||||
lastEvents() should be(Set(OnNext(1), OnComplete))
|
||||
}
|
||||
|
||||
"report error if pull is called while op is terminating" in new TestSetup(Seq(new PushPullStage[Any, Any] {
|
||||
"report error if pull is called while op is terminating" in new OneBoundedSetup[Int](Seq(new PushPullStage[Any, Any] {
|
||||
override def onPull(ctx: Context[Any]): SyncDirective = ctx.pull()
|
||||
override def onPush(elem: Any, ctx: Context[Any]): SyncDirective = ctx.pull()
|
||||
override def onUpstreamFinish(ctx: Context[Any]): TerminationDirective = ctx.absorbTermination()
|
||||
|
|
@ -514,12 +517,12 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
val ev = lastEvents()
|
||||
ev.nonEmpty should be(true)
|
||||
ev.forall {
|
||||
case OnError(_: IllegalStateException) ⇒ true
|
||||
case _ ⇒ false
|
||||
case OnError(_: IllegalArgumentException) ⇒ true
|
||||
case _ ⇒ false
|
||||
} should be(true)
|
||||
}
|
||||
|
||||
"implement take-take" in new TestSetup(Seq(
|
||||
"implement take-take" in new OneBoundedSetup[Int](Seq(
|
||||
Take(1),
|
||||
Take(1))) {
|
||||
lastEvents() should be(Set.empty)
|
||||
|
|
@ -527,12 +530,12 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
downstream.requestOne()
|
||||
lastEvents() should be(Set(RequestOne))
|
||||
|
||||
upstream.onNext("foo")
|
||||
lastEvents() should be(Set(OnNext("foo"), OnComplete, Cancel))
|
||||
upstream.onNext(1)
|
||||
lastEvents() should be(Set(OnNext(1), OnComplete, Cancel))
|
||||
|
||||
}
|
||||
|
||||
"implement take-take with pushAndFinish from upstream" in new TestSetup(Seq(
|
||||
"implement take-take with pushAndFinish from upstream" in new OneBoundedSetup[Int](Seq(
|
||||
Take(1),
|
||||
Take(1))) {
|
||||
lastEvents() should be(Set.empty)
|
||||
|
|
@ -540,8 +543,8 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
downstream.requestOne()
|
||||
lastEvents() should be(Set(RequestOne))
|
||||
|
||||
upstream.onNextAndComplete("foo")
|
||||
lastEvents() should be(Set(OnNext("foo"), OnComplete))
|
||||
upstream.onNextAndComplete(1)
|
||||
lastEvents() should be(Set(OnNext(1), OnComplete))
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -551,7 +554,7 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
override def onDownstreamFinish(ctx: Context[Int]): TerminationDirective = ctx.absorbTermination()
|
||||
}
|
||||
|
||||
"not allow absorbTermination from onDownstreamFinish()" in new TestSetup(Seq(
|
||||
"not allow absorbTermination from onDownstreamFinish()" in new OneBoundedSetup[Int](Seq(
|
||||
new InvalidAbsorbTermination)) {
|
||||
lastEvents() should be(Set.empty)
|
||||
|
||||
|
|
@ -564,4 +567,51 @@ class InterpreterSpec extends InterpreterSpecKit {
|
|||
|
||||
}
|
||||
|
||||
private[akka] case class Doubler[T]() extends PushPullStage[T, T] {
|
||||
var oneMore: Boolean = false
|
||||
var lastElem: T = _
|
||||
|
||||
override def onPush(elem: T, ctx: Context[T]): SyncDirective = {
|
||||
lastElem = elem
|
||||
oneMore = true
|
||||
ctx.push(elem)
|
||||
}
|
||||
|
||||
override def onPull(ctx: Context[T]): SyncDirective = {
|
||||
if (oneMore) {
|
||||
oneMore = false
|
||||
ctx.push(lastElem)
|
||||
} else ctx.pull()
|
||||
}
|
||||
}
|
||||
|
||||
private[akka] case class KeepGoing[T]() extends PushPullStage[T, T] {
|
||||
var lastElem: T = _
|
||||
|
||||
override def onPush(elem: T, ctx: Context[T]): SyncDirective = {
|
||||
lastElem = elem
|
||||
ctx.push(elem)
|
||||
}
|
||||
|
||||
override def onPull(ctx: Context[T]): SyncDirective = {
|
||||
if (ctx.isFinishing) {
|
||||
ctx.push(lastElem)
|
||||
} else ctx.pull()
|
||||
}
|
||||
|
||||
override def onUpstreamFinish(ctx: Context[T]): TerminationDirective = ctx.absorbTermination()
|
||||
}
|
||||
|
||||
// This test is related to issue #17351
|
||||
private[akka] class PushFinishStage(onPostStop: () ⇒ Unit = () ⇒ ()) extends PushStage[Any, Any] {
|
||||
override def onPush(elem: Any, ctx: Context[Any]): SyncDirective =
|
||||
ctx.pushAndFinish(elem)
|
||||
|
||||
override def onUpstreamFinish(ctx: Context[Any]): TerminationDirective =
|
||||
ctx.fail(akka.stream.testkit.Utils.TE("Cannot happen"))
|
||||
|
||||
override def postStop(): Unit =
|
||||
onPostStop()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue