From 4efae1658a875c2acdff53944130e6683aac78dd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 16:59:04 +0000 Subject: [PATCH] Keep a stub WEBrick from hanging the suite on Ruby 4.0 CI jobs on Ruby 4.0 intermittently produced no output after an `ember build` until the 15-minute cap cancelled them (e.g. run 33876168453, jobs `build (4.0, 8.1, 7.0.0)` and `build (4.0, main, 7.0.0)`); no job on Ruby 3.3 or 3.4 ever did. The hang is in `spec/lib/ember_cli/dev_server_spec.rb`: `NullServer#listen` starts WEBrick in a new thread and returns immediately, and the example's `after` hook then calls `GenericServer#shutdown` and joins the thread. `shutdown` is a no-op until `#start` has set up its shutdown pipe, so when the thread has not run yet the server boots afterwards and loops in `IO.select` forever, and the join never returns. On Ruby 4.0 a new thread usually has not run by then: a standalone loop of the same steps hung 187 of 300 times on Ruby 4.0.6 and 0 of 300 times on Ruby 3.3.6, and the spec file alone hung 23 of 25 runs on Ruby 4.0.6 before this change and 0 of 25 after it. The `:js` timeout added in #668 could not catch this because these are plain examples, and their progress dots never reached the log: the runner only flushes complete lines, which is why the jobs looked stuck right after the build output. Wait until WEBrick reports `:Running` before `listen` returns, so that `shutdown` always reaches a server loop that can be stopped. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FRn3BbAGhG1oWGbNkSHkvA --- spec/lib/ember_cli/dev_server_spec.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/lib/ember_cli/dev_server_spec.rb b/spec/lib/ember_cli/dev_server_spec.rb index c83318da..d6e7172a 100644 --- a/spec/lib/ember_cli/dev_server_spec.rb +++ b/spec/lib/ember_cli/dev_server_spec.rb @@ -237,6 +237,7 @@ def listen(host, port, status: 200, body: "") end @thread = Thread.new { @server.start } + wait_until_running @port end @@ -245,6 +246,25 @@ def shutdown @server&.shutdown @thread&.join end + + private + + # Not `shutdown` straight after `Thread.new`: `GenericServer#shutdown` is + # a no-op until `#start` has set up its shutdown pipe, and a new thread + # is not guaranteed to have run by the time the example finishes (on + # Ruby 4.0 it usually has not). Shutting down such a server leaves it + # looping forever, and the join in `#shutdown` never returns. + def wait_until_running + deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5 + + until @server.status == :Running + if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline + raise "The stub development server did not start within 5 seconds" + end + + sleep 0.01 + end + end end def null_server