@@ -19,30 +19,177 @@ package internal
1919
2020import (
2121 "context"
22- "sync/atomic"
22+ "net/url"
23+ "sync"
2324 "testing"
25+ "time"
2426
27+ pb "github.com/apache/pulsar-client-go/pulsar/internal/pulsar_proto"
2528 "github.com/apache/pulsar-client-go/pulsar/log"
29+ "github.com/prometheus/client_golang/prometheus"
2630 "github.com/stretchr/testify/assert"
31+ "github.com/stretchr/testify/require"
2732)
2833
29- func TestConnectionWriteDataShouldNotEnqueueWhenStateClosed (t * testing.T ) {
30- released := atomic.Int64 {}
31- pool := NewBufferPool ()
32- buf := pool .GetBuffer (8 )
34+ func TestConnectionRejectRequestsAfterClose (t * testing.T ) {
35+ c := newTestConnection ()
36+
37+ c .Close ()
38+
39+ assertConnectionClosed (t , c )
40+ }
41+
42+ func TestConnectionSendRequestRaceWithClose (t * testing.T ) {
43+ // Regression test for concurrent Add/Wait on WaitGroup during Close.
44+ //
45+ // Without proper synchronization between:
46+ // - registerIncomingRequest() calling WaitGroup.Add(1)
47+ // - Close() calling WaitGroup.Wait()
48+ //
49+ // Go 1.25+ may panic with:
50+ //
51+ // sync: WaitGroup is reused before previous Wait has returned
52+ //
53+ // This test continuously issues requests while concurrently closing
54+ // the connection to maximize the Add/Wait overlap window.
55+
56+ const (
57+ numTrials = 20
58+ numGoroutines = 100
59+ )
60+
61+ for trial := 0 ; trial < numTrials ; trial ++ {
62+ c := newTestConnection ()
63+
64+ startCh := make (chan struct {})
65+ stopCh := make (chan struct {})
66+
67+ panicCh := make (chan any , numGoroutines )
68+
69+ var wg sync.WaitGroup
70+
71+ for i := 0 ; i < numGoroutines ; i ++ {
72+ wg .Add (1 )
73+
74+ go func (idx int ) {
75+ defer wg .Done ()
76+
77+ defer func () {
78+ if r := recover (); r != nil {
79+ panicCh <- r
80+ }
81+ }()
82+
83+ <- startCh
84+
85+ for {
86+ select {
87+ case <- stopCh :
88+ return
89+ default :
90+ }
91+
92+ if idx % 2 == 0 {
93+ c .SendRequest (
94+ uint64 (idx ),
95+ & pb.BaseCommand {},
96+ func (* pb.BaseCommand , error ) {},
97+ )
98+ } else {
99+ _ = c .SendRequestNoWait (& pb.BaseCommand {})
100+ }
101+ }
102+ }(i )
103+ }
104+
105+ // Start all concurrent request producers together.
106+ close (startCh )
107+
108+ // Allow requests to race with Close().
109+ time .Sleep (10 * time .Millisecond )
110+
111+ c .Close ()
112+
113+ close (stopCh )
114+
115+ wg .Wait ()
116+
117+ select {
118+ case p := <- panicCh :
119+ t .Fatalf ("unexpected panic during concurrent Close: %v" , p )
120+ default :
121+ }
122+
123+ assertConnectionClosed (t , c )
124+ }
125+ }
126+
127+ func assertConnectionClosed (t * testing.T , c * connection ) {
128+ t .Helper ()
129+
130+ callbackCh := make (chan error , 1 )
131+
132+ c .SendRequest (
133+ 999 ,
134+ & pb.BaseCommand {},
135+ func (cmd * pb.BaseCommand , err error ) {
136+ callbackCh <- err
137+ },
138+ )
139+
140+ select {
141+ case err := <- callbackCh :
142+ assert .Error (t , err )
143+ case <- time .After (time .Second ):
144+ t .Fatal ("SendRequest callback was not invoked" )
145+ }
146+
147+ assert .Error (t , c .SendRequestNoWait (& pb.BaseCommand {}))
148+
149+ released := make (chan struct {}, 1 )
150+
151+ buf := NewBufferPool ().GetBuffer (8 )
33152 buf .SetReleaseCallback (func () {
34- released . Add ( 1 )
153+ released <- struct {}{}
35154 })
36155
37- c := & connection {
38- log : log .DefaultNopLogger (),
39- closeCh : make (chan struct {}),
40- writeRequestsCh : make (chan * dataRequest , 1 ),
156+ c .WriteData (context .Background (), buf )
157+
158+ select {
159+ case <- released :
160+ case <- time .After (time .Second ):
161+ t .Fatal ("WriteData buffer was not released" )
41162 }
42- c . setStateClosed ()
163+ }
43164
44- c .WriteData (context .Background (), buf )
165+ func newTestConnection () * connection {
166+ opts := connectionOptions {
167+ logicalAddr : & url.URL {Host : "test:6650" },
168+ physicalAddr : & url.URL {Host : "test:6650" },
169+ connectionTimeout : time .Second ,
170+ keepAliveInterval : 30 * time .Second ,
171+ logger : log .DefaultNopLogger (),
172+ metrics : newMockMetrics (),
173+ }
45174
46- assert .Equal (t , 0 , len (c .writeRequestsCh ))
47- assert .EqualValues (t , 1 , released .Load ())
175+ c := newConnection (opts )
176+
177+ require .NotNil (& testing.T {}, c )
178+
179+ return c
180+ }
181+
182+ // newMockMetrics creates Metrics with real prometheus counters for testing.
183+ func newMockMetrics () * Metrics {
184+ return & Metrics {
185+ ConnectionsClosed : prometheus .NewCounter (prometheus.CounterOpts {
186+ Name : "test_connections_closed" ,
187+ }),
188+ ConnectionsEstablishmentErrors : prometheus .NewCounter (prometheus.CounterOpts {
189+ Name : "test_connections_establishment_errors" ,
190+ }),
191+ ConnectionsHandshakeErrors : prometheus .NewCounter (prometheus.CounterOpts {
192+ Name : "test_connections_handshake_errors" ,
193+ }),
194+ }
48195}
0 commit comments