mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-24 16:41:30 +02:00
On mobile the client kept dialing management, signal, relay and peer connections while the device had no usable network at all (airplane mode), burning battery for attempts that cannot succeed. Stopping the engine is not an option: tearing it down destroys the TUN device, and traffic can leak outside the tunnel until it is rebuilt. Add client/netstate, a small gate the platform feeds from its own connectivity callbacks. Every reconnection loop waits on it instead of retrying blindly, and resets its backoff when the network returns so recovery is immediate. The state is injected through functional options and consumers hold a *State that may be nil, so every platform that does not report availability behaves exactly as before. The relay quick-reconnect rechecks availability after its 1.5s wait: the disconnect that triggers it is usually the first symptom of the network going away, so the flag typically arrives while it sleeps. Report the suspension to the UI as well. peer.Listener grows OnStateChanged with a typed ClientState, re-exported across the gomobile boundary as integer constants, and the notifier maps Connecting to a new NoNetwork state while the OS reports no network, so mobile clients can show "no network available" instead of a misleading "connecting". Finally, exit the client retry loop cleanly when its context is cancelled. backoff.WithContext surfaces the bare context error, which callers could not distinguish from a real failure — on Android that turned an engine restart into an unrecoverable error.
171 lines
3.4 KiB
Go
171 lines
3.4 KiB
Go
package netstate
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewStateIsOnline(t *testing.T) {
|
|
assert.True(t, New().IsOnline(), "a fresh State should start online")
|
|
}
|
|
|
|
func TestSetTogglesOnlineState(t *testing.T) {
|
|
s := New()
|
|
|
|
s.Set(false)
|
|
assert.False(t, s.IsOnline(), "state should be offline after Set(false)")
|
|
|
|
s.Set(true)
|
|
assert.True(t, s.IsOnline(), "state should be online after Set(true)")
|
|
}
|
|
|
|
func TestWaitReturnsImmediatelyWhenOnline(t *testing.T) {
|
|
s := New()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
waited, err := s.Wait(ctx)
|
|
require.NoError(t, err)
|
|
assert.False(t, waited, "Wait should not block when the network is online")
|
|
}
|
|
|
|
func TestWaitBlocksUntilOnline(t *testing.T) {
|
|
s := New()
|
|
s.Set(false)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
result := make(chan bool, 1)
|
|
go func() {
|
|
waited, err := s.Wait(ctx)
|
|
if err != nil {
|
|
result <- false
|
|
return
|
|
}
|
|
result <- waited
|
|
}()
|
|
|
|
// Verify Wait is actually blocking while offline
|
|
select {
|
|
case <-result:
|
|
t.Fatal("Wait should block while the network is offline")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
|
|
s.Set(true)
|
|
|
|
select {
|
|
case waited := <-result:
|
|
assert.True(t, waited, "Wait should report that it had to wait for the network")
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Wait should return promptly after the network becomes available")
|
|
}
|
|
}
|
|
|
|
func TestWaitReturnsOnContextCancel(t *testing.T) {
|
|
s := New()
|
|
s.Set(false)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
result := make(chan error, 1)
|
|
go func() {
|
|
_, err := s.Wait(ctx)
|
|
result <- err
|
|
}()
|
|
|
|
cancel()
|
|
|
|
select {
|
|
case err := <-result:
|
|
assert.ErrorIs(t, err, context.Canceled)
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Wait should return promptly after context cancellation")
|
|
}
|
|
}
|
|
|
|
func TestWaitWakesAllWaiters(t *testing.T) {
|
|
s := New()
|
|
s.Set(false)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
const waiters = 10
|
|
var wg sync.WaitGroup
|
|
results := make(chan bool, waiters)
|
|
for i := 0; i < waiters; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
waited, err := s.Wait(ctx)
|
|
if err != nil {
|
|
results <- false
|
|
return
|
|
}
|
|
results <- waited
|
|
}()
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
s.Set(true)
|
|
wg.Wait()
|
|
|
|
close(results)
|
|
count := 0
|
|
for waited := range results {
|
|
assert.True(t, waited, "every waiter should report that it waited")
|
|
count++
|
|
}
|
|
assert.Equal(t, waiters, count, "all waiters should have returned")
|
|
}
|
|
|
|
func TestNilStateReadsAreNoops(t *testing.T) {
|
|
var s *State
|
|
|
|
assert.True(t, s.IsOnline(), "nil State should report online")
|
|
|
|
waited, err := s.Wait(context.Background())
|
|
require.NoError(t, err)
|
|
assert.False(t, waited, "nil State's Wait should not block")
|
|
}
|
|
|
|
func TestConcurrentSetAndWait(t *testing.T) {
|
|
s := New()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 4; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < 100; j++ {
|
|
s.Set(j%2 == 0)
|
|
s.IsOnline()
|
|
}
|
|
}()
|
|
}
|
|
for i := 0; i < 4; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < 100; j++ {
|
|
if _, err := s.Wait(ctx); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|