Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/ed25519"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
Expand Down Expand Up @@ -520,9 +521,19 @@ func (s *Server) readLoopBatches(conn *net.UDPConn) (err error, done bool) {
for {
n, rerr := pc.ReadBatch(msgs, 0)
if rerr != nil {
if opErr, ok := rerr.(*net.OpError); ok && opErr.Err.Error() == "use of closed network connection" {
// A closed socket, or a server being closed, ends the loop. On
// the Linux recvmmsg path the error is wrapped twice ("read udp
// ...: raw-read udp4 ...: use of closed network connection"),
// so comparing the inner error's text never matched there and
// every reader spun on its closed socket after Close.
if errors.Is(rerr, net.ErrClosed) {
return nil, true
}
select {
case <-s.done:
return nil, true
default:
}
slog.Debug("beacon read batch error", "err", rerr)
continue
}
Expand Down
46 changes: 46 additions & 0 deletions zz_close_readers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

package beacon

import (
"runtime"
"testing"
"time"
)

// TestCloseStopsReaders: Close ends every UDP reader. On Linux the
// recvmmsg path wraps the closed-socket error twice, which the old check
// (the inner error's exact text) never matched, so all 2脳NumCPU readers
// kept spinning on their closed sockets for the life of the process.
func TestCloseStopsReaders(t *testing.T) {
runtime.GC()
base := runtime.NumGoroutine()

s := New()
go func() { _ = s.ListenAndServe("127.0.0.1:0") }()
select {
case <-s.Ready():
case <-time.After(5 * time.Second):
t.Fatal("beacon did not start")
}
time.Sleep(100 * time.Millisecond)
running := runtime.NumGoroutine()

if err := s.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
deadline := time.Now().Add(5 * time.Second)
for {
n := runtime.NumGoroutine()
if n <= base+2 {
t.Logf("goroutines: base=%d running=%d after Close=%d", base, running, n)
return
}
if time.Now().After(deadline) {
buf := make([]byte, 1<<16)
t.Fatalf("goroutines still running after Close: base=%d running=%d now=%d\n%s",
base, running, n, buf[:runtime.Stack(buf, true)])
}
time.Sleep(20 * time.Millisecond)
}
}
Loading