mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 04:07:07 +00:00
217 lines
5.1 KiB
Go
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()
|
|
}
|