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-http.git


The following commit(s) were added to refs/heads/main by this push:
     new 18796ca69 Fix byte string transformer cleanup on stage stop (#1154)
18796ca69 is described below

commit 18796ca69492bcb84b98b52005161fe18c4a055c
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Sat Jul 11 04:20:30 2026 +0800

    Fix byte string transformer cleanup on stage stop (#1154)
    
    Motivation:
    A byte string transformer marked itself finished before a pending final 
emission was delivered, so downstream cancellation in that window skipped 
cleanup.
    
    Modification:
    Treat cleanup as the stage finalizer by invoking it unconditionally from 
postStop, and add directional tests for normal completion and cancellation 
while the final emission is pending.
    
    Result:
    All stage termination paths release transformer resources without relying 
on a fragile finished flag. Existing compressor cleanup remains safe because it 
is idempotent.
    
    Tests:
    - sbt "http-core / Test / testOnly 
org.apache.pekko.http.impl.util.StreamUtilsSpec": 7 passed
    - sbt "http-tests / Test / testOnly 
org.apache.pekko.http.scaladsl.coding.DeflateSpec 
org.apache.pekko.http.scaladsl.coding.GzipSpec": 54 passed
    - sbt checkCodeStyle: passed
    - sbt headerCreateAll and +headerCheckAll: passed
    - scalafmt --list --mode diff-ref=origin/main: passed
    - sbt sortImports: environment failure, Scalafix/scala.meta 
NoSuchMethodError
    - sbt validatePullRequest: not run locally per maintainer request; 
delegated to CI
    
    References:
    Refs #1133; follow-up to #1142
---
 .../apache/pekko/http/impl/util/StreamUtils.scala  |  8 ++---
 .../pekko/http/impl/util/StreamUtilsSpec.scala     | 39 ++++++++++++++++++++++
 2 files changed, 41 insertions(+), 6 deletions(-)

diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/util/StreamUtils.scala 
b/http-core/src/main/scala/org/apache/pekko/http/impl/util/StreamUtils.scala
index 5f43eca17..0613a0a9b 100644
--- a/http-core/src/main/scala/org/apache/pekko/http/impl/util/StreamUtils.scala
+++ b/http-core/src/main/scala/org/apache/pekko/http/impl/util/StreamUtils.scala
@@ -43,8 +43,7 @@ private[http] object StreamUtils {
    * Creates a transformer that will call `f` for each incoming ByteString and 
output its result. After the complete
    * input has been read it will call `finish` once to determine the final 
ByteString to post to the output.
    * Empty ByteStrings are discarded.
-   * If the stage is stopped before the input is fully consumed (e.g. on 
downstream cancellation or upstream failure),
-   * `cleanup` is called to release any resources held by the transformer.
+   * When the stage stops, `cleanup` is called to release any resources held 
by the transformer.
    */
   def byteStringTransformer(
       f: ByteString => ByteString,
@@ -53,8 +52,6 @@ private[http] object StreamUtils {
     new SimpleLinearGraphStage[ByteString] {
       override def createLogic(inheritedAttributes: Attributes): 
GraphStageLogic =
         new GraphStageLogic(shape) with InHandler with OutHandler {
-          private var finished = false
-
           override def onPush(): Unit = {
             val data = f(grab(in))
             if (data.nonEmpty) push(out, data)
@@ -65,12 +62,11 @@ private[http] object StreamUtils {
 
           override def onUpstreamFinish(): Unit = {
             val data = finish()
-            finished = true
             if (data.nonEmpty) emit(out, data)
             completeStage()
           }
 
-          override def postStop(): Unit = if (!finished) cleanup()
+          override def postStop(): Unit = cleanup()
 
           setHandlers(in, out, this)
         }
diff --git 
a/http-core/src/test/scala/org/apache/pekko/http/impl/util/StreamUtilsSpec.scala
 
b/http-core/src/test/scala/org/apache/pekko/http/impl/util/StreamUtilsSpec.scala
index 49c3a4f74..13a18860c 100644
--- 
a/http-core/src/test/scala/org/apache/pekko/http/impl/util/StreamUtilsSpec.scala
+++ 
b/http-core/src/test/scala/org/apache/pekko/http/impl/util/StreamUtilsSpec.scala
@@ -16,6 +16,7 @@ package org.apache.pekko.http.impl.util
 import org.apache.pekko
 import pekko.stream.Attributes
 import pekko.stream.scaladsl.{ Sink, Source }
+import pekko.stream.testkit.scaladsl.TestSink
 import pekko.util.ByteString
 import pekko.testkit._
 import org.scalatest.concurrent.ScalaFutures
@@ -25,6 +26,44 @@ import scala.util.Failure
 
 class StreamUtilsSpec extends PekkoSpec with ScalaFutures {
 
+  "byteStringTransformer" should {
+    "clean up after normal completion" in {
+      val events = TestProbe()
+      val transformed = ByteString("transformed")
+      val trailer = ByteString("trailer")
+      val result =
+        Source
+          .single(ByteString("input"))
+          .via(StreamUtils.byteStringTransformer(_ => transformed, () => 
trailer, () => events.ref ! "cleanup"))
+          .runWith(Sink.seq)
+
+      Await.result(result, 3.seconds.dilated) shouldBe Seq(transformed, 
trailer)
+      events.expectMsg("cleanup")
+    }
+
+    "clean up if downstream cancels before the final emission" in {
+      val events = TestProbe()
+      val transformed = ByteString("transformed")
+      val trailer = ByteString("trailer")
+      val downstream =
+        Source
+          .single(ByteString("input"))
+          .via(StreamUtils.byteStringTransformer(
+            _ => transformed,
+            () => {
+              events.ref ! "finish"
+              trailer
+            },
+            () => events.ref ! "cleanup"))
+          .runWith(TestSink[ByteString]())
+
+      downstream.request(1).expectNext(transformed)
+      events.expectMsg("finish")
+      downstream.cancel()
+      events.expectMsg("cleanup")
+    }
+  }
+
   "captureTermination" should {
     "signal completion" when {
       "upstream terminates" in {


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

Reply via email to