diff --git a/common/atomic/value.go b/common/atomic/value.go index 82d40076..0de9fade 100644 --- a/common/atomic/value.go +++ b/common/atomic/value.go @@ -27,11 +27,16 @@ type tValue[T any] struct { } func (t *TypedValue[T]) Load() T { + value, _ := t.LoadOk() + return value +} + +func (t *TypedValue[T]) LoadOk() (_ T, ok bool) { value := t.value.Load() if value == nil { - return DefaultValue[T]() + return DefaultValue[T](), false } - return value.(tValue[T]).value + return value.(tValue[T]).value, true } func (t *TypedValue[T]) Store(value T) { @@ -47,7 +52,11 @@ func (t *TypedValue[T]) Swap(new T) T { } func (t *TypedValue[T]) CompareAndSwap(old, new T) bool { - return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new}) + return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new}) || + // In the edge-case where [atomic.Value.Store] is uninitialized + // and trying to compare with the zero value of T, + // then compare-and-swap with the nil any value. + (any(old) == any(DefaultValue[T]()) && t.value.CompareAndSwap(any(nil), tValue[T]{new})) } func (t *TypedValue[T]) MarshalJSON() ([]byte, error) { diff --git a/common/atomic/value_test.go b/common/atomic/value_test.go new file mode 100644 index 00000000..881b3e8d --- /dev/null +++ b/common/atomic/value_test.go @@ -0,0 +1,77 @@ +package atomic + +import ( + "io" + "os" + "testing" +) + +func TestTypedValue(t *testing.T) { + { + // Always wrapping should not allocate for simple values + // because tValue[T] has the same memory layout as T. + var v TypedValue[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 TypedValue[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 TypedValue[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) + } + } + + { + c1, c2, c3 := make(chan struct{}), make(chan struct{}), make(chan struct{}) + var v TypedValue[chan struct{}] + if v.CompareAndSwap(c1, c2) != false { + t.Fatalf("CompareAndSwap = true, want false") + } + if v.CompareAndSwap(nil, c1) != true { + t.Fatalf("CompareAndSwap = false, want true") + } + if v.CompareAndSwap(c2, c3) != false { + t.Fatalf("CompareAndSwap = true, want false") + } + if v.CompareAndSwap(c1, c2) != true { + t.Fatalf("CompareAndSwap = false, want true") + } + } +} diff --git a/go.mod b/go.mod index b808996e..7808752e 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 github.com/metacubex/randv2 v0.2.0 - github.com/metacubex/sing v0.5.3 + github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29 github.com/metacubex/sing-mux v0.3.2 github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f github.com/metacubex/sing-shadowsocks v0.2.10 diff --git a/go.sum b/go.sum index 7f2c16fe..d39544a9 100644 --- a/go.sum +++ b/go.sum @@ -116,8 +116,8 @@ github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639/go.mod h1:Kc6 github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= -github.com/metacubex/sing v0.5.3 h1:QWdN16WFKMk06x4nzkc8SvZ7y2x+TLQrpkPoHs+WSVM= -github.com/metacubex/sing v0.5.3/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= +github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29 h1:SD9q025FNTaepuFXFOKDhnGLVu6PQYChBvw2ZYPXeLo= +github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw= github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw= github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f h1:mP3vIm+9hRFI0C0Vl3pE0NESF/L85FDbuB0tGgUii6I=