telegabber/calls/signaling/tgsig/adapter_race_test.go
2026-05-25 06:27:20 -07:00

217 lines
5.1 KiB
Go

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()
}