mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-18 02:48:40 +00:00
syncs: fix AtomicValue for interface kinds (#11943)
If AtomicValue[T] is used with a T that is an interface kind, then Store may panic if different concret types are ever stored. Fix this by always wrapping in a concrete type. Technically, this is only needed if T is an interface kind, but there is no harm in doing it also for non-interface kinds. Updates #cleanup Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This commit is contained in:
parent
44aa809cb0
commit
5db80cf2d8
@ -23,11 +23,18 @@ func initClosedChan() <-chan struct{} {
|
|||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
// AtomicValue is the generic version of atomic.Value.
|
// AtomicValue is the generic version of [atomic.Value].
|
||||||
type AtomicValue[T any] struct {
|
type AtomicValue[T any] struct {
|
||||||
v atomic.Value
|
v atomic.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wrappedValue is used to wrap a value T in a concrete type,
|
||||||
|
// otherwise atomic.Value.Store may panic due to mismatching types in interfaces.
|
||||||
|
// This wrapping is not necessary for non-interface kinds of T,
|
||||||
|
// but there is no harm in wrapping anyways.
|
||||||
|
// See https://cs.opensource.google/go/go/+/refs/tags/go1.22.2:src/sync/atomic/value.go;l=78
|
||||||
|
type wrappedValue[T any] struct{ v T }
|
||||||
|
|
||||||
// Load returns the value set by the most recent Store.
|
// Load returns the value set by the most recent Store.
|
||||||
// It returns the zero value for T if the value is empty.
|
// It returns the zero value for T if the value is empty.
|
||||||
func (v *AtomicValue[T]) Load() T {
|
func (v *AtomicValue[T]) Load() T {
|
||||||
@ -40,7 +47,7 @@ func (v *AtomicValue[T]) Load() T {
|
|||||||
func (v *AtomicValue[T]) LoadOk() (_ T, ok bool) {
|
func (v *AtomicValue[T]) LoadOk() (_ T, ok bool) {
|
||||||
x := v.v.Load()
|
x := v.v.Load()
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.(T), true
|
return x.(wrappedValue[T]).v, true
|
||||||
}
|
}
|
||||||
var zero T
|
var zero T
|
||||||
return zero, false
|
return zero, false
|
||||||
@ -48,22 +55,22 @@ func (v *AtomicValue[T]) LoadOk() (_ T, ok bool) {
|
|||||||
|
|
||||||
// Store sets the value of the Value to x.
|
// Store sets the value of the Value to x.
|
||||||
func (v *AtomicValue[T]) Store(x T) {
|
func (v *AtomicValue[T]) Store(x T) {
|
||||||
v.v.Store(x)
|
v.v.Store(wrappedValue[T]{x})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Swap stores new into Value and returns the previous value.
|
// Swap stores new into Value and returns the previous value.
|
||||||
// It returns the zero value for T if the value is empty.
|
// It returns the zero value for T if the value is empty.
|
||||||
func (v *AtomicValue[T]) Swap(x T) (old T) {
|
func (v *AtomicValue[T]) Swap(x T) (old T) {
|
||||||
oldV := v.v.Swap(x)
|
oldV := v.v.Swap(wrappedValue[T]{x})
|
||||||
if oldV != nil {
|
if oldV != nil {
|
||||||
return oldV.(T)
|
return oldV.(wrappedValue[T]).v
|
||||||
}
|
}
|
||||||
return old
|
return old
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompareAndSwap executes the compare-and-swap operation for the Value.
|
// CompareAndSwap executes the compare-and-swap operation for the Value.
|
||||||
func (v *AtomicValue[T]) CompareAndSwap(oldV, newV T) (swapped bool) {
|
func (v *AtomicValue[T]) CompareAndSwap(oldV, newV T) (swapped bool) {
|
||||||
return v.v.CompareAndSwap(oldV, newV)
|
return v.v.CompareAndSwap(wrappedValue[T]{oldV}, wrappedValue[T]{newV})
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitGroupChan is like a sync.WaitGroup, but has a chan that closes
|
// WaitGroupChan is like a sync.WaitGroup, but has a chan that closes
|
||||||
|
@ -5,12 +5,67 @@ package syncs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestAtomicValue(t *testing.T) {
|
||||||
|
{
|
||||||
|
// Always wrapping should not allocate for simple values
|
||||||
|
// because wrappedValue[T] has the same memory layout as T.
|
||||||
|
var v AtomicValue[bool]
|
||||||
|
bools := []bool{true, false}
|
||||||
|
if n := int(testing.AllocsPerRun(1000, func() {
|
||||||
|
for _, b := range bools {
|
||||||
|
v.Store(b)
|
||||||
|
}
|
||||||
|
})); n != 0 {
|
||||||
|
t.Errorf("AllocsPerRun = %d, want 0", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var v AtomicValue[int]
|
||||||
|
got, gotOk := v.LoadOk()
|
||||||
|
if got != 0 || gotOk {
|
||||||
|
t.Fatalf("LoadOk = (%v, %v), want (0, false)", got, gotOk)
|
||||||
|
}
|
||||||
|
v.Store(1)
|
||||||
|
got, gotOk = v.LoadOk()
|
||||||
|
if got != 1 || !gotOk {
|
||||||
|
t.Fatalf("LoadOk = (%v, %v), want (1, true)", got, gotOk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var v AtomicValue[error]
|
||||||
|
got, gotOk := v.LoadOk()
|
||||||
|
if got != nil || gotOk {
|
||||||
|
t.Fatalf("LoadOk = (%v, %v), want (nil, false)", got, gotOk)
|
||||||
|
}
|
||||||
|
v.Store(io.EOF)
|
||||||
|
got, gotOk = v.LoadOk()
|
||||||
|
if got != io.EOF || !gotOk {
|
||||||
|
t.Fatalf("LoadOk = (%v, %v), want (EOF, true)", got, gotOk)
|
||||||
|
}
|
||||||
|
err := &os.PathError{}
|
||||||
|
v.Store(err)
|
||||||
|
got, gotOk = v.LoadOk()
|
||||||
|
if got != err || !gotOk {
|
||||||
|
t.Fatalf("LoadOk = (%v, %v), want (%v, true)", got, gotOk, err)
|
||||||
|
}
|
||||||
|
v.Store(nil)
|
||||||
|
got, gotOk = v.LoadOk()
|
||||||
|
if got != nil || !gotOk {
|
||||||
|
t.Fatalf("LoadOk = (%v, %v), want (nil, true)", got, gotOk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWaitGroupChan(t *testing.T) {
|
func TestWaitGroupChan(t *testing.T) {
|
||||||
wg := NewWaitGroupChan()
|
wg := NewWaitGroupChan()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user