mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-30 11:31:29 +02:00
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.
92 lines
2.0 KiB
Go
92 lines
2.0 KiB
Go
package grpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/cenkalti/backoff/v4"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/client/netstate"
|
|
)
|
|
|
|
func TestRetryWakesOnNetworkChange(t *testing.T) {
|
|
ns := netstate.New()
|
|
attempts := 0
|
|
operation := func() error {
|
|
attempts++
|
|
if attempts == 1 {
|
|
return errors.New("cut by network change")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
go func() {
|
|
time.Sleep(20 * time.Millisecond)
|
|
ns.Set(false)
|
|
}()
|
|
|
|
start := time.Now()
|
|
err := Retry(context.Background(), operation, backoff.NewConstantBackOff(time.Minute), ns)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, attempts)
|
|
assert.Less(t, time.Since(start), time.Second, "the transition must cut the minute-long sleep short")
|
|
}
|
|
|
|
func TestRetryPermanentError(t *testing.T) {
|
|
sentinel := errors.New("permission denied")
|
|
operation := func() error {
|
|
return backoff.Permanent(sentinel)
|
|
}
|
|
|
|
err := Retry(context.Background(), operation, backoff.NewConstantBackOff(time.Millisecond), nil)
|
|
assert.ErrorIs(t, err, sentinel)
|
|
}
|
|
|
|
func TestRetryNilNetState(t *testing.T) {
|
|
attempts := 0
|
|
operation := func() error {
|
|
attempts++
|
|
if attempts < 3 {
|
|
return errors.New("transient")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
err := Retry(context.Background(), operation, backoff.NewConstantBackOff(time.Millisecond), nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 3, attempts)
|
|
}
|
|
|
|
func TestRetryStops(t *testing.T) {
|
|
failure := errors.New("still failing")
|
|
operation := func() error {
|
|
return failure
|
|
}
|
|
|
|
err := Retry(context.Background(), operation, &backoff.StopBackOff{}, nil)
|
|
assert.ErrorIs(t, err, failure)
|
|
}
|
|
|
|
func TestRetryCtxCancelDuringSleep(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
operation := func() error {
|
|
return errors.New("failing")
|
|
}
|
|
|
|
go func() {
|
|
time.Sleep(20 * time.Millisecond)
|
|
cancel()
|
|
}()
|
|
|
|
start := time.Now()
|
|
err := Retry(ctx, operation, backoff.NewConstantBackOff(time.Minute), netstate.New())
|
|
|
|
assert.ErrorIs(t, err, context.Canceled)
|
|
assert.Less(t, time.Since(start), time.Second)
|
|
}
|