mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-07 14:09:07 +02:00
* [client] Coalesce peer list change notifications to the mobile listener Every peer state change spawned a goroutine to call the platform listener. During a reconnect storm this pinned hundreds of OS threads in JNI and let the UI call back into the engine from each of them. Deliver peer list changes from a single goroutine per listener and collapse pending changes into the latest count. * [client] Cover a pending wake-up when the peer list deliverer is replaced The replacement test waited for the old callback to finish before swapping listeners, so it never exercised the stop check that runs after a wake-up. Block the old callback, queue a peer list change and swap while it is blocked, then assert the old listener never sees the new count. * [client] Signal peer list deliverer exit and wait for it in the test The replacement test sampled the old listener after a fixed sleep, so a late stale delivery could slip past it. Close a done channel when the deliverer goroutine returns and let the test wait on it instead. * [client] Drop the test-only peer list deliverer exit channel The done channel was only read by the replacement test. Production code cannot wait on it, since joining the deliverer would block on a mobile callback. The tests now drive the deliverer loop directly and check that setListener and removeListener close its stop channel.
273 lines
6.4 KiB
Go
273 lines
6.4 KiB
Go
package peer
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type mocListener struct {
|
|
lastState ClientState
|
|
wg sync.WaitGroup
|
|
peersWg sync.WaitGroup
|
|
peers int
|
|
}
|
|
|
|
func (l *mocListener) OnConnected() {
|
|
l.lastState = ClientStateConnected
|
|
l.wg.Done()
|
|
}
|
|
func (l *mocListener) OnDisconnected() {
|
|
l.lastState = ClientStateDisconnected
|
|
l.wg.Done()
|
|
}
|
|
func (l *mocListener) OnConnecting() {
|
|
l.lastState = ClientStateConnecting
|
|
l.wg.Done()
|
|
}
|
|
func (l *mocListener) OnDisconnecting() {
|
|
l.lastState = ClientStateDisconnecting
|
|
l.wg.Done()
|
|
}
|
|
|
|
func (l *mocListener) OnStateChanged(state ClientState) {
|
|
|
|
}
|
|
func (l *mocListener) OnAddressChanged(host, addr string) {
|
|
|
|
}
|
|
func (l *mocListener) OnPeersListChanged(size int) {
|
|
l.peers = size
|
|
l.peersWg.Done()
|
|
}
|
|
|
|
func (l *mocListener) setWaiter() {
|
|
l.wg.Add(1)
|
|
}
|
|
|
|
func (l *mocListener) wait() {
|
|
l.wg.Wait()
|
|
}
|
|
|
|
func (l *mocListener) setPeersWaiter() {
|
|
l.peersWg.Add(1)
|
|
}
|
|
|
|
func (l *mocListener) waitPeers() {
|
|
l.peersWg.Wait()
|
|
}
|
|
|
|
func Test_notifier_serverState(t *testing.T) {
|
|
|
|
type scenario struct {
|
|
name string
|
|
expected ClientState
|
|
mgmState bool
|
|
signalState bool
|
|
}
|
|
scenarios := []scenario{
|
|
{"connected", ClientStateConnected, true, true},
|
|
{"mgm down", ClientStateConnecting, false, true},
|
|
{"signal down", ClientStateConnecting, true, false},
|
|
{"disconnected", ClientStateDisconnected, false, false},
|
|
}
|
|
|
|
for _, tt := range scenarios {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
n := newNotifier()
|
|
n.updateServerStates(tt.mgmState, tt.signalState)
|
|
if n.lastNotification != tt.expected {
|
|
t.Errorf("invalid serverstate: %d, expected: %d", n.lastNotification, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_notifier_SetListener(t *testing.T) {
|
|
listener := &mocListener{}
|
|
listener.setWaiter()
|
|
listener.setPeersWaiter()
|
|
|
|
n := newNotifier()
|
|
n.lastNotification = ClientStateConnecting
|
|
n.setListener(listener)
|
|
listener.wait()
|
|
listener.waitPeers()
|
|
if listener.lastState != n.lastNotification {
|
|
t.Errorf("invalid state: %d, expected: %d", listener.lastState, n.lastNotification)
|
|
}
|
|
}
|
|
|
|
func Test_notifier_RemoveListener(t *testing.T) {
|
|
listener := &mocListener{}
|
|
listener.setWaiter()
|
|
listener.setPeersWaiter()
|
|
n := newNotifier()
|
|
n.lastNotification = ClientStateConnecting
|
|
n.setListener(listener)
|
|
// setListener replays cached state on a goroutine; wait for both the state
|
|
// and peers callbacks to finish so we don't race on listener.peers.
|
|
listener.wait()
|
|
listener.waitPeers()
|
|
n.removeListener()
|
|
n.peerListChanged(1)
|
|
|
|
if listener.peers != 0 {
|
|
t.Errorf("invalid state: %d", listener.peers)
|
|
}
|
|
}
|
|
|
|
type coalescingListener struct {
|
|
final int
|
|
calls atomic.Int32
|
|
inFlight atomic.Int32
|
|
maxInFlight atomic.Int32
|
|
last atomic.Int32
|
|
done chan struct{}
|
|
entered chan struct{}
|
|
release chan struct{}
|
|
once sync.Once
|
|
}
|
|
|
|
func (l *coalescingListener) OnStateChanged(ClientState) {}
|
|
func (l *coalescingListener) OnConnected() {}
|
|
func (l *coalescingListener) OnDisconnected() {}
|
|
func (l *coalescingListener) OnConnecting() {}
|
|
func (l *coalescingListener) OnDisconnecting() {}
|
|
func (l *coalescingListener) OnAddressChanged(string, string) {}
|
|
|
|
func (l *coalescingListener) OnPeersListChanged(size int) {
|
|
current := l.inFlight.Add(1)
|
|
for {
|
|
seen := l.maxInFlight.Load()
|
|
if current <= seen || l.maxInFlight.CompareAndSwap(seen, current) {
|
|
break
|
|
}
|
|
}
|
|
if l.calls.Add(1) == 1 && l.entered != nil {
|
|
close(l.entered)
|
|
}
|
|
if l.release != nil {
|
|
<-l.release
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
l.last.Store(int32(size))
|
|
l.inFlight.Add(-1)
|
|
if size == l.final {
|
|
l.once.Do(func() { close(l.done) })
|
|
}
|
|
}
|
|
|
|
func Test_notifier_PeerListChangedCoalesces(t *testing.T) {
|
|
const events = 1000
|
|
listener := &coalescingListener{final: events, done: make(chan struct{})}
|
|
n := newNotifier()
|
|
n.setListener(listener)
|
|
|
|
for i := 1; i <= events; i++ {
|
|
n.peerListChanged(i)
|
|
}
|
|
|
|
select {
|
|
case <-listener.done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("last peer count not delivered, last seen: %d", listener.last.Load())
|
|
}
|
|
|
|
if got := listener.maxInFlight.Load(); got != 1 {
|
|
t.Errorf("concurrent deliveries: %d, expected 1", got)
|
|
}
|
|
if got := listener.calls.Load(); got >= events {
|
|
t.Errorf("deliveries not coalesced: %d calls for %d events", got, events)
|
|
}
|
|
}
|
|
|
|
func Test_notifier_SetListenerStopsPreviousDeliverer(t *testing.T) {
|
|
old := &coalescingListener{final: -1}
|
|
replacement := &coalescingListener{final: 7, done: make(chan struct{})}
|
|
n := newNotifier()
|
|
n.setListener(old)
|
|
oldStop := n.peerListStop
|
|
|
|
n.peerListChanged(7)
|
|
n.setListener(replacement)
|
|
|
|
select {
|
|
case <-oldStop:
|
|
default:
|
|
t.Fatal("old deliverer not stopped on listener replacement")
|
|
}
|
|
waitFor(t, replacement.done, "replacement listener not notified")
|
|
}
|
|
|
|
func Test_notifier_RemoveListenerStopsDeliverer(t *testing.T) {
|
|
n := newNotifier()
|
|
n.setListener(&coalescingListener{final: -1})
|
|
stop := n.peerListStop
|
|
|
|
n.removeListener()
|
|
|
|
select {
|
|
case <-stop:
|
|
default:
|
|
t.Fatal("deliverer not stopped on listener removal")
|
|
}
|
|
}
|
|
|
|
func Test_notifier_DelivererExitsAfterInFlightCallback(t *testing.T) {
|
|
listener := &coalescingListener{
|
|
final: -1,
|
|
entered: make(chan struct{}),
|
|
release: make(chan struct{}),
|
|
}
|
|
n := newNotifier()
|
|
wake := make(chan struct{}, 1)
|
|
stop := make(chan struct{})
|
|
exited := make(chan struct{})
|
|
go func() {
|
|
n.deliverPeerListChanges(listener, wake, stop)
|
|
close(exited)
|
|
}()
|
|
|
|
wake <- struct{}{}
|
|
waitFor(t, listener.entered, "listener not called")
|
|
|
|
n.peerListChanged(7)
|
|
wake <- struct{}{}
|
|
close(stop)
|
|
close(listener.release)
|
|
|
|
waitFor(t, exited, "deliverer did not exit after stop")
|
|
if got := listener.calls.Load(); got != 1 {
|
|
t.Errorf("deliverer ran %d callbacks after stop, expected only the in-flight one", got)
|
|
}
|
|
if got := listener.last.Load(); got == 7 {
|
|
t.Errorf("deliverer delivered the peer count queued after stop")
|
|
}
|
|
}
|
|
|
|
func Test_notifier_DelivererPrefersStopOverPendingWake(t *testing.T) {
|
|
listener := &coalescingListener{final: -1}
|
|
n := newNotifier()
|
|
wake := make(chan struct{}, 1)
|
|
stop := make(chan struct{})
|
|
|
|
wake <- struct{}{}
|
|
close(stop)
|
|
n.deliverPeerListChanges(listener, wake, stop)
|
|
|
|
if got := listener.calls.Load(); got != 0 {
|
|
t.Errorf("deliverer ran %d callbacks with stop closed, expected 0", got)
|
|
}
|
|
}
|
|
|
|
func waitFor(t *testing.T, ch <-chan struct{}, msg string) {
|
|
t.Helper()
|
|
select {
|
|
case <-ch:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal(msg)
|
|
}
|
|
}
|