Three pieces, each verified on Android across WiFi/cellular switches:
- A sweep-aware backoff wrapper: the first retry after a disconnect
that follows a recent network-change mark comes after 200ms instead
of the randomized [0..1.6s] interval. Any other failure keeps the
unchanged spread, so the clients of a restarted server still scatter
their reconnects.
- The retry sleep wakes on OS network availability transitions
(nbgrpc.Retry): a disconnect that precedes the offline flag by a few
milliseconds no longer sleeps blindly through the recovery - the
loop parks on the netstate gate and resumes the moment the network
returns.
- The connection state is re-checked after WaitForStateChange: a dial
settling in Ready proceeds immediately instead of burning another
backoff round on an already-usable channel.
Measured after a network switch: management and signal recover in
270-470ms deterministically, down from a 312-1593ms lottery.
Stamp every registered connection and in-flight dial with a network
generation, bumped by MarkNetworkChange, which replaces the immediate
full sweep with one delayed by a configurable 500ms. The sweep then
cuts only registrations older than the last change: subsystems that
redialed on their own hold fresh-generation connections and survive,
so the callers no longer need cancellation logic around the sweep.
A connection inherits its dial's generation, because the socket was
bound to the network that was default when the dial started.
This fixes the sweep being cancelled by the engine's management-level
reconnect while the relay was still down, and lets the mobile
notifiers shrink to plain forwarders.
The connection and dial registries keyed on a bare uint64, which says
nothing about what the number identifies. Introduce sweepID so the maps,
the counter and the id fields state their intent. No behavior change.
Move the Dial type and its Ctx/Release methods above the Sweeper in
netsweep.go, and relocate SetNetworkAvailable / NotifyNetworkChange below
GetTunSettings in the Android binding. Pure code moves, no behavior change.
WrapDialContext and WrapConn registered the dial and the connection
independently, so a sweep landing between the dial finishing and WrapConn
cancelled only the dial registration: the connection dialed on the old
network entered the fresh registry and survived the network change.
Replace the pair with a Dial handle. Sweep marks pending dials under the
sweeper mutex, and WrapConn decides under the same mutex: a swept dial's
connection is closed and ErrSwept returned, so the caller redials on the
new network; otherwise the connection transfers to the registry with no
window in between.
connPair closed the accepted connection right after the handshake, so the
reads in TestSweepClosesRegisteredConns failed on the peer's own close
rather than on the sweep. The test passed even with Sweep's close loop
removed. Hold the peer until cleanup so the read errors come from Sweep.
Adding OnStateChanged to the gomobile interface forces every Swift
implementation to grow the method before the app builds again. Drop it
from the iOS binding for now — the adapter satisfies the internal
listener with a no-op and the legacy per-state callbacks keep firing —
so the app upgrades on its own schedule. The state constants stay
exported for that follow-up.