Skip to content

fix(ci): stop the macOS editor-ON job hanging in the local socket tests - #660

Merged
Mosch0512 merged 6 commits into
sven-n:mainfrom
Mosch0512:fix/macos-local-socket-hang
Sep 29, 2026
Merged

Mosch0512 merged 6 commits into
sven-n:mainfrom
Mosch0512:fix/macos-local-socket-hang

Conversation

@Mosch0512

Copy link
Copy Markdown
Collaborator

Summary

On macOS with the control socket enabled, ctest hangs in Local socket bounds the unterminated tail, not a pipelined batch [core][local-socket] until CI cancels the job after six hours. The test's client sends 16 KiB chunks from the same thread that reads them, and the client was blocking. macOS gives an AF_UNIX stream an 8 KiB buffer (net.local.stream.sendspace), so send() waited for a read that could never start. Linux and Windows buffer more than one chunk, so they passed. ci.yml builds macOS without the editor, so these tests are not compiled there.

The transport's behaviour is unchanged; only the test client was wrong. Reviewed on the fork in Mosch0512#92.

The hang

  • The large-payload cases write through SendWhileServing, which sends what the socket takes and lets the connection read before sending the rest. It stops once the client has not got a byte out for five seconds, so a connection that stops reading fails the case instead of spinning.
  • Only ConnectForLargeWrites makes a client SendWhileServing accepts: non-blocking, so the mode cannot be forgotten, and with a 4 KiB send buffer, so Linux and Windows take the partial sends that only macOS took before.
  • The final line counts wait for the end of the stream with a deadline instead of assuming the last chunk is readable the moment send() returns.

Test coverage

  • The pacing case now reaches the read pause: it sends without serving until the writer is held off, checks that the buffered lines reached the pause mark and stopped within one read of it, then serves the rest and checks that every line arrives. Before, every pass drained all lines, so the pause never fired; with the pause disabled in the transport, the case now fails.

Shared code

  • Core/Platform/NonBlockingSocket.h holds Enable and WouldBlock, used by both LocalSocket.cpp and the test. The test previously had its own copies, and its WouldBlock missed EAGAIN and EINTR.

Time limits

  • mu_add_test gives every discovered test case a TIMEOUT of MU_TEST_CASE_TIMEOUT_SECONDS (120 s; the slowest case takes about a second), so a hanging case fails under its own name wherever ctest runs.
  • Each fork-ci.yml job has timeout-minutes: 90 (the slowest, Windows, takes about 35 minutes). That also covers doctest's test discovery during the build, restore and configure.

Testing

  • Reproduced the hang on Linux (WSL, Ubuntu 24.04) by shrinking the test client's SO_SNDBUF to macOS's 8 KiB. All 9 local-socket cases pass there, with the default buffer and on Windows (MSVC).
  • All Fork CI jobs pass on fix(ci): stop the macOS editor-ON job hanging in the local socket tests Mosch0512/MuMain#92, including macOS arm64 editor ON (each local-socket case about 0.01 s).
  • Removing the read pause from the transport makes the pacing case fail.
  • In a small CMake project using the vendored doctest module, a hanging case with the TIMEOUT property is stopped and reported by name.
  • clang-format 21.1.8 on the changed lines and cppcheck with the Quality Gates flags are clean.

Related issues

Closes #633

Checklist

  • I have read and followed docs/CODING_RULES.md.
  • I have built the project locally (see docs/build/README.md). The local-socket tests locally on Linux and Windows; the full project in Fork CI on Windows, Linux and macOS.
  • Changes are focused on one concern; the diff is as small as it reasonably can be.

The pipelined-batch and pacing cases send 16 KiB chunks from the thread
that also reads them. The client was blocking, so a chunk larger than the
kernel's socket buffer waited for a read that could not start. Linux and
Windows buffer more than a chunk; macOS gives an AF_UNIX stream 8 KiB,
so the macOS editor-ON job hung in the first of these cases until GitHub
cancelled it after six hours.

Those two cases now use a non-blocking client that sends what fits and
lets the connection read before sending the rest. Reproduced on Linux by
shrinking the client's send buffer to 8 KiB; all nine cases pass there
and on Windows.
A test that hangs held its job until GitHub cancelled it after six hours,
and the run showed only "cancelled", not which test hung. ctest now stops
each test after five minutes and reports it as a failure under its name.
The slowest test takes about 30 seconds.
The socket test had its own copy of the transport's SetNonBlocking and a
narrower WouldBlock that missed EAGAIN and EINTR, so it failed a send the
transport would have retried. Both now live in NonBlockingSocket.h and
the transport and the test call the same code.
The send-then-read loop was written out three times, and after the macOS
fix none of the copies required progress: a pass that sent nothing was
accepted, so a connection that stopped reading kept the loop spinning at
full CPU until ctest stopped it, or forever outside ctest.

SendWhileServing replaces all three. It gives up once the client has not
got a byte out for five seconds, sends from a string_view instead of
copying each chunk, and either takes the lines as they arrive or leaves
them buffered. The final count waits for the end of the stream with a
deadline instead of assuming the last chunk is readable the moment send()
returns.

Only ConnectForLargeWrites makes a client that SendWhileServing accepts.
It is non-blocking, so forgetting the mode no longer compiles, and it asks
for a send buffer smaller than one chunk, so Linux and Windows take the
partial sends that only macOS took before.

SendAll made a single send() call and is now SendOnce, and the comments
that still called the client blocking are updated.
Every pass drained all lines, so the inbox never held more than a chunk
and the read pause never fired: a change that removed it would still
have passed.

The case now sends without serving first, like a frame loop that has
fallen behind, until the writer is held off. The buffered lines must
reach the pause mark and stop within one read of it, with the connection
still open. Serving then resumes, and the rest of the batch has to arrive
in full. With the pause disabled in the transport the case fails.
ctest --timeout covered only the three Fork CI jobs: ci.yml's test runs
and local runs had no limit, and the 300 was written out three times.
mu_add_test now gives every discovered case a TIMEOUT from one named
setting, MU_TEST_CASE_TIMEOUT_SECONDS (120 s; the slowest case takes
about a second), so the limit applies wherever ctest runs.

A per-test limit does not cover the rest of a job: doctest's discovery
runs each test binary during the build with no timeout, and a restore or
configure can stall too. Each Fork CI job now has timeout-minutes: 90,
over twice the slowest job (Windows, about 35 minutes).
@Mosch0512
Mosch0512 merged commit 4b3da1d into sven-n:main Sep 29, 2026
3 checks passed
@Mosch0512
Mosch0512 deleted the fix/macos-local-socket-hang branch September 29, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Local socket tests hang on macOS (blocking send with small AF_UNIX buffers)

1 participant