[client, android, ios] Coalesce peer list change notifications to the mobile listener (#7546)

* [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.
This commit is contained in:
Zoltan Papp
2026-10-06 15:41:01 +02:00
committed by GitHub
parent c5aa55d2b9
commit d56e6fc5f4
2 changed files with 207 additions and 8 deletions
+52 -8
View File
@@ -12,6 +12,8 @@ type notifier struct {
serverStateLock sync.Mutex
listenersLock sync.Mutex
listener Listener
peerListWake chan struct{}
peerListStop chan struct{}
currentClientState bool
lastNotification ClientState
lastNumberOfPeers int
@@ -62,7 +64,6 @@ func (n *notifier) setNetworkAvailable(available bool) {
func (n *notifier) setListener(listener Listener) {
n.serverStateLock.Lock()
lastNotification := n.effectiveState(n.lastNotification)
numOfPeers := n.lastNumberOfPeers
fqdnAddress := n.lastFqdnAddress
address := n.lastIPAddress
n.serverStateLock.Unlock()
@@ -70,17 +71,19 @@ func (n *notifier) setListener(listener Listener) {
n.listenersLock.Lock()
defer n.listenersLock.Unlock()
n.stopPeerListDelivererLocked()
n.listener = listener
listener.OnAddressChanged(fqdnAddress, address)
notifyListener(listener, lastNotification)
// run on go routine to avoid on Java layer to call go functions on same thread
go listener.OnPeersListChanged(numOfPeers)
n.startPeerListDelivererLocked(listener)
n.wakePeerListDelivererLocked()
}
func (n *notifier) removeListener() {
n.listenersLock.Lock()
defer n.listenersLock.Unlock()
n.stopPeerListDelivererLocked()
n.listener = nil
}
@@ -178,15 +181,56 @@ func (n *notifier) peerListChanged(numOfPeers int) {
n.serverStateLock.Unlock()
n.listenersLock.Lock()
listener := n.listener
n.listenersLock.Unlock()
defer n.listenersLock.Unlock()
n.wakePeerListDelivererLocked()
}
if listener == nil {
func (n *notifier) startPeerListDelivererLocked(listener Listener) {
wake := make(chan struct{}, 1)
stop := make(chan struct{})
n.peerListWake = wake
n.peerListStop = stop
go n.deliverPeerListChanges(listener, wake, stop)
}
func (n *notifier) stopPeerListDelivererLocked() {
if n.peerListStop == nil {
return
}
close(n.peerListStop)
n.peerListStop = nil
n.peerListWake = nil
}
// run on go routine to avoid on Java layer to call go functions on same thread
go listener.OnPeersListChanged(numOfPeers)
func (n *notifier) wakePeerListDelivererLocked() {
if n.peerListWake == nil {
return
}
select {
case n.peerListWake <- struct{}{}:
default:
}
}
func (n *notifier) deliverPeerListChanges(listener Listener, wake <-chan struct{}, stop <-chan struct{}) {
for {
select {
case <-stop:
return
case <-wake:
}
select {
case <-stop:
return
default:
}
n.serverStateLock.Lock()
numOfPeers := n.lastNumberOfPeers
n.serverStateLock.Unlock()
listener.OnPeersListChanged(numOfPeers)
}
}
func (n *notifier) localAddressChanged(fqdn, address string) {