Skip to content

fix(server): UDP readers exit when the server is closed - #57

Merged
TeoSlayer merged 3 commits into
mainfrom
fix/reader-exit-on-close
Oct 1, 2026
Merged

TeoSlayer merged 3 commits into
mainfrom
fix/reader-exit-on-close

Conversation

@TeoSlayer

Copy link
Copy Markdown
Contributor

Problem

readLoopBatches detected a closed socket by comparing the inner error's text to "use of closed network connection". On Linux's recvmmsg path the error is wrapped twice:

read udp 127.0.0.1:55092: raw-read udp4 127.0.0.1:55092: use of closed network connection

so the comparison never matched. After Close, each of the 2 x NumCPU readers logged at Debug and retried its closed socket forever, which is a hot loop per core for the rest of the process. errors.Is(err, net.ErrClosed) is true for that error.

Measured on v0.2.9 (Linux arm64 container, 10 CPUs): 2 goroutines before ListenAndServe, 36 while running, 22 two seconds after Close.

This also affects web4's TestDaemonShutdownStopsGoroutines, which starts an in-process beacon. On a runner with 10 or more CPUs the 20 spinning readers exceed that test's baseline+15 allowance, so it fails (baseline=3, final=24). On CI's 4-CPU runner the 8 spinning readers still use up most of the allowance, which leaves little room to catch a real daemon leak.

Fix

  • Match net.ErrClosed with errors.Is.
  • Also end the loop on any read error once Close has begun (s.done).

Tests

TestCloseStopsReaders starts a server, closes it, and waits for the goroutine count to return to baseline.

  • Linux without the fix: FAIL goroutines still running after Close: base=2 running=36 now=22
  • Linux with the fix: PASS base=2 running=36 after Close=2
  • macOS: PASS

go test -race ./... passes on macOS arm64. On Linux arm64 (10 CPUs) the package passes some runs and fails others, on origin/main too, because of an existing data race: EnableCompatWSS (server.go:302) writes a field that dispatchRelay (server.go:900) reads, in TestEnableCompatWSS_HappyPath. Once the race detector trips, every test running concurrently is marked failed. That race is separate from this change and isn't fixed here. With the fix the package's Linux run also drops from 61-104 s to about 17 s, because closed test beacons no longer spin.

🤖 Generated with Claude Code

readLoopBatches recognised a closed socket by comparing the inner
error's text to "use of closed network connection". On the Linux
recvmmsg path the error is wrapped twice ("read udp ...: raw-read udp4
...: use of closed network connection"), so the comparison never
matched: after Close every reader (2 x NumCPU of them) logged at Debug
and retried the closed socket forever, a hot loop per core for the
rest of the process.

Match net.ErrClosed with errors.Is, and also end the loop on any read
error once Close has begun (s.done).

TestCloseStopsReaders starts a server, closes it and waits for the
goroutine count to return to its baseline. On Linux (10 CPUs, 20
readers) it fails without this change ("base=2 running=36 now=22")
and passes with it ("after Close=2").

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@TeoSlayer
TeoSlayer merged commit 267162d into main Oct 1, 2026
4 of 5 checks passed
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.

2 participants