// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause // Package expvarx provides some extensions to the [expvar] package. package expvarx import ( "encoding/json" "expvar" "sync" "time" "tailscale.com/types/lazy" ) // SafeFunc is a wrapper around [expvar.Func] that guards against unbounded call // time and ensures that only a single call is in progress at any given time. type SafeFunc struct { f expvar.Func limit time.Duration onSlow func(time.Duration, any) mu sync.Mutex inflight *lazy.SyncValue[any] } // NewSafeFunc returns a new SafeFunc that wraps f. // If f takes longer than limit to execute then Value calls return nil. // If onSlow is non-nil, it is called when f takes longer than limit to execute. // onSlow is called with the duration of the slow call and the final computed // value. func NewSafeFunc(f expvar.Func, limit time.Duration, onSlow func(time.Duration, any)) *SafeFunc { return &SafeFunc{f: f, limit: limit, onSlow: onSlow} } // Value acts similarly to [expvar.Func.Value], but if the underlying function // takes longer than the configured limit, all callers will receive nil until // the underlying operation completes. On completion of the underlying // operation, the onSlow callback is called if set. func (s *SafeFunc) Value() any { s.mu.Lock() if s.inflight == nil { s.inflight = new(lazy.SyncValue[any]) } var inflight = s.inflight s.mu.Unlock() // inflight ensures that only a single work routine is spawned at any given // time, but if the routine takes too long inflight is populated with a nil // result. The long running computed value is lost forever. return inflight.Get(func() any { start := time.Now() result := make(chan any, 1) // work is spawned in routine so that the caller can timeout. go func() { // Allow new work to be started after this work completes defer func() { s.mu.Lock() s.inflight = nil s.mu.Unlock() }() v := s.f.Value() result <- v }() select { case v := <-result: return v case <-time.After(s.limit): if s.onSlow != nil { go func() { s.onSlow(time.Since(start), <-result) }() } return nil } }) } // String implements stringer in the same pattern as [expvar.Func], calling // Value and serializing the result as JSON, ignoring errors. func (s *SafeFunc) String() string { v, _ := json.Marshal(s.Value()) return string(v) }