Delay operator emit early fix #28269

* Bugfix for delay stage with EmitEarly #28269

Emit early would make it pull eagerly even if out was not available,
when element arrived and buffer was full and out not available that
element would be left in the input port and never end up in the buffer.

* Additional cleanup of the Delay stage
This commit is contained in:
Johan Andrén 2019-11-29 14:46:51 +01:00 committed by GitHub
parent bfabdebace
commit 583fe3e2a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 38 additions and 24 deletions

View file

@ -1700,37 +1700,46 @@ private[stream] object Collect {
/**
* INTERNAL API
*/
@InternalApi private[akka] final class Delay[T](
private[this] val delayStrategySupplier: () => DelayStrategy[_ >: T],
private[this] val overflowStrategy: DelayOverflowStrategy)
extends SimpleLinearGraphStage[T] {
private[this] def timerName = "DelayedTimer"
@InternalApi object Delay {
private val TimerName = "DelayedTimer"
private val DelayPrecisionMS = 10
}
private[this] val DelayPrecisionMS = 10
/**
* INTERNAL API
*/
@InternalApi private[akka] final class Delay[T](
delayStrategySupplier: () => DelayStrategy[T],
overflowStrategy: DelayOverflowStrategy)
extends SimpleLinearGraphStage[T] {
override def initialAttributes: Attributes = DefaultAttributes.delay
override def createLogic(inheritedAttributes: Attributes): GraphStageLogic =
new TimerGraphStageLogic(shape) with InHandler with OutHandler {
import Delay._
private[this] val size = inheritedAttributes.mandatoryAttribute[InputBuffer].max
private[this] val delayStrategy = delayStrategySupplier()
private[this] var buffer
: BufferImpl[(Long, T)] = _ // buffer has pairs timestamp of expected push with upstream element
override def preStart(): Unit = buffer = BufferImpl(size, inheritedAttributes)
// buffer has pairs of timestamp of expected push and element
private[this] val buffer = BufferImpl[(Long, T)](size, inheritedAttributes)
private[this] val onPushWhenBufferFull: () => Unit = overflowStrategy match {
case EmitEarly =>
() => {
if (isAvailable(out)) {
if (isTimerActive(timerName))
cancelTimer(timerName)
if (isTimerActive(TimerName)) {
cancelTimer(TimerName)
}
push(out, buffer.dequeue()._2)
grabAndPull()
completeIfReady()
} else {
throw new IllegalStateException(
"Was configured to emitEarly and got element when out is not ready and buffer is full, should not be possible.")
}
}
case _: DropHead =>
@ -1746,7 +1755,7 @@ private[stream] object Collect {
case _: DropNew =>
() => {
grab(in)
if (pullCondition) pull(in)
if (shouldPull) pull(in)
}
case _: DropBuffer =>
() => {
@ -1768,39 +1777,41 @@ private[stream] object Collect {
onPushWhenBufferFull()
else {
grabAndPull()
if (!isTimerActive(timerName)) {
if (!isTimerActive(TimerName)) {
val waitTime = nextElementWaitTime()
if (waitTime <= DelayPrecisionMS && isAvailable(out)) {
push(out, buffer.dequeue()._2)
completeIfReady()
} else
scheduleOnce(timerName, waitTime.millis)
scheduleOnce(TimerName, waitTime.millis)
}
}
}
private def pullCondition: Boolean =
!overflowStrategy.isBackpressure || buffer.used < size
private def shouldPull: Boolean =
buffer.used < size || !overflowStrategy.isBackpressure ||
// we can only emit early if output is ready
(overflowStrategy == EmitEarly && isAvailable(out))
private def grabAndPull(): Unit = {
val element = grab(in)
buffer.enqueue((System.nanoTime() + delayStrategy.nextDelay(element).toNanos, element))
if (pullCondition) pull(in)
if (shouldPull) pull(in)
}
override def onUpstreamFinish(): Unit =
completeIfReady()
def onPull(): Unit = {
if (!isTimerActive(timerName) && !buffer.isEmpty) {
if (!isTimerActive(TimerName) && !buffer.isEmpty) {
val waitTime = nextElementWaitTime()
if (waitTime <= DelayPrecisionMS)
push(out, buffer.dequeue()._2)
else
scheduleOnce(timerName, waitTime.millis)
scheduleOnce(TimerName, waitTime.millis)
}
if (!isClosed(in) && !hasBeenPulled(in) && pullCondition)
if (!isClosed(in) && !hasBeenPulled(in) && shouldPull)
pull(in)
completeIfReady()