// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package expvarx import ( "expvar" "fmt" "sync" "sync/atomic" "testing" "time" ) func ExampleNewSafeFunc() { // An artificial blocker to emulate a slow operation. blocker := make(chan struct{}) // limit is the amount of time a call can take before Value returns nil. No // new calls to the unsafe func will be started until the slow call // completes, at which point onSlow will be called. limit := time.Millisecond // onSlow is called with the final call duration and the final value in the // event a slow call. onSlow := func(d time.Duration, v any) { _ = d // d contains the time the call took _ = v // v contains the final value computed by the slow call fmt.Println("slow call!") } // An unsafe expvar.Func that blocks on the blocker channel. unsafeFunc := expvar.Func(func() any { for range blocker { } return "hello world" }) // f implements the same interface as expvar.Func, but returns nil values // when the unsafe func is too slow. f := NewSafeFunc(unsafeFunc, limit, onSlow) fmt.Println(f.Value()) fmt.Println(f.Value()) close(blocker) time.Sleep(time.Millisecond) fmt.Println(f.Value()) // Output: // // slow call! // hello world } func TestSafeFuncHappyPath(t *testing.T) { var count int f := NewSafeFunc(expvar.Func(func() any { count++ return count }), time.Millisecond, nil) if got, want := f.Value(), 1; got != want { t.Errorf("got %v, want %v", got, want) } if got, want := f.Value(), 2; got != want { t.Errorf("got %v, want %v", got, want) } } func TestSafeFuncSlow(t *testing.T) { var count int blocker := make(chan struct{}) var wg sync.WaitGroup wg.Add(1) f := NewSafeFunc(expvar.Func(func() any { defer wg.Done() count++ <-blocker return count }), time.Millisecond, nil) if got := f.Value(); got != nil { t.Errorf("got %v; want nil", got) } if got := f.Value(); got != nil { t.Errorf("got %v; want nil", got) } close(blocker) wg.Wait() if count != 1 { t.Errorf("got count=%d; want 1", count) } } func TestSafeFuncSlowOnSlow(t *testing.T) { var count int blocker := make(chan struct{}) var wg sync.WaitGroup wg.Add(2) var slowDuration atomic.Pointer[time.Duration] var slowCallCount atomic.Int32 var slowValue atomic.Value f := NewSafeFunc(expvar.Func(func() any { defer wg.Done() count++ <-blocker return count }), time.Millisecond, func(d time.Duration, v any) { defer wg.Done() slowDuration.Store(&d) slowCallCount.Add(1) slowValue.Store(v) }) for range 10 { if got := f.Value(); got != nil { t.Fatalf("got value=%v; want nil", got) } } close(blocker) wg.Wait() if count != 1 { t.Errorf("got count=%d; want 1", count) } if got, want := *slowDuration.Load(), 1*time.Millisecond; got < want { t.Errorf("got slowDuration=%v; want at least %d", got, want) } if got, want := slowCallCount.Load(), int32(1); got != want { t.Errorf("got slowCallCount=%d; want %d", got, want) } if got, want := slowValue.Load().(int), 1; got != want { t.Errorf("got slowValue=%d, want %d", got, want) } }