This is an automated email from the ASF dual-hosted git repository.

He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git


The following commit(s) were added to refs/heads/main by this push:
     new 22084da85c feat: add supervision strategy support for 
groupedWeightedWithin costFn (#3180)
22084da85c is described below

commit 22084da85cbbcca9eef916a1137a81f82d083fd9
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Sat Jun 27 21:52:03 2026 +0800

    feat: add supervision strategy support for groupedWeightedWithin costFn 
(#3180)
    
    Motivation:
    The groupedWeightedWithin operator's costFn is user-provided and may throw,
    but it was called without a try-catch. Any exception failed the stream
    unconditionally, ignoring the configured SupervisionStrategy. This was
    inconsistent with operators such as map, filter and batch.
    
    Modification:
    Wrap costFn(elem) inside GroupedWeightedWithin.nextElement() with a 
try-catch
    that consults the SupervisionStrategy decider. The cost is computed before 
any
    state mutation so the happy path is unchanged. Stop fails the stage, Resume
    skips the offending element while keeping the current group, and Restart 
drops
    the in-progress group via a new resetGroupState() helper and reschedules the
    time-window timer so the fresh group gets a full window. The decider is a 
lazy
    val for zero overhead on the happy path. Update the Scala and Java DSL 
scaladoc
    (both overloads) and the operator reference page to document supervision
    adherence.
    
    Result:
    groupedWeightedWithin now adheres to the SupervisionStrategy attribute with
    well-defined Resume and Restart semantics.
    
    Tests:
    - sbt "stream-tests/Test/testOnly 
org.apache.pekko.stream.scaladsl.FlowGroupedWithinSpec" -- 21/21 passed
    
    References:
    Refs #3110
---
 .../Source-or-Flow/groupedWeightedWithin.md        |   1 +
 .../stream/scaladsl/FlowGroupedWithinSpec.scala    | 127 +++++++++++++++++++++
 .../org/apache/pekko/stream/impl/fusing/Ops.scala  |  34 +++++-
 .../org/apache/pekko/stream/javadsl/Flow.scala     |   8 ++
 .../org/apache/pekko/stream/javadsl/Source.scala   |   8 ++
 .../org/apache/pekko/stream/javadsl/SubFlow.scala  |   8 ++
 .../apache/pekko/stream/javadsl/SubSource.scala    |   8 ++
 .../org/apache/pekko/stream/scaladsl/Flow.scala    |   8 ++
 8 files changed, 201 insertions(+), 1 deletion(-)

diff --git 
a/docs/src/main/paradox/stream/operators/Source-or-Flow/groupedWeightedWithin.md
 
b/docs/src/main/paradox/stream/operators/Source-or-Flow/groupedWeightedWithin.md
index bbf9a94d18..e7c15491df 100644
--- 
a/docs/src/main/paradox/stream/operators/Source-or-Flow/groupedWeightedWithin.md
+++ 
b/docs/src/main/paradox/stream/operators/Source-or-Flow/groupedWeightedWithin.md
@@ -15,6 +15,7 @@ Chunk up this stream into groups of elements received within 
a time window, or l
 Chunk up this stream into groups of elements received within a time window, or 
limited by the weight and number of 
 the elements, whatever happens first. Empty groups will not be emitted if no 
elements are received from upstream.
 The last group before end-of-stream will contain the buffered elements since 
the previously emitted group.
+The `groupedWeightedWithin` operator adheres to the 
ActorAttributes.SupervisionStrategy attribute. On `Supervision.Resume` the 
offending element is skipped and the current group is kept; on 
`Supervision.Restart` the current group is dropped.
 
 See also:
 
diff --git 
a/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowGroupedWithinSpec.scala
 
b/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowGroupedWithinSpec.scala
index 53092a3de8..53165db432 100644
--- 
a/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowGroupedWithinSpec.scala
+++ 
b/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowGroupedWithinSpec.scala
@@ -19,6 +19,8 @@ import scala.collection.immutable
 import scala.concurrent.duration._
 
 import org.apache.pekko
+import pekko.stream.ActorAttributes
+import pekko.stream.Supervision
 import pekko.stream.ThrottleMode
 import pekko.stream.testkit._
 import pekko.testkit.TimingTest
@@ -336,5 +338,130 @@ class FlowGroupedWithinSpec extends StreamSpec with 
ScriptedTest {
       downstream.expectNext(Vector(7, 2): immutable.Seq[Long])
       downstream.expectComplete()
     }
+
+    // These supervision tests use a 30.seconds window that never fires, so 
they are deterministic and
+    // intentionally NOT tagged TimingTest — they must run in PR validation as 
regression guards.
+    "stop when cost function throws and supervision is Stop" in {
+      val ex = new RuntimeException("cost function boom")
+      val upstream = TestPublisher.probe[Long]()
+      val downstream = TestSubscriber.probe[immutable.Seq[Long]]()
+      Source
+        .fromPublisher(upstream)
+        .groupedWeightedWithin(100, 3, 30.seconds) { elem =>
+          if (elem == 99L) throw ex
+          else elem
+        }
+        
.withAttributes(ActorAttributes.supervisionStrategy(Supervision.stoppingDecider))
+        .to(Sink.fromSubscriber(downstream))
+        .run()
+
+      downstream.ensureSubscription()
+      downstream.request(1)
+      upstream.sendNext(1L)
+      upstream.sendNext(99L)
+      downstream.expectError(ex)
+    }
+
+    "stop when cost function throws with default supervision" in {
+      val ex = new RuntimeException("cost function boom")
+      Source(List(1L, 99L, 2L))
+        .groupedWeightedWithin(100, 3, 30.seconds) { elem =>
+          if (elem == 99L) throw ex
+          else elem
+        }
+        .runWith(Sink.ignore)
+        .failed
+        .futureValue shouldBe ex
+    }
+
+    "resume when cost function throws and keep current group" in {
+      val ex = new RuntimeException("cost function boom")
+      val upstream = TestPublisher.probe[Long]()
+      val downstream = TestSubscriber.probe[immutable.Seq[Long]]()
+      Source
+        .fromPublisher(upstream)
+        .groupedWeightedWithin(100, 3, 30.seconds) { elem =>
+          if (elem == 99L) throw ex
+          else elem
+        }
+        
.withAttributes(ActorAttributes.supervisionStrategy(Supervision.resumingDecider))
+        .to(Sink.fromSubscriber(downstream))
+        .run()
+
+      downstream.ensureSubscription()
+      upstream.sendNext(1L)
+      upstream.sendNext(2L)
+      upstream.sendNext(99L)
+      upstream.sendNext(3L)
+      upstream.sendNext(4L)
+      upstream.sendNext(5L)
+      upstream.sendComplete()
+
+      downstream.request(1)
+      downstream.expectNext(Vector(1L, 2L, 3L): immutable.Seq[Long])
+      downstream.request(1)
+      downstream.expectNext(Vector(4L, 5L): immutable.Seq[Long])
+      downstream.expectComplete()
+    }
+
+    "resume keeps weight accounting when cost function throws" in {
+      val ex = new RuntimeException("cost function boom")
+      val upstream = TestPublisher.probe[Long]()
+      val downstream = TestSubscriber.probe[immutable.Seq[Long]]()
+      Source
+        .fromPublisher(upstream)
+        // maxWeight=5, large maxNumber so grouping is weight-driven; identity 
cost
+        .groupedWeightedWithin(5, 100, 30.seconds) { elem =>
+          if (elem == 99L) throw ex
+          else elem
+        }
+        
.withAttributes(ActorAttributes.supervisionStrategy(Supervision.resumingDecider))
+        .to(Sink.fromSubscriber(downstream))
+        .run()
+
+      downstream.ensureSubscription()
+      upstream.sendNext(2L)
+      upstream.sendNext(2L)
+      upstream.sendNext(99L) // skipped; its weight must NOT be counted, [2,2] 
(weight 4) kept
+      upstream.sendNext(3L) // 4 + 3 = 7 > maxWeight 5 -> [2,2] flushed, 3 
starts a new group
+      upstream.sendComplete()
+
+      downstream.request(1)
+      downstream.expectNext(Vector(2L, 2L): immutable.Seq[Long])
+      downstream.request(1)
+      downstream.expectNext(Vector(3L): immutable.Seq[Long])
+      downstream.expectComplete()
+    }
+
+    "restart when cost function throws and drop current group" in {
+      val ex = new RuntimeException("cost function boom")
+      val upstream = TestPublisher.probe[Long]()
+      val downstream = TestSubscriber.probe[immutable.Seq[Long]]()
+      Source
+        .fromPublisher(upstream)
+        .groupedWeightedWithin(100, 3, 30.seconds) { elem =>
+          if (elem == 99L) throw ex
+          else elem
+        }
+        
.withAttributes(ActorAttributes.supervisionStrategy(Supervision.restartingDecider))
+        .to(Sink.fromSubscriber(downstream))
+        .run()
+
+      downstream.ensureSubscription()
+      upstream.sendNext(1L)
+      upstream.sendNext(2L)
+      upstream.sendNext(99L)
+      upstream.sendNext(3L)
+      upstream.sendNext(4L)
+      upstream.sendNext(5L)
+      upstream.sendNext(6L)
+      upstream.sendComplete()
+
+      downstream.request(1)
+      downstream.expectNext(Vector(3L, 4L, 5L): immutable.Seq[Long])
+      downstream.request(1)
+      downstream.expectNext(Vector(6L): immutable.Seq[Long])
+      downstream.expectComplete()
+    }
   }
 }
diff --git 
a/stream/src/main/scala/org/apache/pekko/stream/impl/fusing/Ops.scala 
b/stream/src/main/scala/org/apache/pekko/stream/impl/fusing/Ops.scala
index 882ec381a8..c267659b03 100644
--- a/stream/src/main/scala/org/apache/pekko/stream/impl/fusing/Ops.scala
+++ b/stream/src/main/scala/org/apache/pekko/stream/impl/fusing/Ops.scala
@@ -1872,6 +1872,7 @@ private[stream] object Collect {
       private var totalWeight = 0L
       private var totalNumber = 0
       private var hasElements = false
+      private lazy val decider = 
inheritedAttributes.mandatoryAttribute[SupervisionStrategy].decider
       private val contextPropagation = ContextPropagation()
 
       override def preStart() = {
@@ -1880,8 +1881,28 @@ private[stream] object Collect {
       }
 
       private def nextElement(elem: T): Unit = {
+        // costFn is user code and may throw; honor supervision strategy.
+        val cost =
+          try costFn(elem)
+          catch {
+            case NonFatal(ex) =>
+              decider(ex) match {
+                case Supervision.Stop =>
+                  failStage(ex)
+                  return
+                case Supervision.Resume =>
+                  // skip the offending element and keep the current group
+                  if (!hasBeenPulled(in)) pull(in)
+                  return
+                case Supervision.Restart =>
+                  // drop the current group and reset the time window, then 
continue from a fresh accumulator
+                  resetGroupState()
+                  
scheduleWithFixedDelay(GroupedWeightedWithin.groupedWeightedWithinTimer, 
interval, interval)
+                  if (!hasBeenPulled(in)) pull(in)
+                  return
+              }
+          }
         groupEmitted = false
-        val cost = costFn(elem)
         if (cost < 0L)
           failStage(new IllegalArgumentException(s"Negative weight [$cost] for 
element [$elem] is not allowed"))
         else {
@@ -1924,6 +1945,17 @@ private[stream] object Collect {
         }
       }
 
+      private def resetGroupState(): Unit = {
+        builder.clear()
+        pending = null.asInstanceOf[T]
+        pendingWeight = 0L
+        pushEagerly = false
+        groupEmitted = true
+        totalWeight = 0L
+        totalNumber = 0
+        hasElements = false
+      }
+
       private def tryCloseGroup(): Unit = {
         if (isAvailable(out)) emitGroup()
         else if (pending != null || finished) pushEagerly = true
diff --git a/stream/src/main/scala/org/apache/pekko/stream/javadsl/Flow.scala 
b/stream/src/main/scala/org/apache/pekko/stream/javadsl/Flow.scala
index 512b8ac022..1fda31b964 100755
--- a/stream/src/main/scala/org/apache/pekko/stream/javadsl/Flow.scala
+++ b/stream/src/main/scala/org/apache/pekko/stream/javadsl/Flow.scala
@@ -1699,6 +1699,10 @@ final class Flow[In, Out, Mat](delegate: 
scaladsl.Flow[In, Out, Mat]) extends Gr
    * The last group before end-of-stream will contain the buffered elements
    * since the previously emitted group.
    *
+   * Adheres to the [[ActorAttributes.SupervisionStrategy]] attribute. On 
`Supervision.Resume` the
+   * offending element is skipped and the current group is kept; on 
`Supervision.Restart` the current
+   * group is dropped.
+   *
    * '''Emits when''' the configured time elapses since the last group has 
been emitted or weight limit reached
    *
    * '''Backpressures when''' downstream backpressures, and buffered group (+ 
pending element) weighs more than `maxWeight`
@@ -1723,6 +1727,10 @@ final class Flow[In, Out, Mat](delegate: 
scaladsl.Flow[In, Out, Mat]) extends Gr
    * The last group before end-of-stream will contain the buffered elements
    * since the previously emitted group.
    *
+   * Adheres to the [[ActorAttributes.SupervisionStrategy]] attribute. On 
`Supervision.Resume` the
+   * offending element is skipped and the current group is kept; on 
`Supervision.Restart` the current
+   * group is dropped.
+   *
    * '''Emits when''' the configured time elapses since the last group has 
been emitted or weight limit reached
    *
    * '''Backpressures when''' downstream backpressures, and buffered group (+ 
pending element) weighs more than
diff --git a/stream/src/main/scala/org/apache/pekko/stream/javadsl/Source.scala 
b/stream/src/main/scala/org/apache/pekko/stream/javadsl/Source.scala
index e6ed914a8e..e889fbf5ea 100755
--- a/stream/src/main/scala/org/apache/pekko/stream/javadsl/Source.scala
+++ b/stream/src/main/scala/org/apache/pekko/stream/javadsl/Source.scala
@@ -3583,6 +3583,10 @@ final class Source[Out, Mat](delegate: 
scaladsl.Source[Out, Mat]) extends Graph[
    * The last group before end-of-stream will contain the buffered elements
    * since the previously emitted group.
    *
+   * Adheres to the [[ActorAttributes.SupervisionStrategy]] attribute. On 
`Supervision.Resume` the
+   * offending element is skipped and the current group is kept; on 
`Supervision.Restart` the current
+   * group is dropped.
+   *
    * '''Emits when''' the configured time elapses since the last group has 
been emitted or weight limit reached
    *
    * '''Backpressures when''' downstream backpressures, and buffered group (+ 
pending element) weighs more than `maxWeight`
@@ -3607,6 +3611,10 @@ final class Source[Out, Mat](delegate: 
scaladsl.Source[Out, Mat]) extends Graph[
    * The last group before end-of-stream will contain the buffered elements
    * since the previously emitted group.
    *
+   * Adheres to the [[ActorAttributes.SupervisionStrategy]] attribute. On 
`Supervision.Resume` the
+   * offending element is skipped and the current group is kept; on 
`Supervision.Restart` the current
+   * group is dropped.
+   *
    * '''Emits when''' the configured time elapses since the last group has 
been emitted or weight limit reached
    *
    * '''Backpressures when''' downstream backpressures, and buffered group (+ 
pending element) weighs more than
diff --git 
a/stream/src/main/scala/org/apache/pekko/stream/javadsl/SubFlow.scala 
b/stream/src/main/scala/org/apache/pekko/stream/javadsl/SubFlow.scala
index e2a1644e12..e54eebec08 100755
--- a/stream/src/main/scala/org/apache/pekko/stream/javadsl/SubFlow.scala
+++ b/stream/src/main/scala/org/apache/pekko/stream/javadsl/SubFlow.scala
@@ -1093,6 +1093,10 @@ final class SubFlow[In, Out, Mat](
    * The last group before end-of-stream will contain the buffered elements
    * since the previously emitted group.
    *
+   * Adheres to the [[ActorAttributes.SupervisionStrategy]] attribute. On 
`Supervision.Resume` the
+   * offending element is skipped and the current group is kept; on 
`Supervision.Restart` the current
+   * group is dropped.
+   *
    * '''Emits when''' the configured time elapses since the last group has 
been emitted or weight limit reached
    *
    * '''Backpressures when''' downstream backpressures, and buffered group (+ 
pending element) weighs more than `maxWeight`
@@ -1117,6 +1121,10 @@ final class SubFlow[In, Out, Mat](
    * The last group before end-of-stream will contain the buffered elements
    * since the previously emitted group.
    *
+   * Adheres to the [[ActorAttributes.SupervisionStrategy]] attribute. On 
`Supervision.Resume` the
+   * offending element is skipped and the current group is kept; on 
`Supervision.Restart` the current
+   * group is dropped.
+   *
    * '''Emits when''' the configured time elapses since the last group has 
been emitted or weight limit reached
    *
    * '''Backpressures when''' downstream backpressures, and buffered group (+ 
pending element) weighs more than
diff --git 
a/stream/src/main/scala/org/apache/pekko/stream/javadsl/SubSource.scala 
b/stream/src/main/scala/org/apache/pekko/stream/javadsl/SubSource.scala
index c756c91c56..5e16f5d39f 100755
--- a/stream/src/main/scala/org/apache/pekko/stream/javadsl/SubSource.scala
+++ b/stream/src/main/scala/org/apache/pekko/stream/javadsl/SubSource.scala
@@ -1079,6 +1079,10 @@ final class SubSource[Out, Mat](
    * The last group before end-of-stream will contain the buffered elements
    * since the previously emitted group.
    *
+   * Adheres to the [[ActorAttributes.SupervisionStrategy]] attribute. On 
`Supervision.Resume` the
+   * offending element is skipped and the current group is kept; on 
`Supervision.Restart` the current
+   * group is dropped.
+   *
    * '''Emits when''' the configured time elapses since the last group has 
been emitted or weight limit reached
    *
    * '''Backpressures when''' downstream backpressures, and buffered group (+ 
pending element) weighs more than `maxWeight`
@@ -1103,6 +1107,10 @@ final class SubSource[Out, Mat](
    * The last group before end-of-stream will contain the buffered elements
    * since the previously emitted group.
    *
+   * Adheres to the [[ActorAttributes.SupervisionStrategy]] attribute. On 
`Supervision.Resume` the
+   * offending element is skipped and the current group is kept; on 
`Supervision.Restart` the current
+   * group is dropped.
+   *
    * '''Emits when''' the configured time elapses since the last group has 
been emitted or weight limit reached
    *
    * '''Backpressures when''' downstream backpressures, and buffered group (+ 
pending element) weighs more than
diff --git a/stream/src/main/scala/org/apache/pekko/stream/scaladsl/Flow.scala 
b/stream/src/main/scala/org/apache/pekko/stream/scaladsl/Flow.scala
index 4ae16e46ce..6e3c0ffe4d 100755
--- a/stream/src/main/scala/org/apache/pekko/stream/scaladsl/Flow.scala
+++ b/stream/src/main/scala/org/apache/pekko/stream/scaladsl/Flow.scala
@@ -2207,6 +2207,10 @@ trait FlowOps[+Out, +Mat] {
    * `maxWeight` must be positive, and `d` must be greater than 0 seconds, 
otherwise
    * IllegalArgumentException is thrown.
    *
+   * Adheres to the [[ActorAttributes.SupervisionStrategy]] attribute. On 
`Supervision.Resume` the
+   * offending element is skipped and the current group is kept; on 
`Supervision.Restart` the current
+   * group is dropped.
+   *
    * '''Emits when''' the configured time elapses since the last group has 
been emitted or weight limit reached
    *
    * '''Backpressures when''' downstream backpressures, and buffered group (+ 
pending element) weighs more than `maxWeight`
@@ -2228,6 +2232,10 @@ trait FlowOps[+Out, +Mat] {
    * `maxWeight` must be positive, `maxNumber` must be positive, and `d` must 
be greater than 0 seconds,
    * otherwise IllegalArgumentException is thrown.
    *
+   * Adheres to the [[ActorAttributes.SupervisionStrategy]] attribute. On 
`Supervision.Resume` the
+   * offending element is skipped and the current group is kept; on 
`Supervision.Restart` the current
+   * group is dropped.
+   *
    * '''Emits when''' the configured time elapses since the last group has 
been emitted or weight limit reached
    *
    * '''Backpressures when''' downstream backpressures, and buffered group (+ 
pending element) weighs more than


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to