Skip to content

Commit 8a5d9cc

Browse files
adamwclaude
andauthored
Make Vert.x test server Resource repeatable so the flaky-test retry works (#5411)
## Problem The flaky-WebSocket-test retry added in #5410 (`TestSuite.retries`) did **not** actually rescue the Vert.x WS flake. On CI the retry ran but every attempt after the first failed **instantly** with: ``` ERROR DefaultCreateServerTest - Starting server failed because of null java.lang.IllegalStateException at io.vertx.core.http.impl.CleanableHttpServer.listen(CleanableHttpServer.java:61) ``` ## Root cause The three Vert.x **test** interpreters build the HTTP server **eagerly, outside** the `Resource.make` acquire: ```scala val server = vertx.createHttpServer(...).requestHandler(router) // captured once val listenIO = vertxFutureToIo(server.listen(0)) Resource.make(listenIO)(...) ``` The retry re-runs the whole test, re-using the same server `Resource`, which calls `listen()` a **second time on the same server instance**. Vert.x 5's `CleanableHttpServer.listen` throws `IllegalStateException` (empty message → logged as "because of null") when its `listenContext` is already set. So the per-test server Resource simply wasn't repeatable — the shared Vert.x was never actually broken. ## Fix Create the server **inside** the acquire, so each acquire (and each retry) gets a fresh server: ```scala val listenIO = IO.delay { val router = Router.router(vertx) val _ = route(router) vertx.createHttpServer(...).requestHandler(router) }.flatMap(server => vertxFutureToIo(server.listen(0))) Resource.make(listenIO)(s => vertxFutureToIo(s.close()).void).map(_.actualPort()) ``` Applied to `VertxTestServerInterpreter`, `CatsVertxTestServerInterpreter`, `ZioVertxTestServerInterpreter`. ## Test `ServerResourceRepeatableSpec` acquires the server Resource twice (what the retry does). It throws the exact `IllegalStateException` on the old eager code and passes with this fix. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f31bde1 commit 8a5d9cc

4 files changed

Lines changed: 65 additions & 16 deletions

File tree

server/vertx-server/cats/src/test/scala/sttp/tapir/server/vertx/cats/CatsVertxTestServerInterpreter.scala

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,17 @@ class CatsVertxTestServerInterpreter(vertx: Vertx, dispatcher: Dispatcher[IO])
3030
route: Router => Route,
3131
gracefulShutdownTimeout: Option[FiniteDuration]
3232
): Resource[IO, Port] = {
33-
val router = Router.router(vertx)
34-
route(router)
35-
// the maxFormAttributeSize must be higher than in ServerMultipartTests.maxContentLengthTests
36-
val server = vertx.createHttpServer(new HttpServerOptions().setPort(0).setMaxFormAttributeSize(100000)).requestHandler(router)
37-
val listenIO = ioFromVFuture(server.listen(0))
33+
// The server is created inside the Resource's acquire (not eagerly), so that the Resource can be safely re-run:
34+
// the flaky-WebSocket-test retry re-uses this Resource, and Vert.x's CleanableHttpServer.listen throws
35+
// IllegalStateException if listen() is called a second time on the same server instance.
36+
val listenIO = IO
37+
.delay {
38+
val router = Router.router(vertx)
39+
val _ = route(router)
40+
// the maxFormAttributeSize must be higher than in ServerMultipartTests.maxContentLengthTests
41+
vertx.createHttpServer(new HttpServerOptions().setPort(0).setMaxFormAttributeSize(100000)).requestHandler(router)
42+
}
43+
.flatMap(server => ioFromVFuture(server.listen(0)))
3844
// Vertx doesn't offer graceful shutdown with timeout OOTB
3945
Resource.make(listenIO)(s => ioFromVFuture(s.close).void).map(_.actualPort())
4046
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package sttp.tapir.server.vertx.cats
2+
3+
import cats.effect.IO
4+
import cats.effect.std.Dispatcher
5+
import cats.effect.unsafe.implicits.global
6+
import io.vertx.core.Vertx
7+
import io.vertx.ext.web.{Route, Router}
8+
import org.scalatest.funsuite.AnyFunSuite
9+
10+
// Guards that the per-test server Resource can be acquired more than once. The flaky-WebSocket-test retry
11+
// (TestSuite.retries) re-runs the whole test, hence re-uses this Resource; if the server were created eagerly
12+
// (outside the acquire) rather than inside it, the second listen() would hit Vert.x's CleanableHttpServer guard
13+
// and throw IllegalStateException, defeating the retry. See CatsVertxTestServerInterpreter.server.
14+
class ServerResourceRepeatableSpec extends AnyFunSuite {
15+
test("server Resource can be acquired twice without IllegalStateException") {
16+
val vertx = Vertx.vertx()
17+
try {
18+
Dispatcher
19+
.parallel[IO]
20+
.use { dispatcher =>
21+
val interpreter = new CatsVertxTestServerInterpreter(vertx, dispatcher)
22+
val trivialRoute: Router => Route = router => router.route()
23+
val res = interpreter.server(trivialRoute, None)
24+
// first use = original run; second use = the retry
25+
res.use(_ => IO.unit) >> res.use(port => IO(assert(port > 0)))
26+
}
27+
.unsafeRunSync()
28+
} finally { val _ = vertx.close() }
29+
}
30+
}

server/vertx-server/src/test/scala/sttp/tapir/server/vertx/VertxTestServerInterpreter.scala

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,18 @@ class VertxTestServerInterpreter(vertx: Vertx)
2828
route: Router => Route,
2929
gracefulShutdownTimeout: Option[FiniteDuration]
3030
): Resource[IO, Port] = {
31-
val router = Router.router(vertx)
32-
// the maxFormAttributeSize must be higher than in ServerMultipartTests.maxContentLengthTests
33-
val server = vertx.createHttpServer(new HttpServerOptions().setPort(0).setMaxFormAttributeSize(100000)).requestHandler(router)
34-
val listenIO = vertxFutureToIo(server.listen(0))
35-
route(router)
31+
// The server is created inside the Resource's acquire (not eagerly), so that the Resource can be safely re-run:
32+
// the flaky-WebSocket-test retry re-uses this Resource, and Vert.x's CleanableHttpServer.listen throws
33+
// IllegalStateException if listen() is called a second time on the same server instance.
34+
val listenIO = IO
35+
.delay {
36+
val router = Router.router(vertx)
37+
// the maxFormAttributeSize must be higher than in ServerMultipartTests.maxContentLengthTests
38+
val server = vertx.createHttpServer(new HttpServerOptions().setPort(0).setMaxFormAttributeSize(100000)).requestHandler(router)
39+
val _ = route(router)
40+
server
41+
}
42+
.flatMap(server => vertxFutureToIo(server.listen(0)))
3643
// Vertx doesn't offer graceful shutdown with timeout OOTB
3744
Resource.make(listenIO)(s => vertxFutureToIo(s.close()).void).map(s => s.actualPort())
3845
}

server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxTestServerInterpreter.scala

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,18 @@ class ZioVertxTestServerInterpreter(vertx: Vertx)
2727
route: Router => Route,
2828
gracefulShutdownTimeout: Option[FiniteDuration]
2929
): Resource[IO, Port] = {
30-
val router = Router.router(vertx)
31-
// the maxFormAttributeSize must be higher than in ServerMultipartTests.maxContentLengthTests
32-
val server = vertx.createHttpServer(new HttpServerOptions().setPort(0).setMaxFormAttributeSize(100000)).requestHandler(router)
33-
route(router)
34-
35-
val listenIO = VertxTestServerInterpreter.vertxFutureToIo(server.listen(0))
30+
// The server is created inside the Resource's acquire (not eagerly), so that the Resource can be safely re-run:
31+
// the flaky-WebSocket-test retry re-uses this Resource, and Vert.x's CleanableHttpServer.listen throws
32+
// IllegalStateException if listen() is called a second time on the same server instance.
33+
val listenIO = IO
34+
.delay {
35+
val router = Router.router(vertx)
36+
// the maxFormAttributeSize must be higher than in ServerMultipartTests.maxContentLengthTests
37+
val server = vertx.createHttpServer(new HttpServerOptions().setPort(0).setMaxFormAttributeSize(100000)).requestHandler(router)
38+
val _ = route(router)
39+
server
40+
}
41+
.flatMap(server => VertxTestServerInterpreter.vertxFutureToIo(server.listen(0)))
3642
// Vertx doesn't offer graceful shutdown with timeout OOTB
3743
Resource.make(listenIO)(server => VertxTestServerInterpreter.vertxFutureToIo(server.close).void).map(_.actualPort())
3844
}

0 commit comments

Comments
 (0)