udp_linux_test.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. //go:build linux && !android && !e2e_testing
  2. package udp
  3. import (
  4. "errors"
  5. "fmt"
  6. "log/slog"
  7. "net"
  8. "net/netip"
  9. "os"
  10. "runtime"
  11. "sync/atomic"
  12. "testing"
  13. "time"
  14. "golang.org/x/sys/unix"
  15. )
  16. func testLogger() *slog.Logger {
  17. return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
  18. }
  19. // TestShutdownWakesAfterRx_Mechanism exercises the kernel quirk our teardown
  20. // relies on: once a socket has received a packet, shutdown(2) wakes a blocked
  21. // recvmmsg with n>=1/Len==0 (not n==0). recvmmsg must turn that into net.ErrClosed
  22. // once Close set closed, so a parked reader exits instead of spinning.
  23. func TestShutdownWakesAfterRx_Mechanism(t *testing.T) {
  24. c, err := NewListener(testLogger(), netip.MustParseAddr("127.0.0.1"), 0, true, 64)
  25. if err != nil {
  26. t.Fatalf("NewListener: %v", err)
  27. }
  28. sc := c.(*StdConn)
  29. addr, err := sc.LocalAddr()
  30. if err != nil {
  31. t.Fatalf("LocalAddr: %v", err)
  32. }
  33. msgs, _, _ := sc.PrepareRawMessages(sc.batch)
  34. // Receive a real packet so the socket has carried data.
  35. send, err := net.Dial("udp", addr.String())
  36. if err != nil {
  37. t.Fatalf("dial: %v", err)
  38. }
  39. if _, err := send.Write([]byte("hello")); err != nil {
  40. t.Fatalf("write: %v", err)
  41. }
  42. time.Sleep(50 * time.Millisecond)
  43. n, err := sc.recvmmsg(msgs)
  44. t.Logf("drain of real packet: n=%d err=%v msgs[0].Len=%d", n, err, msgs[0].Len)
  45. _ = send.Close()
  46. // Block a reader on the now-empty queue, then tear down as Close() does.
  47. // recvmmsg must return net.ErrClosed (not hang, not spin) even post-rx.
  48. done := make(chan error, 1)
  49. go func() {
  50. _, err := sc.recvmmsg(msgs)
  51. done <- err
  52. }()
  53. time.Sleep(150 * time.Millisecond) // let it park in recvmmsg
  54. sc.closed.Store(true)
  55. if serr := unix.Shutdown(sc.sysFd, unix.SHUT_RDWR); serr != nil {
  56. t.Logf("shutdown returned %v (expected ENOTCONN on unconnected UDP)", serr)
  57. }
  58. select {
  59. case err := <-done:
  60. if !errors.Is(err, net.ErrClosed) {
  61. t.Errorf("recvmmsg after post-rx shutdown returned %v, want net.ErrClosed", err)
  62. }
  63. case <-time.After(2 * time.Second):
  64. t.Fatalf("HANG: recvmmsg did not return after shutdown following a received packet")
  65. }
  66. _ = unix.Close(sc.sysFd)
  67. }
  68. // TestListenOutTeardown_TrafficPatterns reproduces the field report: a blocking
  69. // reader must tear down cleanly on Close() regardless of what the socket has
  70. // carried. The three cases the report called out:
  71. //
  72. // no traffic ever -> works (shutdown wakes recvmmsg with n==0)
  73. // ping once, then idle -> historically HUNG: once the socket has received a
  74. // packet, shutdown(2) wakes recvmmsg with n>=1/Len==0,
  75. // which an n==0-only teardown check misses
  76. // continuous traffic -> works (a real packet is always arriving)
  77. //
  78. // All three must return within the deadline; a hang dumps goroutines so the
  79. // stuck reader is visible.
  80. func TestListenOutTeardown_TrafficPatterns(t *testing.T) {
  81. cases := []struct {
  82. name string
  83. traffic func(send net.Conn, stop <-chan struct{})
  84. }{
  85. {"no_traffic_ever", func(net.Conn, <-chan struct{}) {}},
  86. {"ping_once_then_idle", func(send net.Conn, _ <-chan struct{}) {
  87. _, _ = send.Write([]byte("hello"))
  88. }},
  89. {"continuous", func(send net.Conn, stop <-chan struct{}) {
  90. for {
  91. select {
  92. case <-stop:
  93. return
  94. default:
  95. _, _ = send.Write([]byte("hello"))
  96. time.Sleep(2 * time.Millisecond)
  97. }
  98. }
  99. }},
  100. }
  101. // batch 1 exercises the recvmsg path, batch 64 the recvmmsg path; both must
  102. // tear down cleanly.
  103. for _, batch := range []int{1, 64} {
  104. for _, tc := range cases {
  105. t.Run(fmt.Sprintf("batch%d/%s", batch, tc.name), func(t *testing.T) {
  106. runTeardownCase(t, batch, tc.name, tc.traffic)
  107. })
  108. }
  109. }
  110. }
  111. func runTeardownCase(t *testing.T, batch int, name string, traffic func(send net.Conn, stop <-chan struct{})) {
  112. c, err := NewListener(testLogger(), netip.MustParseAddr("127.0.0.1"), 0, true, batch)
  113. if err != nil {
  114. t.Fatalf("NewListener: %v", err)
  115. }
  116. sc := c.(*StdConn)
  117. addr, err := sc.LocalAddr()
  118. if err != nil {
  119. t.Fatalf("LocalAddr: %v", err)
  120. }
  121. var received atomic.Int64
  122. loopDone := make(chan error, 1)
  123. go func() {
  124. loopDone <- sc.ListenOut(func(netip.AddrPort, []byte) {
  125. received.Add(1)
  126. })
  127. }()
  128. send, err := net.Dial("udp", addr.String())
  129. if err != nil {
  130. t.Fatalf("dial: %v", err)
  131. }
  132. defer send.Close()
  133. stop := make(chan struct{})
  134. trafficDone := make(chan struct{})
  135. go func() {
  136. traffic(send, stop)
  137. close(trafficDone)
  138. }()
  139. // Let the pattern run and, for the idle case, the reader park again on an
  140. // empty queue with the socket already having received a packet.
  141. time.Sleep(500 * time.Millisecond)
  142. start := time.Now()
  143. if err := sc.Close(); err != nil {
  144. t.Fatalf("Close: %v", err)
  145. }
  146. close(stop)
  147. select {
  148. case err := <-loopDone:
  149. // Clean teardown surfaces as net.ErrClosed (propagated like the other
  150. // platforms); the caller absorbs it via its closed flag.
  151. if err != nil && !errors.Is(err, net.ErrClosed) {
  152. t.Fatalf("%s: ListenOut returned unexpected error on teardown: %v", name, err)
  153. }
  154. t.Logf("%s: closed in %v (received %d packets)", name, time.Since(start), received.Load())
  155. case <-time.After(3 * time.Second):
  156. buf := make([]byte, 1<<20)
  157. n := runtime.Stack(buf, true)
  158. t.Fatalf("%s: HANG, ListenOut did not return within 3s of Close\n%s", name, buf[:n])
  159. }
  160. <-trafficDone
  161. }