@@ -2,6 +2,9 @@ package pool
22
33import (
44 "context"
5+ "runtime"
6+ "sync"
7+ "sync/atomic"
58 "testing"
69 "time"
710
@@ -25,14 +28,15 @@ func (m *mockConn) IsArchiveNode() bool {
2528}
2629
2730func (m * mockConn ) ID () int {
28- return 0
31+ return m . id
2932}
3033
3134func (m * mockConn ) MasterHead () ton.BlockIDExt {
3235 return ton.BlockIDExt {BlockID : ton.BlockID {Seqno : m .seqno }}
3336}
3437
3538func (m * mockConn ) SetMasterHead (ext ton.BlockIDExt ) {
39+ m .seqno = ext .Seqno
3640}
3741
3842func (m * mockConn ) MasterSeqno () uint32 {
@@ -156,8 +160,174 @@ func TestConnPool_updateBest(t *testing.T) {
156160 p .updateBest ()
157161 c := p .bestConnection ().(* mockConn )
158162 if tt .wantID != c .id {
159- t .Fatalf ("want connection id: %v , got: %v " , tt .wantID , c .id )
163+ t .Fatalf ("expected connection id %d , got %d " , tt .wantID , c .id )
160164 }
161165 })
162166 }
163167}
168+
169+ type mockConnWithClient struct {
170+ * connection
171+ mockIsOK bool
172+ }
173+
174+ func (m * mockConnWithClient ) IsOK () bool {
175+ return m .mockIsOK
176+ }
177+
178+ func TestWaitMasterchainSeqno (t * testing.T ) {
179+ t .Run ("timer reuse prevents memory leak" , func (t * testing.T ) {
180+ pool := New (BestPingStrategy )
181+
182+ ctx , cancel := context .WithCancel (context .Background ())
183+ defer cancel ()
184+
185+ go pool .Run (ctx )
186+
187+ testConn := & connection {
188+ id : 1 ,
189+ masterHeadUpdatedCh : pool .masterHeadUpdatedCh ,
190+ }
191+ testConn .SetMasterHead (ton.BlockIDExt {BlockID : ton.BlockID {Seqno : 1 }})
192+
193+ wrappedConn := & mockConnWithClient {
194+ connection : testConn ,
195+ mockIsOK : true ,
196+ }
197+
198+ pool .mu .Lock ()
199+ pool .conns = []conn {wrappedConn }
200+ pool .bestConn = wrappedConn
201+ pool .mu .Unlock ()
202+
203+ go func () {
204+ for i := uint32 (2 ); i < 50 ; i ++ {
205+ time .Sleep (10 * time .Millisecond )
206+ testConn .SetMasterHead (ton.BlockIDExt {BlockID : ton.BlockID {Seqno : i }})
207+ }
208+ }()
209+
210+ var m1 , m2 runtime.MemStats
211+ runtime .GC ()
212+ runtime .ReadMemStats (& m1 )
213+
214+ for i := uint32 (2 ); i < 45 ; i ++ {
215+ err := pool .WaitMasterchainSeqno (ctx , i , 500 * time .Millisecond )
216+ if err != nil {
217+ t .Fatalf ("failed to wait for seqno %d: %v" , i , err )
218+ }
219+ }
220+
221+ runtime .GC ()
222+ runtime .ReadMemStats (& m2 )
223+
224+ allocatedKB := (m2 .TotalAlloc - m1 .TotalAlloc ) / 1024
225+ if allocatedKB > 500 {
226+ t .Errorf ("excessive memory allocation: %d KB (expected < 500 KB with timer reuse)" , allocatedKB )
227+ }
228+ })
229+
230+ t .Run ("timeout when seqno not reached" , func (t * testing.T ) {
231+ pool := New (BestPingStrategy )
232+
233+ ctx := context .Background ()
234+ go pool .Run (ctx )
235+
236+ testConn := & connection {
237+ id : 1 ,
238+ masterHeadUpdatedCh : pool .masterHeadUpdatedCh ,
239+ }
240+ testConn .SetMasterHead (ton.BlockIDExt {BlockID : ton.BlockID {Seqno : 1 }})
241+
242+ wrappedConn := & mockConnWithClient {
243+ connection : testConn ,
244+ mockIsOK : true ,
245+ }
246+
247+ pool .mu .Lock ()
248+ pool .conns = []conn {wrappedConn }
249+ pool .bestConn = wrappedConn
250+ pool .mu .Unlock ()
251+
252+ err := pool .WaitMasterchainSeqno (ctx , 9999 , 100 * time .Millisecond )
253+ if err == nil {
254+ t .Fatal ("expected timeout error, got nil" )
255+ }
256+ if err .Error () != "timeout" {
257+ t .Fatalf ("expected 'timeout' error, got %q" , err .Error ())
258+ }
259+ })
260+ }
261+
262+ func TestMultipleConnectionsChannelHandling (t * testing.T ) {
263+ pool := New (BestPingStrategy )
264+
265+ ctx , cancel := context .WithCancel (context .Background ())
266+ defer cancel ()
267+
268+ go pool .Run (ctx )
269+
270+ var wrappedConns []* mockConnWithClient
271+ for i := 0 ; i < 5 ; i ++ {
272+ c := & connection {
273+ id : i ,
274+ masterHeadUpdatedCh : pool .masterHeadUpdatedCh ,
275+ }
276+ wrapped := & mockConnWithClient {
277+ connection : c ,
278+ mockIsOK : true ,
279+ }
280+ wrappedConns = append (wrappedConns , wrapped )
281+ }
282+
283+ pool .mu .Lock ()
284+ pool .conns = make ([]conn , len (wrappedConns ))
285+ for i , c := range wrappedConns {
286+ pool .conns [i ] = c
287+ }
288+ pool .bestConn = wrappedConns [0 ]
289+ pool .mu .Unlock ()
290+
291+ var blocked atomic.Int32
292+ var wg sync.WaitGroup
293+
294+ for idx , wrapped := range wrappedConns {
295+ wg .Add (1 )
296+ go func (connID int , conn * connection ) {
297+ defer wg .Done ()
298+
299+ for seqno := uint32 (1 ); seqno <= 20 ; seqno ++ {
300+ done := make (chan bool , 1 )
301+
302+ go func (s uint32 ) {
303+ conn .SetMasterHead (ton.BlockIDExt {BlockID : ton.BlockID {Seqno : s }})
304+ done <- true
305+ }(seqno )
306+
307+ select {
308+ case <- done :
309+ case <- time .After (100 * time .Millisecond ):
310+ blocked .Add (1 )
311+ return
312+ }
313+
314+ time .Sleep (5 * time .Millisecond )
315+ }
316+ }(idx , wrapped .connection )
317+ }
318+
319+ wg .Wait ()
320+
321+ if blocked .Load () > 0 {
322+ t .Errorf ("failed to send updates: %d connection(s) blocked (channel overflow)" , blocked .Load ())
323+ }
324+ }
325+
326+ func TestChannelBufferCapacity (t * testing.T ) {
327+ pool := New (BestPingStrategy )
328+
329+ capacity := cap (pool .masterHeadUpdatedCh )
330+ if capacity < 10 {
331+ t .Errorf ("insufficient channel buffer: got %d, expected >= 10" , capacity )
332+ }
333+ }
0 commit comments