mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-18 04:29:54 +00:00
* [iface] Drop redundant device dump in kernel configure() wgctrl.ConfigureDevice already returns an error when the interface is missing, so the preceding wg.Device() existence check is redundant. That check dumps the entire device (all peers) on every configure() call, making it O(peers) per call and turning bulk peer insertion into O(peers^2): inserting N peers one by one re-parsed the whole growing peer list N times. Removing it keeps each peer write constant-time regardless of how many peers are already configured. * [iface] Cache WireGuard stats to collapse per-peer device dumps Each peer runs a WGWatcher that polls GetStats(), and every call dumps the whole device, so with N peers the watchers perform O(N) full dumps per poll cycle (O(N^2) work) while each keeps only its own peer's entry. Wrap the kernel and userspace configurer GetStats() in a short-TTL cache with singleflight: the staggered per-peer calls share a single device dump per window and concurrent misses collapse into one dump. The kernel and userspace WireGuard APIs have no per-peer stats query (a get always returns the whole device), so a shared cached snapshot avoids the repeated full dumps. * Ignore .claude directory
71 lines
1.6 KiB
Go
71 lines
1.6 KiB
Go
package configurer
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestStatsCache_CachesWithinTTL(t *testing.T) {
|
|
var calls atomic.Int64
|
|
c := newStatsCache(50*time.Millisecond, func() (map[string]WGStats, error) {
|
|
calls.Add(1)
|
|
return map[string]WGStats{"p": {}}, nil
|
|
})
|
|
|
|
for i := 0; i < 10; i++ {
|
|
_, err := c.get()
|
|
require.NoError(t, err)
|
|
}
|
|
require.Equal(t, int64(1), calls.Load(), "within TTL only one underlying fetch")
|
|
|
|
time.Sleep(60 * time.Millisecond)
|
|
_, err := c.get()
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(2), calls.Load(), "after TTL expiry a fresh fetch happens")
|
|
}
|
|
|
|
func TestStatsCache_SingleFlight(t *testing.T) {
|
|
var calls atomic.Int64
|
|
release := make(chan struct{})
|
|
c := newStatsCache(time.Minute, func() (map[string]WGStats, error) {
|
|
calls.Add(1)
|
|
<-release
|
|
return map[string]WGStats{}, nil
|
|
})
|
|
|
|
const n = 50
|
|
var wg sync.WaitGroup
|
|
wg.Add(n)
|
|
for i := 0; i < n; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
_, _ = c.get()
|
|
}()
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
close(release)
|
|
wg.Wait()
|
|
|
|
require.Equal(t, int64(1), calls.Load(), "concurrent misses collapse into one fetch")
|
|
}
|
|
|
|
func TestStatsCache_ErrorNotCached(t *testing.T) {
|
|
var calls atomic.Int64
|
|
wantErr := errors.New("dump failed")
|
|
c := newStatsCache(time.Minute, func() (map[string]WGStats, error) {
|
|
calls.Add(1)
|
|
return nil, wantErr
|
|
})
|
|
|
|
_, err := c.get()
|
|
require.ErrorIs(t, err, wantErr)
|
|
_, err = c.get()
|
|
require.ErrorIs(t, err, wantErr)
|
|
require.Equal(t, int64(2), calls.Load(), "errors are not cached; each call retries")
|
|
}
|