[client] Serialize availability transitions in the netevents manager

SetNetworkAvailable read IsOnline before updating netState without any
locking, so concurrent availability flips could interleave: a false call
could observe a stale offline state, skip MarkNetworkChange, and land
after a racing true call, leaving the client offline with unswept
connections. The netState and recorder updates could also apply out of
order, letting the listener state disagree with netState. On Android the
initial availability push at service start races the ConnectivityManager
callbacks, and iOS feeds the same manager from NWPathMonitor.

Hold a manager mutex across the check, sweep mark, state update and
recorder update so transitions apply atomically and in order.
This commit is contained in:
Zoltan Papp
2026-08-24 21:58:04 +02:00
parent acdeb385a9
commit 1f1bccb674

View File

@@ -6,6 +6,7 @@ package netevents
import (
"context"
"sync"
"time"
"github.com/cenkalti/backoff/v4"
@@ -25,6 +26,10 @@ type Recorder interface {
// the valid no-events value: the read methods report always-online and never
// sweep.
type Manager struct {
// mu serializes availability transitions: the IsOnline check and the
// state update must be atomic, or a racing offline flip can skip the sweep
// and leave netState and the recorder disagreeing.
mu sync.Mutex
netState *netstate.State
sweeper *sweep.Sweeper
recorder Recorder
@@ -48,6 +53,9 @@ func NewManager(recorder Recorder) *Manager {
// "connected" until their own timeouts and the client would keep reporting
// Connected with no network at all.
func (m *Manager) SetNetworkAvailable(available bool) {
m.mu.Lock()
defer m.mu.Unlock()
if !available && m.netState.IsOnline() {
m.sweeper.MarkNetworkChange()
}