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 f57cc814c7 fix(stream): log supervision Resume/Restart in MapAsync
operators (#3124)
f57cc814c7 is described below
commit f57cc814c7d475fd58743e0ad8c6592c5aaf35b4
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Tue Jun 23 01:25:12 2026 +0800
fix(stream): log supervision Resume/Restart in MapAsync operators (#3124)
Motivation:
MapAsync, MapAsyncUnordered, and MapAsyncPartitioned silently skip
failed elements when the supervision strategy returns Resume or
Restart, making production debugging difficult.
Modification:
Add StageLogging mixin and log supervision failures at the configured
error level (respecting stage-errors-default-log-level and per-stage
LogLevels.onFailure attribute) before skipping elements. Logging is
placed in pushNextIfPossible / futureCompleted / onPush sync handler
to avoid double-logging via Holder's directive cache.
Result:
Supervision Resume/Restart decisions now produce log entries with
exception details, enabling users to diagnose dropped elements.
Tests:
- FlowMapAsyncSpec: 3 new log verification tests (async, sync, Off)
- FlowMapAsyncUnorderedSpec: 3 new log verification tests
- FlowMapAsyncPartitionedSpec: 3 new log verification tests (ordered +
unordered + Off)
- All 74 existing + new tests pass
References:
Fixes #3103
---
.../scaladsl/FlowMapAsyncPartitionedSpec.scala | 55 +++++++++++++++++
.../pekko/stream/scaladsl/FlowMapAsyncSpec.scala | 47 ++++++++++++++
.../scaladsl/FlowMapAsyncUnorderedSpec.scala | 53 ++++++++++++++++
.../apache/pekko/stream/MapAsyncPartitioned.scala | 41 ++++++++++--
.../org/apache/pekko/stream/impl/fusing/Ops.scala | 72 ++++++++++++++++++++--
5 files changed, 258 insertions(+), 10 deletions(-)
diff --git
a/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowMapAsyncPartitionedSpec.scala
b/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowMapAsyncPartitionedSpec.scala
index 8b3635050d..1920af03c2 100644
---
a/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowMapAsyncPartitionedSpec.scala
+++
b/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowMapAsyncPartitionedSpec.scala
@@ -27,10 +27,12 @@ import scala.concurrent.duration._
import org.apache.pekko
import pekko.Done
import pekko.stream.ActorAttributes
+import pekko.stream.Attributes
import pekko.stream.Supervision
import pekko.stream.testkit._
import pekko.stream.testkit.scaladsl.TestSink
import pekko.stream.testkit.scaladsl.TestSource
+import pekko.testkit.EventFilter
import pekko.testkit.TestDuration
import pekko.testkit.TestLatch
import pekko.testkit.TestProbe
@@ -578,5 +580,58 @@ class FlowMapAsyncPartitionedSpec extends StreamSpec with
WithLogCapturing {
else Future.successful(elem)
}
}
+
+ "log error when future fails with resume supervision (ordered)" in {
+ EventFilter.error(
+ start = "Supervision strategy resumed after exception in
MapAsyncPartitioned",
+ occurrences = 1).intercept {
+ val result = Source(1 to 5)
+ .mapAsyncPartitioned(4)(_ % 2) { (n, _) =>
+ if (n == 3) Future.failed(TE("err3"))
+ else Future.successful(n)
+ }
+
.withAttributes(ActorAttributes.supervisionStrategy(Supervision.resumingDecider))
+ .runWith(Sink.seq)
+
+ result.futureValue should ===(Seq(1, 2, 4, 5))
+ }
+ }
+
+ "log error when future fails with resume supervision (unordered)" in {
+ EventFilter.error(
+ start = "Supervision strategy resumed after exception in
MapAsyncPartitioned",
+ occurrences = 1).intercept {
+ val result = Source(1 to 5)
+ .mapAsyncPartitionedUnordered(4)(_ % 2) { (n, _) =>
+ if (n == 3) Future.failed(TE("err3"))
+ else Future.successful(n)
+ }
+
.withAttributes(ActorAttributes.supervisionStrategy(Supervision.resumingDecider))
+ .runWith(Sink.seq)
+
+ result.futureValue.toSet should ===(Set(1, 2, 4, 5))
+ }
+ }
+
+ "not log when log level is Off and resume supervision is used" in {
+ EventFilter.error(
+ start = "Supervision strategy resumed after exception in
MapAsyncPartitioned",
+ occurrences = 0).intercept {
+ val result = Source(1 to 5)
+ .mapAsyncPartitioned(4)(_ % 2) { (n, _) =>
+ if (n == 3) Future.failed(TE("err3"))
+ else Future.successful(n)
+ }
+ .withAttributes(
+ ActorAttributes.supervisionStrategy(Supervision.resumingDecider)
and
+ Attributes.logLevels(
+ onElement = Attributes.LogLevels.Off,
+ onFinish = Attributes.LogLevels.Off,
+ onFailure = Attributes.LogLevels.Off))
+ .runWith(Sink.seq)
+
+ result.futureValue should ===(Seq(1, 2, 4, 5))
+ }
+ }
}
}
diff --git
a/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowMapAsyncSpec.scala
b/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowMapAsyncSpec.scala
index 281b0c3560..37ad2fb353 100644
---
a/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowMapAsyncSpec.scala
+++
b/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowMapAsyncSpec.scala
@@ -28,11 +28,13 @@ import scala.util.control.NoStackTrace
import org.apache.pekko
import pekko.stream.ActorAttributes
import pekko.stream.ActorAttributes.supervisionStrategy
+import pekko.stream.Attributes
import pekko.stream.Supervision
import pekko.stream.Supervision.resumingDecider
import pekko.stream.testkit._
import pekko.stream.testkit.Utils._
import pekko.stream.testkit.scaladsl.TestSink
+import pekko.testkit.EventFilter
import pekko.testkit.TestDuration
import pekko.testkit.TestLatch
import pekko.testkit.TestProbe
@@ -552,6 +554,51 @@ class FlowMapAsyncSpec extends StreamSpec {
failCount.get() should ===(1)
}
+ "log error when future fails with resume supervision" in {
+ EventFilter.error(start = "Supervision strategy resumed after exception
in MapAsync", occurrences = 1).intercept {
+ val result = Source(1 to 5)
+ .mapAsync(4)(n =>
+ if (n == 3) Future.failed(new TE("err3"))
+ else Future.successful(n))
+ .withAttributes(supervisionStrategy(resumingDecider))
+ .runWith(Sink.seq)
+
+ result.futureValue should ===(Seq(1, 2, 4, 5))
+ }
+ }
+
+ "log error when mapAsync function throws with resume supervision" in {
+ implicit val ec = system.dispatcher
+ EventFilter.error(start = "Supervision strategy resumed after exception
in MapAsync", occurrences = 1).intercept {
+ val result = Source(1 to 5)
+ .mapAsync(4)(n =>
+ if (n == 3) throw TE("err-sync")
+ else Future(n))
+ .withAttributes(supervisionStrategy(resumingDecider))
+ .runWith(Sink.seq)
+
+ result.futureValue should ===(Seq(1, 2, 4, 5))
+ }
+ }
+
+ "not log when log level is Off and resume supervision is used" in {
+ EventFilter.error(start = "Supervision strategy resumed after exception
in MapAsync", occurrences = 0).intercept {
+ val result = Source(1 to 5)
+ .mapAsync(4)(n =>
+ if (n == 3) Future.failed(new TE("err3"))
+ else Future.successful(n))
+ .withAttributes(
+ supervisionStrategy(resumingDecider) and
+ Attributes.logLevels(
+ onElement = Attributes.LogLevels.Off,
+ onFinish = Attributes.LogLevels.Off,
+ onFailure = Attributes.LogLevels.Off))
+ .runWith(Sink.seq)
+
+ result.futureValue should ===(Seq(1, 2, 4, 5))
+ }
+ }
+
}
}
diff --git
a/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowMapAsyncUnorderedSpec.scala
b/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowMapAsyncUnorderedSpec.scala
index 21d0a4a566..8c7f5ca595 100644
---
a/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowMapAsyncUnorderedSpec.scala
+++
b/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/FlowMapAsyncUnorderedSpec.scala
@@ -26,9 +26,11 @@ import scala.util.control.NoStackTrace
import org.apache.pekko
import pekko.stream.ActorAttributes.supervisionStrategy
+import pekko.stream.Attributes
import pekko.stream.Supervision.resumingDecider
import pekko.stream.testkit._
import pekko.stream.testkit.scaladsl._
+import pekko.testkit.EventFilter
import pekko.testkit.TestDuration
import pekko.testkit.TestLatch
import pekko.testkit.TestProbe
@@ -376,5 +378,56 @@ class FlowMapAsyncUnorderedSpec extends StreamSpec {
}
}
+ "log error when future fails with resume supervision" in {
+ EventFilter.error(
+ start = "Supervision strategy resumed after exception in
MapAsyncUnordered",
+ occurrences = 1).intercept {
+ val result = Source(1 to 5)
+ .mapAsyncUnordered(4)(n =>
+ if (n == 3) Future.failed(new Exception("err3") with NoStackTrace)
+ else Future.successful(n))
+ .withAttributes(supervisionStrategy(resumingDecider))
+ .runWith(Sink.seq)
+
+ result.futureValue.toSet should ===(Set(1, 2, 4, 5))
+ }
+ }
+
+ "log error when mapAsyncUnordered function throws with resume supervision"
in {
+ implicit val ec = system.dispatcher
+ EventFilter.error(
+ start = "Supervision strategy resumed after exception in
MapAsyncUnordered",
+ occurrences = 1).intercept {
+ val result = Source(1 to 5)
+ .mapAsyncUnordered(4)(n =>
+ if (n == 3) throw Utils.TE("err-sync")
+ else Future(n))
+ .withAttributes(supervisionStrategy(resumingDecider))
+ .runWith(Sink.seq)
+
+ result.futureValue.toSet should ===(Set(1, 2, 4, 5))
+ }
+ }
+
+ "not log when log level is Off and resume supervision is used" in {
+ EventFilter.error(
+ start = "Supervision strategy resumed after exception in
MapAsyncUnordered",
+ occurrences = 0).intercept {
+ val result = Source(1 to 5)
+ .mapAsyncUnordered(4)(n =>
+ if (n == 3) Future.failed(new Exception("err3") with NoStackTrace)
+ else Future.successful(n))
+ .withAttributes(
+ supervisionStrategy(resumingDecider) and
+ Attributes.logLevels(
+ onElement = Attributes.LogLevels.Off,
+ onFinish = Attributes.LogLevels.Off,
+ onFailure = Attributes.LogLevels.Off))
+ .runWith(Sink.seq)
+
+ result.futureValue.toSet should ===(Set(1, 2, 4, 5))
+ }
+ }
+
}
}
diff --git
a/stream/src/main/scala/org/apache/pekko/stream/MapAsyncPartitioned.scala
b/stream/src/main/scala/org/apache/pekko/stream/MapAsyncPartitioned.scala
index 565d8a2b51..b7ca9219d2 100644
--- a/stream/src/main/scala/org/apache/pekko/stream/MapAsyncPartitioned.scala
+++ b/stream/src/main/scala/org/apache/pekko/stream/MapAsyncPartitioned.scala
@@ -25,7 +25,10 @@ import scala.util.control.{ NoStackTrace, NonFatal }
import org.apache.pekko
import pekko.annotation.InternalApi
+import pekko.event.Logging
+import pekko.event.Logging.LogLevel
import pekko.stream.ActorAttributes.SupervisionStrategy
+import pekko.stream.Attributes.LogLevels
import pekko.stream.stage._
import pekko.util.OptionVal
@@ -82,9 +85,11 @@ private[stream] final class MapAsyncPartitioned[In, Out,
Partition](
override val shape: FlowShape[In, Out] = FlowShape(in, out)
override def createLogic(inheritedAttributes: Attributes): GraphStageLogic =
- new GraphStageLogic(shape) with InHandler with OutHandler {
+ new GraphStageLogic(shape) with InHandler with OutHandler with
StageLogging {
private val contextPropagation = pekko.stream.impl.ContextPropagation()
+ override protected def logSource: Class[?] =
classOf[MapAsyncPartitioned[?, ?, ?]]
+
private final class Contextual[T](context: AnyRef, val element: T) {
private var suspended = false
@@ -107,6 +112,30 @@ private[stream] final class MapAsyncPartitioned[In, Out,
Partition](
private var partitionsInProgress: mutable.Set[Partition] = _
private var buffer: mutable.Queue[(Partition, Contextual[Holder[In,
Out]])] = _
+ private def logSupervisionFailure(ex: Throwable): Unit = {
+ val logAt: LogLevel = inheritedAttributes.get[LogLevels] match {
+ case Some(levels) => levels.onFailure
+ case None => LogLevels.defaultErrorLevel(materializer.system)
+ }
+ logAt match {
+ case Logging.ErrorLevel =>
+ log.error(ex, "Supervision strategy resumed after exception in
MapAsyncPartitioned: {}", ex.getMessage)
+ case Logging.WarningLevel =>
+ log.warning(ex, "Supervision strategy resumed after exception in
MapAsyncPartitioned: {}", ex.getMessage)
+ case Logging.InfoLevel =>
+ log.info(
+ "Supervision strategy resumed after exception in
MapAsyncPartitioned: {}: {}",
+ Logging.simpleName(ex.getClass),
+ ex.getMessage)
+ case Logging.DebugLevel =>
+ log.debug(
+ "Supervision strategy resumed after exception in
MapAsyncPartitioned: {}: {}",
+ Logging.simpleName(ex.getClass),
+ ex.getMessage)
+ case _ => // Off, nop
+ }
+ }
+
private val futureCB = getAsyncCallback[Holder[In, Out]](holder =>
holder.out match {
case Success(_) => pushNextIfPossible()
@@ -142,7 +171,9 @@ private[stream] final class MapAsyncPartitioned[In, Out,
Partition](
wrappedInput.suspend()
}
} catch {
- case NonFatal(ex) => if (decider(ex) == Supervision.Stop)
failStage(ex)
+ case NonFatal(ex) =>
+ if (decider(ex) == Supervision.Stop) failStage(ex)
+ else logSupervisionFailure(ex)
}
pullIfNeeded()
@@ -201,7 +232,8 @@ private[stream] final class MapAsyncPartitioned[In, Out,
Partition](
case Supervision.Stop =>
failStage(ex)
case _ =>
- // try next element
+ // try next element
+ logSupervisionFailure(ex)
}
case Failure(ex) =>
// fatal exception in buffer, not sure that it can actually
happen, but for good measure
@@ -242,7 +274,8 @@ private[stream] final class MapAsyncPartitioned[In, Out,
Partition](
case Supervision.Stop =>
failStage(ex)
case _ =>
- // try next element
+ // try next element
+ logSupervisionFailure(ex)
}
case Failure(ex) =>
// fatal exception in buffer, not sure that it can actually
happen, but for good measure
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 3832ccf269..010260e03d 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
@@ -1296,11 +1296,37 @@ private[stream] object Collect {
override val shape = FlowShape(in, out)
override def createLogic(inheritedAttributes: Attributes): GraphStageLogic =
- new GraphStageLogic(shape) with InHandler with OutHandler {
+ new GraphStageLogic(shape) with InHandler with OutHandler with
StageLogging {
+
+ override protected def logSource: Class[?] = classOf[MapAsync[?, ?]]
lazy val decider =
inheritedAttributes.mandatoryAttribute[SupervisionStrategy].decider
var buffer: BufferImpl[Holder[Out]] = _
+ private def logSupervisionFailure(ex: Throwable): Unit = {
+ val logAt: LogLevel = inheritedAttributes.get[LogLevels] match {
+ case Some(levels) => levels.onFailure
+ case None => LogLevels.defaultErrorLevel(materializer.system)
+ }
+ logAt match {
+ case Logging.ErrorLevel =>
+ log.error(ex, "Supervision strategy resumed after exception in
MapAsync: {}", ex.getMessage)
+ case Logging.WarningLevel =>
+ log.warning(ex, "Supervision strategy resumed after exception in
MapAsync: {}", ex.getMessage)
+ case Logging.InfoLevel =>
+ log.info(
+ "Supervision strategy resumed after exception in MapAsync: {}:
{}",
+ Logging.simpleName(ex.getClass),
+ ex.getMessage)
+ case Logging.DebugLevel =>
+ log.debug(
+ "Supervision strategy resumed after exception in MapAsync: {}:
{}",
+ Logging.simpleName(ex.getClass),
+ ex.getMessage)
+ case _ => // Off, nop
+ }
+ }
+
private val futureCB = getAsyncCallback[Holder[Out]](holder =>
holder.elem match {
case Success(_) => pushNextIfPossible()
@@ -1348,7 +1374,9 @@ private[stream] object Collect {
} catch {
// this logic must only be executed if f throws, not if the future
is failed
- case NonFatal(ex) => if (decider(ex) == Supervision.Stop)
failStage(ex)
+ case NonFatal(ex) =>
+ if (decider(ex) == Supervision.Stop) failStage(ex)
+ else logSupervisionFailure(ex)
}
pullIfNeeded()
@@ -1380,6 +1408,7 @@ private[stream] object Collect {
case Supervision.Stop => failStage(ex)
case _ =>
// try next element
+ logSupervisionFailure(ex)
pushNextIfPossible()
}
case Failure(ex) =>
@@ -1413,9 +1442,11 @@ private[stream] object Collect {
override val shape = FlowShape(in, out)
override def createLogic(inheritedAttributes: Attributes): GraphStageLogic =
- new GraphStageLogic(shape) with InHandler with OutHandler {
+ new GraphStageLogic(shape) with InHandler with OutHandler with
StageLogging {
override def toString = s"MapAsyncUnordered.Logic(inFlight=$inFlight,
buffer=$buffer)"
+ override protected def logSource: Class[?] =
classOf[MapAsyncUnordered[?, ?]]
+
private lazy val decider =
inheritedAttributes.mandatoryAttribute[SupervisionStrategy].decider
private var inFlight = 0
@@ -1424,6 +1455,30 @@ private[stream] object Collect {
private def todo: Int = inFlight + buffer.used
+ private def logSupervisionFailure(ex: Throwable): Unit = {
+ val logAt: LogLevel = inheritedAttributes.get[LogLevels] match {
+ case Some(levels) => levels.onFailure
+ case None => LogLevels.defaultErrorLevel(materializer.system)
+ }
+ logAt match {
+ case Logging.ErrorLevel =>
+ log.error(ex, "Supervision strategy resumed after exception in
MapAsyncUnordered: {}", ex.getMessage)
+ case Logging.WarningLevel =>
+ log.warning(ex, "Supervision strategy resumed after exception in
MapAsyncUnordered: {}", ex.getMessage)
+ case Logging.InfoLevel =>
+ log.info(
+ "Supervision strategy resumed after exception in
MapAsyncUnordered: {}: {}",
+ Logging.simpleName(ex.getClass),
+ ex.getMessage)
+ case Logging.DebugLevel =>
+ log.debug(
+ "Supervision strategy resumed after exception in
MapAsyncUnordered: {}: {}",
+ Logging.simpleName(ex.getClass),
+ ex.getMessage)
+ case _ => // Off, nop
+ }
+ }
+
override def preStart(): Unit = buffer = BufferImpl(parallelism,
inheritedAttributes)
def futureCompleted(result: Try[Out]): Unit = {
@@ -1441,8 +1496,11 @@ private[stream] object Collect {
else if (!hasBeenPulled(in)) tryPull(in)
case Failure(ex) =>
if (decider(ex) == Supervision.Stop) failStage(ex)
- else if (isCompleted) completeStage()
- else if (!hasBeenPulled(in)) tryPull(in)
+ else {
+ logSupervisionFailure(ex)
+ if (isCompleted) completeStage()
+ else if (!hasBeenPulled(in)) tryPull(in)
+ }
}
}
@@ -1455,7 +1513,9 @@ private[stream] object Collect {
case Some(v) => futureCompleted(v)
}
} catch {
- case NonFatal(ex) => if (decider(ex) == Supervision.Stop)
failStage(ex)
+ case NonFatal(ex) =>
+ if (decider(ex) == Supervision.Stop) failStage(ex)
+ else logSupervisionFailure(ex)
}
if (todo < parallelism && !hasBeenPulled(in)) tryPull(in)
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]