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>
pull/11944/head
Joe Tsai 1 month ago committed by GitHub
parent 44aa809cb0
commit 5db80cf2d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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…
Cancel
Save