@@ -2,8 +2,11 @@ package notifier
22
33import (
44 "context"
5+ "fmt"
6+ "math/rand"
57 "net/netip"
68 "sort"
9+ "sync"
710 "testing"
811 "time"
912
@@ -263,3 +266,78 @@ func TestBatcher(t *testing.T) {
263266 })
264267 }
265268}
269+
270+ // TestIsLikelyConnectedRaceCondition tests for a race condition in IsLikelyConnected
271+ // Multiple goroutines calling AddNode and RemoveNode cause panics when trying to
272+ // close a channel that was already closed, which can happen when a node changes
273+ // network transport quickly (eg mobile->wifi) and reconnects whilst also disconnecting
274+ func TestIsLikelyConnectedRaceCondition (t * testing.T ) {
275+ // mock config for the notifier
276+ cfg := & types.Config {
277+ Tuning : types.Tuning {
278+ NotifierSendTimeout : 1 * time .Second ,
279+ BatchChangeDelay : 1 * time .Second ,
280+ NodeMapSessionBufferedChanSize : 30 ,
281+ },
282+ }
283+
284+ notifier := NewNotifier (cfg )
285+ defer notifier .Close ()
286+
287+ nodeID := types .NodeID (1 )
288+ updateChan := make (chan types.StateUpdate , 10 )
289+
290+ var wg sync.WaitGroup
291+
292+ // Number of goroutines to spawn for concurrent access
293+ concurrentAccessors := 100
294+ iterations := 100
295+
296+ // Add node to notifier
297+ notifier .AddNode (nodeID , updateChan )
298+
299+ // Track errors
300+ errChan := make (chan string , concurrentAccessors * iterations )
301+
302+ // Start goroutines to cause a race
303+ wg .Add (concurrentAccessors )
304+ for i := 0 ; i < concurrentAccessors ; i ++ {
305+ go func (routineID int ) {
306+ defer wg .Done ()
307+
308+ for j := 0 ; j < iterations ; j ++ {
309+ // Simulate race by having some goroutines check IsLikelyConnected
310+ // while others add/remove the node
311+ if routineID % 3 == 0 {
312+ // This goroutine checks connection status
313+ isConnected := notifier .IsLikelyConnected (nodeID )
314+ if isConnected != true && isConnected != false {
315+ errChan <- fmt .Sprintf ("Invalid connection status: %v" , isConnected )
316+ }
317+ } else if routineID % 3 == 1 {
318+ // This goroutine removes the node
319+ notifier .RemoveNode (nodeID , updateChan )
320+ } else {
321+ // This goroutine adds the node back
322+ notifier .AddNode (nodeID , updateChan )
323+ }
324+
325+ // Small random delay to increase chance of races
326+ time .Sleep (time .Duration (rand .Intn (100 )) * time .Microsecond )
327+ }
328+ }(i )
329+ }
330+
331+ wg .Wait ()
332+ close (errChan )
333+
334+ // Collate errors
335+ var errors []string
336+ for err := range errChan {
337+ errors = append (errors , err )
338+ }
339+
340+ if len (errors ) > 0 {
341+ t .Errorf ("Detected %d race condition errors: %v" , len (errors ), errors )
342+ }
343+ }
0 commit comments