Skip to content

Commit 8d7f947

Browse files
committed
fix: TypedValue.CompareAndSwap
tailscale/tailscale@84aa7ff
1 parent 71a8705 commit 8d7f947

File tree

4 files changed

+92
-6
lines changed

4 files changed

+92
-6
lines changed

common/atomic/value.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,16 @@ type tValue[T any] struct {
2727
}
2828

2929
func (t *TypedValue[T]) Load() T {
30+
value, _ := t.LoadOk()
31+
return value
32+
}
33+
34+
func (t *TypedValue[T]) LoadOk() (_ T, ok bool) {
3035
value := t.value.Load()
3136
if value == nil {
32-
return DefaultValue[T]()
37+
return DefaultValue[T](), false
3338
}
34-
return value.(tValue[T]).value
39+
return value.(tValue[T]).value, true
3540
}
3641

3742
func (t *TypedValue[T]) Store(value T) {
@@ -47,7 +52,11 @@ func (t *TypedValue[T]) Swap(new T) T {
4752
}
4853

4954
func (t *TypedValue[T]) CompareAndSwap(old, new T) bool {
50-
return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new})
55+
return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new}) ||
56+
// In the edge-case where [atomic.Value.Store] is uninitialized
57+
// and trying to compare with the zero value of T,
58+
// then compare-and-swap with the nil any value.
59+
(any(old) == any(DefaultValue[T]()) && t.value.CompareAndSwap(any(nil), tValue[T]{new}))
5160
}
5261

5362
func (t *TypedValue[T]) MarshalJSON() ([]byte, error) {

common/atomic/value_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package atomic
2+
3+
import (
4+
"io"
5+
"os"
6+
"testing"
7+
)
8+
9+
func TestTypedValue(t *testing.T) {
10+
{
11+
// Always wrapping should not allocate for simple values
12+
// because tValue[T] has the same memory layout as T.
13+
var v TypedValue[bool]
14+
bools := []bool{true, false}
15+
if n := int(testing.AllocsPerRun(1000, func() {
16+
for _, b := range bools {
17+
v.Store(b)
18+
}
19+
})); n != 0 {
20+
t.Errorf("AllocsPerRun = %d, want 0", n)
21+
}
22+
}
23+
24+
{
25+
var v TypedValue[int]
26+
got, gotOk := v.LoadOk()
27+
if got != 0 || gotOk {
28+
t.Fatalf("LoadOk = (%v, %v), want (0, false)", got, gotOk)
29+
}
30+
v.Store(1)
31+
got, gotOk = v.LoadOk()
32+
if got != 1 || !gotOk {
33+
t.Fatalf("LoadOk = (%v, %v), want (1, true)", got, gotOk)
34+
}
35+
}
36+
37+
{
38+
var v TypedValue[error]
39+
got, gotOk := v.LoadOk()
40+
if got != nil || gotOk {
41+
t.Fatalf("LoadOk = (%v, %v), want (nil, false)", got, gotOk)
42+
}
43+
v.Store(io.EOF)
44+
got, gotOk = v.LoadOk()
45+
if got != io.EOF || !gotOk {
46+
t.Fatalf("LoadOk = (%v, %v), want (EOF, true)", got, gotOk)
47+
}
48+
err := &os.PathError{}
49+
v.Store(err)
50+
got, gotOk = v.LoadOk()
51+
if got != err || !gotOk {
52+
t.Fatalf("LoadOk = (%v, %v), want (%v, true)", got, gotOk, err)
53+
}
54+
v.Store(nil)
55+
got, gotOk = v.LoadOk()
56+
if got != nil || !gotOk {
57+
t.Fatalf("LoadOk = (%v, %v), want (nil, true)", got, gotOk)
58+
}
59+
}
60+
61+
{
62+
c1, c2, c3 := make(chan struct{}), make(chan struct{}), make(chan struct{})
63+
var v TypedValue[chan struct{}]
64+
if v.CompareAndSwap(c1, c2) != false {
65+
t.Fatalf("CompareAndSwap = true, want false")
66+
}
67+
if v.CompareAndSwap(nil, c1) != true {
68+
t.Fatalf("CompareAndSwap = false, want true")
69+
}
70+
if v.CompareAndSwap(c2, c3) != false {
71+
t.Fatalf("CompareAndSwap = true, want false")
72+
}
73+
if v.CompareAndSwap(c1, c2) != true {
74+
t.Fatalf("CompareAndSwap = false, want true")
75+
}
76+
}
77+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ require (
2525
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
2626
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639
2727
github.com/metacubex/randv2 v0.2.0
28-
github.com/metacubex/sing v0.5.3
28+
github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29
2929
github.com/metacubex/sing-mux v0.3.2
3030
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f
3131
github.com/metacubex/sing-shadowsocks v0.2.10

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639/go.mod h1:Kc6
116116
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
117117
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
118118
github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
119-
github.com/metacubex/sing v0.5.3 h1:QWdN16WFKMk06x4nzkc8SvZ7y2x+TLQrpkPoHs+WSVM=
120-
github.com/metacubex/sing v0.5.3/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
119+
github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29 h1:SD9q025FNTaepuFXFOKDhnGLVu6PQYChBvw2ZYPXeLo=
120+
github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
121121
github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
122122
github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
123123
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f h1:mP3vIm+9hRFI0C0Vl3pE0NESF/L85FDbuB0tGgUii6I=

0 commit comments

Comments
 (0)