Skip to content

Commit 732614a

Browse files
adamwclaude
andauthored
Fix flaky SSE tests in akka/pekko server modules (#5387)
## Problem The "Send and receive SSE" test in `AkkaHttpServerTest` flaked on CI (seen on PR #5381's CI run, 2026-07-07, ci 2.13 / JVM 11) with only: ``` org.scalatest.exceptions.TestFailedException was thrown. (AkkaHttpServerTest.scala:82) ``` No message, no cause. An identical sibling test exists in `PekkoHttpServerTest`. Two things went wrong: 1. **Swallowed exception**: the test ended with `.transform(sse => sse shouldBe ..., ex => fail(ex))`. `fail(ex)` wraps the real exception as the *cause* of a message-less `TestFailedException`, and the test reporter prints neither — hence the bare failure line. 2. **The actual flake**: the sttp client backend was created with default `ConnectionPoolSettings`, whose `response-entity-subscription-timeout` is **1s**. The test consumes the response via `asStreamUnsafe` and only materializes the SSE-parsing `runFold` in a later future callback. On a loaded CI runner that gap can exceed 1s, at which point the akka/pekko-http client pool fails the entity stream ("Response entity was not subscribed after 1 second..."). ## Fix - Build the per-test client backend with `ConnectionPoolSettings(actorSystem).withResponseEntitySubscriptionTimeout(1.minute)` so a slow runner can't hit the subscription timeout. - Replace the `.transform(..., ex => fail(ex))` with a plain `.map(...)` so if the test ever fails again, the real exception (message + type) propagates to the report instead of a bare `TestFailedException`. - Close the per-test client backend after the request completes (it was previously leaked). Same changes applied to both the akka and pekko variants. ## Force-repro proof - A `-Dakka.http.host-connection-pool.response-entity-subscription-timeout=1ms` override propagates into the (non-forked) test JVM but does *not* reproduce locally: on an idle machine the stream is subscribed before the scheduler-tick-granularity timeout task can fire. - Simulating CI load instead — inserting `Thread.sleep(1500)` before the `runFold` materialization on unmodified master (default 1s timeout) — reproduces the **exact** CI symptom: `TestFailedException was thrown. (AkkaHttpServerTest.scala:82)`. - The same 1500ms delay with this fix applied: test **passes**. ## Verification - Both SSE tests (akka + pekko) run 3x each: all green. - `akkaHttpServer2_12/Test/compile`, `pekkoHttpServer2_12/Test/compile`, `pekkoHttpServer3/Test/compile`: all pass. - Full `AkkaHttpServerTest` suite: 301 tests, 0 failures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent a122c5e commit 732614a

2 files changed

Lines changed: 22 additions & 4 deletions

File tree

server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpServerTest.scala

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package sttp.tapir.server.akkahttp
33
import akka.actor.ActorSystem
44
import akka.http.scaladsl.model.HttpEntity
55
import akka.http.scaladsl.server.{Directives, RequestContext}
6+
import akka.http.scaladsl.settings.ConnectionPoolSettings
67
import akka.stream.scaladsl.{Flow, Sink, Source}
78
import cats.effect.unsafe.implicits.global
89
import cats.effect.{IO, Resource}
@@ -27,6 +28,7 @@ import sttp.tapir.tests.{Test, TestSuite}
2728
import java.util.UUID
2829
import java.util.concurrent.atomic.AtomicInteger
2930
import scala.concurrent.Future
31+
import scala.concurrent.duration._
3032
import scala.util.Random
3133

3234
class AkkaHttpServerTest extends TestSuite with EitherValues {
@@ -64,6 +66,12 @@ class AkkaHttpServerTest extends TestSuite with EitherValues {
6466
Future.successful(Source(List(sse1, sse2)))
6567
})
6668
val route = AkkaHttpServerInterpreter().toRoute(e)
69+
// the default 1s response-entity-subscription-timeout can be exceeded on a loaded CI runner before the
70+
// SSE-parsing stream is materialized, failing the entity stream; hence raising the timeout
71+
val clientBackend = AkkaHttpBackend.usingActorSystem(
72+
actorSystem,
73+
customConnectionPoolSettings = Some(ConnectionPoolSettings(actorSystem).withResponseEntitySubscriptionTimeout(1.minute))
74+
)
6775
interpreter
6876
.server(route)
6977
.use { port =>
@@ -78,8 +86,9 @@ class AkkaHttpServerTest extends TestSuite with EitherValues {
7886
)
7987
)
8088
)
81-
.send(AkkaHttpBackend.usingActorSystem(actorSystem))
82-
.flatMap(_.body.value.transform(sse => sse shouldBe List(sse1, sse2), ex => fail(ex)))
89+
.send(clientBackend)
90+
.flatMap(_.body.value.map(sse => sse shouldBe List(sse1, sse2)))
91+
.andThen { case _ => clientBackend.close() }
8392
)
8493
}
8594
}

server/pekko-http-server/src/test/scala/sttp/tapir/server/pekkohttp/PekkoHttpServerTest.scala

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package sttp.tapir.server.pekkohttp
33
import org.apache.pekko.actor.ActorSystem
44
import org.apache.pekko.http.scaladsl.model.HttpEntity
55
import org.apache.pekko.http.scaladsl.server.{Directives, RequestContext}
6+
import org.apache.pekko.http.scaladsl.settings.ConnectionPoolSettings
67
import org.apache.pekko.stream.scaladsl.{Flow, Sink, Source}
78
import cats.effect.unsafe.implicits.global
89
import cats.effect.{IO, Resource}
@@ -28,6 +29,7 @@ import sttp.tapir.tests.{Test, TestSuite}
2829
import java.util.UUID
2930
import java.util.concurrent.atomic.AtomicInteger
3031
import scala.concurrent.Future
32+
import scala.concurrent.duration._
3133
import scala.util.Random
3234

3335
class PekkoHttpServerTest extends TestSuite with EitherValues {
@@ -65,6 +67,12 @@ class PekkoHttpServerTest extends TestSuite with EitherValues {
6567
Future.successful(Source(List(sse1, sse2)))
6668
})
6769
val route = PekkoHttpServerInterpreter().toRoute(e)
70+
// the default 1s response-entity-subscription-timeout can be exceeded on a loaded CI runner before the
71+
// SSE-parsing stream is materialized, failing the entity stream; hence raising the timeout
72+
val clientBackend = PekkoHttpBackend.usingActorSystem(
73+
actorSystem,
74+
customConnectionPoolSettings = Some(ConnectionPoolSettings(actorSystem).withResponseEntitySubscriptionTimeout(1.minute))
75+
)
6876
interpreter
6977
.server(route)
7078
.use { port =>
@@ -79,8 +87,9 @@ class PekkoHttpServerTest extends TestSuite with EitherValues {
7987
)
8088
)
8189
)
82-
.send(PekkoHttpBackend.usingActorSystem(actorSystem))
83-
.flatMap(_.body.value.transform(sse => sse shouldBe List(sse1, sse2), ex => fail(ex)))
90+
.send(clientBackend)
91+
.flatMap(_.body.value.map(sse => sse shouldBe List(sse1, sse2)))
92+
.andThen { case _ => clientBackend.close() }
8493
)
8594
}
8695
}

0 commit comments

Comments
 (0)