tailscale/net/netmon/loghelper_test.go
James Tucker 21695cdbf8 ipn/ipnlocal,net/netmon: make frequent darkwake more efficient
Investigating battery costs on a busy tailnet I noticed a large number
of nodes regularly reconnecting to control and DERP. In one case I was
able to analyze closely `pmset` reported the every-minute wake-ups being
triggered by bluetooth. The node was by side effect reconnecting to
control constantly, and this was at times visible to peers as well.

Three changes here improve the situation:
- Short time jumps (less than 10 minutes) no longer produce "major
  network change" events, and so do not trigger full rebind/reconnect.
- Many "incidental" fields on interfaces are ignored, like MTU, flags
  and so on - if the route is still good, the rest should be manageable.
- Additional log output will provide more detail about the cause of
  major network change events.

Updates #3363

Signed-off-by: James Tucker <james@tailscale.com>
2026-04-06 15:46:51 -07:00

94 lines
2.4 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netmon
import (
"bytes"
"context"
"fmt"
"testing"
"testing/synctest"
"tailscale.com/util/eventbus"
"tailscale.com/util/eventbus/eventbustest"
)
func TestLinkChangeLogLimiter(t *testing.T) { synctest.Test(t, syncTestLinkChangeLogLimiter) }
func syncTestLinkChangeLogLimiter(t *testing.T) {
bus := eventbus.New()
defer bus.Close()
mon, err := New(bus, t.Logf)
if err != nil {
t.Fatal(err)
}
defer mon.Close()
var logBuffer bytes.Buffer
logf := func(format string, args ...any) {
t.Logf("captured log: "+format, args...)
if format[len(format)-1] != '\n' {
format += "\n"
}
fmt.Fprintf(&logBuffer, format, args...)
}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
logf = LinkChangeLogLimiter(ctx, logf, mon)
// Log once, which should write to our log buffer.
logf("hello %s", "world")
if got := logBuffer.String(); got != "hello world\n" {
t.Errorf("unexpected log buffer contents: %q", got)
}
// Log again, which should not write to our log buffer.
logf("hello %s", "andrew")
if got := logBuffer.String(); got != "hello world\n" {
t.Errorf("unexpected log buffer contents: %q", got)
}
// Log a different message, which should write to our log buffer.
logf("other message")
if got := logBuffer.String(); got != "hello world\nother message\n" {
t.Errorf("unexpected log buffer contents: %q", got)
}
// Synthesize a fake major change event, which should clear the format
// string cache and allow the next log to write to our log buffer.
//
// InjectEvent doesn't work because it's not a major event, so we
// instead inject the event ourselves.
injector := eventbustest.NewInjector(t, bus)
cd, err := NewChangeDelta(nil, &State{}, 0, true)
if err != nil {
t.Fatal(err)
}
if cd.RebindLikelyRequired != true {
t.Fatalf("expected RebindLikelyRequired to be true, got false")
}
eventbustest.Inject(injector, cd)
synctest.Wait()
logf("hello %s", "world")
want := "hello world\nother message\nhello world\n"
if got := logBuffer.String(); got != want {
t.Errorf("unexpected log buffer contents, got: %q, want, %q", got, want)
}
// Canceling the context we passed to LinkChangeLogLimiter should
// unregister the callback from the netmon.
cancel()
synctest.Wait()
mon.mu.Lock()
if len(mon.cbs) != 0 {
t.Errorf("expected no callbacks, got %v", mon.cbs)
}
mon.mu.Unlock()
}