Skip to content

Commit 80b8926

Browse files
authored
Add a fail-safe ensuring that we don't act on old timers in groupedWithin (#363)
1 parent 3c03c31 commit 80b8926

1 file changed

Lines changed: 13 additions & 6 deletions

File tree

core/src/main/scala/ox/flow/FlowOps.scala

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ class FlowOps[+T]:
677677
*/
678678
def groupedWithin(n: Int, duration: FiniteDuration)(using BufferCapacity): Flow[Seq[T]] = groupedWeightedWithin(n, duration)(_ => 1)
679679

680-
private case object GroupingTimeout
680+
private case class GroupingTimeout(generation: Long)
681681

682682
/** Chunks up the emitted elements into groups, within a time window, or limited by the cumulative weight being greater or equal to the
683683
* `minWeight`, whatever happens first. The timeout is reset after a group is emitted. If timeout expires and the buffer is empty,
@@ -698,14 +698,18 @@ class FlowOps[+T]:
698698
unsupervised:
699699
val c = outer.runToChannel()
700700
val c2 = BufferCapacity.newChannel[Seq[T]]
701-
val timerChannel = BufferCapacity.newChannel[GroupingTimeout.type]
701+
val timerChannel = BufferCapacity.newChannel[GroupingTimeout]
702702
forkPropagate(c2):
703703
var buffer = Vector.empty[T]
704704
var accumulatedCost: Long = 0
705+
var currentGeneration = 0L
705706

706-
def forkTimeout() = forkCancellable:
707-
sleep(duration)
708-
timerChannel.sendOrClosed(GroupingTimeout).discard
707+
def forkTimeout() =
708+
currentGeneration += 1
709+
val gen = currentGeneration
710+
forkCancellable:
711+
sleep(duration)
712+
timerChannel.sendOrClosed(GroupingTimeout(gen)).discard
709713

710714
var timeoutFork: Option[CancellableFork[Unit]] = Some(forkTimeout())
711715

@@ -727,10 +731,13 @@ class FlowOps[+T]:
727731
timeoutFork.foreach(_.cancelNow())
728732
c2.error(r)
729733
false
730-
case timerChannel.Received(GroupingTimeout) =>
734+
case timerChannel.Received(GroupingTimeout(gen)) if gen == currentGeneration =>
731735
timeoutFork = None // enter 'timed out state', may stay in this state if buffer is empty
732736
if buffer.nonEmpty then sendBufferAndForkNewTimeout()
733737
true
738+
case timerChannel.Received(GroupingTimeout(_)) =>
739+
// different (older) generation, ignore - must have been sent concurrently with cancelling the timer
740+
true
734741
case c.Received(t) =>
735742
buffer = buffer :+ t
736743

0 commit comments

Comments
 (0)