package tgsig import ( "sync" "sync/atomic" "testing" "time" "dev.narayana.im/narayana/telegabber/calls/signaling" "gotgcalls/ntgcalls" "github.com/zelenin/go-tdlib/client" ) // concurrent-safe variant of fakeNtg; vanilla fakeNtg appends to slices // without locking, unusable under -race type raceNtg struct { mu sync.Mutex onSignal ntgcalls.SignalCallback onConnCh ntgcalls.ConnectionChangeCallback stopCount atomic.Int64 signalSent atomic.Int64 } func (f *raceNtg) OnSignal(cb ntgcalls.SignalCallback) { f.mu.Lock() f.onSignal = cb f.mu.Unlock() } func (f *raceNtg) OnConnectionChange(cb ntgcalls.ConnectionChangeCallback) { f.mu.Lock() f.onConnCh = cb f.mu.Unlock() } func (f *raceNtg) CreateP2PCall(int64) error { return nil } func (f *raceNtg) SkipExchange(int64, []byte, bool) error { return nil } func (f *raceNtg) ConnectP2P(int64, []ntgcalls.RTCServer, []string, bool) error { return nil } func (f *raceNtg) SendSignalingData(int64, []byte) error { f.signalSent.Add(1) return nil } func (f *raceNtg) Stop(int64) error { f.stopCount.Add(1) return nil } // simulates ntgcalls' C++ thread reaching into Go func (f *raceNtg) fire(chatID int64, data []byte) { f.mu.Lock() cb := f.onSignal f.mu.Unlock() if cb != nil { cb(chatID, data) } } // concurrent-safe variant of fakeTdlib type raceTdlib struct { created atomic.Int64 accepted atomic.Int64 discarded atomic.Int64 signalSent atomic.Int64 nextCallID atomic.Int32 } func (f *raceTdlib) CreateCall(*client.CreateCallRequest) (*client.CallId, error) { f.created.Add(1) id := f.nextCallID.Add(1) return &client.CallId{Id: id}, nil } func (f *raceTdlib) AcceptCall(*client.AcceptCallRequest) (*client.Ok, error) { f.accepted.Add(1) return &client.Ok{}, nil } func (f *raceTdlib) DiscardCall(*client.DiscardCallRequest) (*client.Ok, error) { f.discarded.Add(1) return &client.Ok{}, nil } func (f *raceTdlib) SendCallSignalingData(*client.SendCallSignalingDataRequest) (*client.Ok, error) { f.signalSent.Add(1) return &client.Ok{}, nil } // many dispatchers in parallel with Adapter.Close; -race must not flag // the ntg/tdlib touch sites, post-Close dispatches must be skipped, and // every withCallContext must release its RLock or Close blocks forever func TestAdapter_NoRaceUnderConcurrentDispatchAndClose(t *testing.T) { tdlib := &raceTdlib{} ntg := &raceNtg{} mgr := NewManager(nil) a := New(tdlib, ntg, nil, "jid@gw", mgr) // pre-register a side so dispatchers find something userID := int64(42) callee := a.NewCallee(7, userID, false) cb := &callee.callBase bridgeSide := &recCaller{} calleeSide := &recCallee{} br := signaling.New(signaling.Config{ Caller: bridgeSide, Callee: calleeSide, Timers: noopTimers{}, }) cb.Bind(br) br.Start() br.Ringing() var wg sync.WaitGroup stop := make(chan struct{}) // Spammer 1: OnSignal callback (simulates ntgcalls C++ thread). wg.Add(1) go func() { defer wg.Done() for { select { case <-stop: return default: } ntg.fire(userID, []byte("data")) } }() // spammer 2: OnNewSignalingData (tdlib updateHandler) wg.Add(1) go func() { defer wg.Done() for { select { case <-stop: return default: } mgr.OnNewSignalingData("jid@gw", &client.UpdateNewCallSignalingData{ CallId: 7, Data: []byte("data"), }) } }() // spammer 3: callBase.Terminate (bridge tear-down attempts) wg.Add(1) go func() { defer wg.Done() for { select { case <-stop: return default: } cb.Terminate(signaling.ReasonHangup) } }() // let spammers warm up time.Sleep(20 * time.Millisecond) // close concurrently with the spammers closeDone := make(chan struct{}) go func() { a.Close() close(closeDone) }() select { case <-closeDone: case <-time.After(2 * time.Second): t.Fatal("Close blocked > 2s - a withCallContext somewhere is not releasing its RLock") } // snapshot counters at the moment Close returned postCloseSignals := tdlib.signalSent.Load() postCloseDiscards := tdlib.discarded.Load() postCloseNtgSignals := ntg.signalSent.Load() postCloseStops := ntg.stopCount.Load() // let spammers run a bit more - counters must stay flat time.Sleep(50 * time.Millisecond) close(stop) wg.Wait() if tdlib.signalSent.Load() != postCloseSignals { t.Errorf("tdlib.signalSent advanced after Close: %d -> %d", postCloseSignals, tdlib.signalSent.Load()) } if tdlib.discarded.Load() != postCloseDiscards { t.Errorf("tdlib.discarded advanced after Close: %d -> %d", postCloseDiscards, tdlib.discarded.Load()) } if ntg.signalSent.Load() != postCloseNtgSignals { t.Errorf("ntg.signalSent advanced after Close: %d -> %d", postCloseNtgSignals, ntg.signalSent.Load()) } if ntg.stopCount.Load() != postCloseStops { t.Errorf("ntg.stopCount advanced after Close: %d -> %d", postCloseStops, ntg.stopCount.Load()) } } // Close twice must not panic or race func TestAdapter_CloseIsIdempotent(t *testing.T) { tdlib := &raceTdlib{} ntg := &raceNtg{} a := New(tdlib, ntg, nil, "jid@gw", NewManager(nil)) var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() a.Close() }() } wg.Wait() }