mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-16 11:41:39 +00:00
types/lazy: add Peek method to SyncValue
This adds the ability to "peek" at the value of a SyncValue, so that it's possible to observe a value without computing this. Updates tailscale/corp#17122 Signed-off-by: Andrew Dunham <andrew@du.nham.ca> Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I06f88c22a1f7ffcbc7ff82946335356bb0ef4622
This commit is contained in:
parent
7dd76c3411
commit
200d92121f
@ -59,7 +59,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
|||||||
tailscale.com/types/lazy from tailscale.com/version+
|
tailscale.com/types/lazy from tailscale.com/version+
|
||||||
tailscale.com/types/logger from tailscale.com/tsweb
|
tailscale.com/types/logger from tailscale.com/tsweb
|
||||||
tailscale.com/types/opt from tailscale.com/envknob+
|
tailscale.com/types/opt from tailscale.com/envknob+
|
||||||
tailscale.com/types/ptr from tailscale.com/tailcfg
|
tailscale.com/types/ptr from tailscale.com/tailcfg+
|
||||||
tailscale.com/types/structs from tailscale.com/tailcfg+
|
tailscale.com/types/structs from tailscale.com/tailcfg+
|
||||||
tailscale.com/types/tkatype from tailscale.com/tailcfg+
|
tailscale.com/types/tkatype from tailscale.com/tailcfg+
|
||||||
tailscale.com/types/views from tailscale.com/net/tsaddr+
|
tailscale.com/types/views from tailscale.com/net/tsaddr+
|
||||||
|
@ -4,7 +4,16 @@
|
|||||||
// Package lazy provides types for lazily initialized values.
|
// Package lazy provides types for lazily initialized values.
|
||||||
package lazy
|
package lazy
|
||||||
|
|
||||||
import "sync"
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"tailscale.com/types/ptr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nilErrPtr is a sentinel *error value for SyncValue.err to signal
|
||||||
|
// that SyncValue.v is valid.
|
||||||
|
var nilErrPtr = ptr.To[error](nil)
|
||||||
|
|
||||||
// SyncValue is a lazily computed value.
|
// SyncValue is a lazily computed value.
|
||||||
//
|
//
|
||||||
@ -17,7 +26,17 @@ import "sync"
|
|||||||
type SyncValue[T any] struct {
|
type SyncValue[T any] struct {
|
||||||
once sync.Once
|
once sync.Once
|
||||||
v T
|
v T
|
||||||
err error
|
|
||||||
|
// err is either:
|
||||||
|
// * nil, if not yet computed
|
||||||
|
// * nilErrPtr, if completed and nil
|
||||||
|
// * non-nil and not nilErrPtr on error.
|
||||||
|
//
|
||||||
|
// It is an atomic.Pointer so it can be read outside of the sync.Once.Do.
|
||||||
|
//
|
||||||
|
// Writes to err must happen after a write to v so a caller seeing a non-nil
|
||||||
|
// err can safely read v.
|
||||||
|
err atomic.Pointer[error]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set attempts to set z's value to val, and reports whether it succeeded.
|
// Set attempts to set z's value to val, and reports whether it succeeded.
|
||||||
@ -26,6 +45,7 @@ func (z *SyncValue[T]) Set(val T) bool {
|
|||||||
var wasSet bool
|
var wasSet bool
|
||||||
z.once.Do(func() {
|
z.once.Do(func() {
|
||||||
z.v = val
|
z.v = val
|
||||||
|
z.err.Store(nilErrPtr) // after write to z.v; see docs
|
||||||
wasSet = true
|
wasSet = true
|
||||||
})
|
})
|
||||||
return wasSet
|
return wasSet
|
||||||
@ -41,15 +61,63 @@ func (z *SyncValue[T]) MustSet(val T) {
|
|||||||
// Get returns z's value, calling fill to compute it if necessary.
|
// Get returns z's value, calling fill to compute it if necessary.
|
||||||
// f is called at most once.
|
// f is called at most once.
|
||||||
func (z *SyncValue[T]) Get(fill func() T) T {
|
func (z *SyncValue[T]) Get(fill func() T) T {
|
||||||
z.once.Do(func() { z.v = fill() })
|
z.once.Do(func() {
|
||||||
|
z.v = fill()
|
||||||
|
z.err.Store(nilErrPtr) // after write to z.v; see docs
|
||||||
|
})
|
||||||
return z.v
|
return z.v
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetErr returns z's value, calling fill to compute it if necessary.
|
// GetErr returns z's value, calling fill to compute it if necessary.
|
||||||
// f is called at most once, and z remembers both of fill's outputs.
|
// f is called at most once, and z remembers both of fill's outputs.
|
||||||
func (z *SyncValue[T]) GetErr(fill func() (T, error)) (T, error) {
|
func (z *SyncValue[T]) GetErr(fill func() (T, error)) (T, error) {
|
||||||
z.once.Do(func() { z.v, z.err = fill() })
|
z.once.Do(func() {
|
||||||
return z.v, z.err
|
var err error
|
||||||
|
z.v, err = fill()
|
||||||
|
|
||||||
|
// Update z.err after z.v; see field docs.
|
||||||
|
if err != nil {
|
||||||
|
z.err.Store(ptr.To(err))
|
||||||
|
} else {
|
||||||
|
z.err.Store(nilErrPtr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return z.v, *z.err.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peek returns z's value and a boolean indicating whether the value has been
|
||||||
|
// set successfully. If a value has not been set, the zero value of T is
|
||||||
|
// returned.
|
||||||
|
//
|
||||||
|
// This function is safe to call concurrently with Get/GetErr/Set, but it's
|
||||||
|
// undefined whether a value set by a concurrent call will be visible to Peek.
|
||||||
|
//
|
||||||
|
// To get any error that's been set, use PeekErr.
|
||||||
|
//
|
||||||
|
// If GetErr's fill function returned a valid T and an non-nil error, Peek
|
||||||
|
// discards that valid T value. PeekErr returns both.
|
||||||
|
func (z *SyncValue[T]) Peek() (v T, ok bool) {
|
||||||
|
if z.err.Load() == nilErrPtr {
|
||||||
|
return z.v, true
|
||||||
|
}
|
||||||
|
var zero T
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeekErr returns z's value and error and a boolean indicating whether the
|
||||||
|
// value or error has been set. If ok is false, T and err are the zero value.
|
||||||
|
//
|
||||||
|
// This function is safe to call concurrently with Get/GetErr/Set, but it's
|
||||||
|
// undefined whether a value set by a concurrent call will be visible to Peek.
|
||||||
|
//
|
||||||
|
// Unlike Peek, PeekErr reports ok if either v or err has been set, not just v,
|
||||||
|
// and returns both the T and err returned by GetErr's fill function.
|
||||||
|
func (z *SyncValue[T]) PeekErr() (v T, err error, ok bool) {
|
||||||
|
if e := z.err.Load(); e != nil {
|
||||||
|
return z.v, *e, true
|
||||||
|
}
|
||||||
|
var zero T
|
||||||
|
return zero, nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncFunc wraps a function to make it lazy.
|
// SyncFunc wraps a function to make it lazy.
|
||||||
|
@ -5,6 +5,7 @@ package lazy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -16,6 +17,11 @@ func TestSyncValue(t *testing.T) {
|
|||||||
if got != 42 {
|
if got != 42 {
|
||||||
t.Fatalf("got %v; want 42", got)
|
t.Fatalf("got %v; want 42", got)
|
||||||
}
|
}
|
||||||
|
if p, ok := lt.Peek(); !ok {
|
||||||
|
t.Fatalf("Peek failed")
|
||||||
|
} else if p != 42 {
|
||||||
|
t.Fatalf("Peek got %v; want 42", p)
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
if n != 0 {
|
if n != 0 {
|
||||||
t.Errorf("allocs = %v; want 0", n)
|
t.Errorf("allocs = %v; want 0", n)
|
||||||
@ -45,6 +51,12 @@ func TestSyncValueErr(t *testing.T) {
|
|||||||
if got != 0 || err != wantErr {
|
if got != 0 || err != wantErr {
|
||||||
t.Fatalf("got %v, %v; want 0, %v", got, err, wantErr)
|
t.Fatalf("got %v, %v; want 0, %v", got, err, wantErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p, ok := lt.Peek(); !ok {
|
||||||
|
t.Fatalf("Peek failed")
|
||||||
|
} else if got != 0 {
|
||||||
|
t.Fatalf("Peek got %v; want 0", p)
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
if n != 0 {
|
if n != 0 {
|
||||||
t.Errorf("allocs = %v; want 0", n)
|
t.Errorf("allocs = %v; want 0", n)
|
||||||
@ -59,6 +71,11 @@ func TestSyncValueSet(t *testing.T) {
|
|||||||
if lt.Set(43) {
|
if lt.Set(43) {
|
||||||
t.Fatalf("Set succeeded after first Set")
|
t.Fatalf("Set succeeded after first Set")
|
||||||
}
|
}
|
||||||
|
if p, ok := lt.Peek(); !ok {
|
||||||
|
t.Fatalf("Peek failed")
|
||||||
|
} else if p != 42 {
|
||||||
|
t.Fatalf("Peek got %v; want 42", p)
|
||||||
|
}
|
||||||
n := int(testing.AllocsPerRun(1000, func() {
|
n := int(testing.AllocsPerRun(1000, func() {
|
||||||
got := lt.Get(fortyTwo)
|
got := lt.Get(fortyTwo)
|
||||||
if got != 42 {
|
if got != 42 {
|
||||||
@ -81,6 +98,30 @@ func TestSyncValueMustSet(t *testing.T) {
|
|||||||
lt.MustSet(43)
|
lt.MustSet(43)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSyncValueErrPeek(t *testing.T) {
|
||||||
|
var sv SyncValue[int]
|
||||||
|
sv.GetErr(func() (int, error) {
|
||||||
|
return 123, errors.New("boom")
|
||||||
|
})
|
||||||
|
p, ok := sv.Peek()
|
||||||
|
if ok {
|
||||||
|
t.Error("unexpected Peek success")
|
||||||
|
}
|
||||||
|
if p != 0 {
|
||||||
|
t.Fatalf("Peek got %v; want 0", p)
|
||||||
|
}
|
||||||
|
p, err, ok := sv.PeekErr()
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("PeekErr ok=false; want true on error")
|
||||||
|
}
|
||||||
|
if got, want := fmt.Sprint(err), "boom"; got != want {
|
||||||
|
t.Errorf("PeekErr error=%v; want %v", got, want)
|
||||||
|
}
|
||||||
|
if p != 123 {
|
||||||
|
t.Fatalf("PeekErr got %v; want 123", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSyncValueConcurrent(t *testing.T) {
|
func TestSyncValueConcurrent(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
lt SyncValue[int]
|
lt SyncValue[int]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user