Skip to content

Commit 9785098

Browse files
committed
Add more test
1 parent 2fcf7d8 commit 9785098

1 file changed

Lines changed: 161 additions & 14 deletions

File tree

pulsar/internal/connection_test.go

Lines changed: 161 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,177 @@ package internal
1919

2020
import (
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

Comments
 (0)