Skip to content

Commit 1505d28

Browse files
authored
Fix Swap and CompareAndSwap for Value wrappers (#130)
* Regenerate code to update copyright end year to 2023 * Test behaviour of default values initialized in different ways This adds repro tests for #126 and #129 * Fix Swap and CompareAndSwap for Value wrappers Fixes #126, #129 All atomic types can be used without initialization, e.g., `var v <AtomicType>`. This works fine for integer types as the initialized value of 0 matches the default value for the user-facing type. However, for Value wrappers, they are initialized to `nil`, which is a value that can't be set (triggers a panic) so the default value for the user-facing type is forced to be stored as a different value. This leads to multiple possible values representing the default user-facing type. E.g., an `atomic.String` with value `""` may be represented by the underlying atomic as either `nil`, or `""`. This causes issues when we don't handle the `nil` value correctly, causing to panics in `Swap` and incorrectly not swapping values in `CompareAndSwap`. This change fixes the above issues by: * Requiring `pack` and `unpack` function in gen-atomicwrapper as the only place we weren't supplying them was for `String`, and the branching adds unnecessary complexity, especially with added `nil` handling. * Extending `CompareAndSwap` for `Value` wrappers to try an additional `CompareAndSwap(nil, <new>)` only if the original `CompareAndSwap` fails and the old value is the zero value.
1 parent 78a3b8e commit 1505d28

18 files changed

+252
-44
lines changed

bool.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated Code generated by gen-atomicwrapper.
22

3-
// Copyright (c) 2020-2022 Uber Technologies, Inc.
3+
// Copyright (c) 2020-2023 Uber Technologies, Inc.
44
//
55
// Permission is hereby granted, free of charge, to any person obtaining a copy
66
// of this software and associated documentation files (the "Software"), to deal

bool_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,67 @@ func TestBool(t *testing.T) {
8484
})
8585
})
8686
}
87+
88+
func TestBool_InitializeDefaults(t *testing.T) {
89+
tests := []struct {
90+
msg string
91+
newBool func() *Bool
92+
}{
93+
{
94+
msg: "Uninitialized",
95+
newBool: func() *Bool {
96+
var b Bool
97+
return &b
98+
},
99+
},
100+
{
101+
msg: "NewBool with default",
102+
newBool: func() *Bool {
103+
return NewBool(false)
104+
},
105+
},
106+
{
107+
msg: "Bool swapped with default",
108+
newBool: func() *Bool {
109+
b := NewBool(true)
110+
b.Swap(false)
111+
return b
112+
},
113+
},
114+
{
115+
msg: "Bool CAS'd with default",
116+
newBool: func() *Bool {
117+
b := NewBool(true)
118+
b.CompareAndSwap(true, false)
119+
return b
120+
},
121+
},
122+
}
123+
124+
for _, tt := range tests {
125+
t.Run(tt.msg, func(t *testing.T) {
126+
t.Run("MarshalJSON", func(t *testing.T) {
127+
b := tt.newBool()
128+
marshalled, err := b.MarshalJSON()
129+
require.NoError(t, err)
130+
assert.Equal(t, "false", string(marshalled))
131+
})
132+
133+
t.Run("String", func(t *testing.T) {
134+
b := tt.newBool()
135+
assert.Equal(t, "false", b.String())
136+
})
137+
138+
t.Run("CompareAndSwap", func(t *testing.T) {
139+
b := tt.newBool()
140+
require.True(t, b.CompareAndSwap(false, true))
141+
assert.Equal(t, true, b.Load())
142+
})
143+
144+
t.Run("Swap", func(t *testing.T) {
145+
b := tt.newBool()
146+
assert.Equal(t, false, b.Swap(true))
147+
})
148+
})
149+
}
150+
}

duration.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated Code generated by gen-atomicwrapper.
22

3-
// Copyright (c) 2020-2022 Uber Technologies, Inc.
3+
// Copyright (c) 2020-2023 Uber Technologies, Inc.
44
//
55
// Permission is hereby granted, free of charge, to any person obtaining a copy
66
// of this software and associated documentation files (the "Software"), to deal

error.go

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated Code generated by gen-atomicwrapper.
22

3-
// Copyright (c) 2020-2022 Uber Technologies, Inc.
3+
// Copyright (c) 2020-2023 Uber Technologies, Inc.
44
//
55
// Permission is hereby granted, free of charge, to any person obtaining a copy
66
// of this software and associated documentation files (the "Software"), to deal
@@ -52,7 +52,17 @@ func (x *Error) Store(val error) {
5252

5353
// CompareAndSwap is an atomic compare-and-swap for error values.
5454
func (x *Error) CompareAndSwap(old, new error) (swapped bool) {
55-
return x.v.CompareAndSwap(packError(old), packError(new))
55+
if x.v.CompareAndSwap(packError(old), packError(new)) {
56+
return true
57+
}
58+
59+
if old == _zeroError {
60+
// If the old value is the empty value, then it's possible the
61+
// underlying Value hasn't been set and is nil, so retry with nil.
62+
return x.v.CompareAndSwap(nil, packError(new))
63+
}
64+
65+
return false
5666
}
5767

5868
// Swap atomically stores the given error and returns the old

error_test.go

+53
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"errors"
2525
"testing"
2626

27+
"github.com/stretchr/testify/assert"
2728
"github.com/stretchr/testify/require"
2829
)
2930

@@ -81,3 +82,55 @@ func TestErrorCompareAndSwap(t *testing.T) {
8182
require.True(t, swapped, "Expected swapped to be true")
8283
require.Equal(t, err2, atom.Load(), "Expected Load to return overridden value")
8384
}
85+
86+
func TestError_InitializeDefaults(t *testing.T) {
87+
tests := []struct {
88+
msg string
89+
newError func() *Error
90+
}{
91+
{
92+
msg: "Uninitialized",
93+
newError: func() *Error {
94+
var e Error
95+
return &e
96+
},
97+
},
98+
{
99+
msg: "NewError with default",
100+
newError: func() *Error {
101+
return NewError(nil)
102+
},
103+
},
104+
{
105+
msg: "Error swapped with default",
106+
newError: func() *Error {
107+
e := NewError(assert.AnError)
108+
e.Swap(nil)
109+
return e
110+
},
111+
},
112+
{
113+
msg: "Error CAS'd with default",
114+
newError: func() *Error {
115+
e := NewError(assert.AnError)
116+
e.CompareAndSwap(assert.AnError, nil)
117+
return e
118+
},
119+
},
120+
}
121+
122+
for _, tt := range tests {
123+
t.Run(tt.msg, func(t *testing.T) {
124+
t.Run("CompareAndSwap", func(t *testing.T) {
125+
e := tt.newError()
126+
require.True(t, e.CompareAndSwap(nil, assert.AnError))
127+
assert.Equal(t, assert.AnError, e.Load())
128+
})
129+
130+
t.Run("Swap", func(t *testing.T) {
131+
e := tt.newError()
132+
assert.Equal(t, nil, e.Swap(assert.AnError))
133+
})
134+
})
135+
}
136+
}

float32.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated Code generated by gen-atomicwrapper.
22

3-
// Copyright (c) 2020-2022 Uber Technologies, Inc.
3+
// Copyright (c) 2020-2023 Uber Technologies, Inc.
44
//
55
// Permission is hereby granted, free of charge, to any person obtaining a copy
66
// of this software and associated documentation files (the "Software"), to deal

float64.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated Code generated by gen-atomicwrapper.
22

3-
// Copyright (c) 2020-2022 Uber Technologies, Inc.
3+
// Copyright (c) 2020-2023 Uber Technologies, Inc.
44
//
55
// Permission is hereby granted, free of charge, to any person obtaining a copy
66
// of this software and associated documentation files (the "Software"), to deal

int32.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated Code generated by gen-atomicint.
22

3-
// Copyright (c) 2020-2022 Uber Technologies, Inc.
3+
// Copyright (c) 2020-2023 Uber Technologies, Inc.
44
//
55
// Permission is hereby granted, free of charge, to any person obtaining a copy
66
// of this software and associated documentation files (the "Software"), to deal

int64.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated Code generated by gen-atomicint.
22

3-
// Copyright (c) 2020-2022 Uber Technologies, Inc.
3+
// Copyright (c) 2020-2023 Uber Technologies, Inc.
44
//
55
// Permission is hereby granted, free of charge, to any person obtaining a copy
66
// of this software and associated documentation files (the "Software"), to deal

internal/gen-atomicwrapper/main.go

+6-9
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,6 @@
4747
//
4848
// The packing/unpacking logic allows the stored value to be different from
4949
// the user-facing value.
50-
//
51-
// Without -pack and -unpack, the output will be cast to the target type,
52-
// defaulting to the zero value.
5350
package main
5451

5552
import (
@@ -143,12 +140,12 @@ func run(args []string) error {
143140
return err
144141
}
145142

146-
if len(opts.Name) == 0 || len(opts.Wrapped) == 0 || len(opts.Type) == 0 {
147-
return errors.New("flags -name, -wrapped, and -type are required")
148-
}
149-
150-
if (len(opts.Pack) == 0) != (len(opts.Unpack) == 0) {
151-
return errors.New("either both, or neither of -pack and -unpack must be specified")
143+
if len(opts.Name) == 0 ||
144+
len(opts.Wrapped) == 0 ||
145+
len(opts.Type) == 0 ||
146+
len(opts.Pack) == 0 ||
147+
len(opts.Unpack) == 0 {
148+
return errors.New("flags -name, -wrapped, -pack, -unpack and -type are required")
152149
}
153150

154151
if opts.CAS {

internal/gen-atomicwrapper/wrapper.tmpl

+15-13
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,7 @@ func (x *{{ .Name }}) Load() {{ .Type }} {
6161

6262
// Store atomically stores the passed {{ .Type }}.
6363
func (x *{{ .Name }}) Store(val {{ .Type }}) {
64-
{{ if .Pack -}}
65-
x.v.Store({{ .Pack }}(val))
66-
{{- else -}}
67-
x.v.Store(val)
68-
{{- end }}
64+
x.v.Store({{ .Pack }}(val))
6965
}
7066

7167
{{ if .CAS -}}
@@ -80,10 +76,20 @@ func (x *{{ .Name }}) Store(val {{ .Type }}) {
8076
{{ if .CompareAndSwap -}}
8177
// CompareAndSwap is an atomic compare-and-swap for {{ .Type }} values.
8278
func (x *{{ .Name }}) CompareAndSwap(old, new {{ .Type }}) (swapped bool) {
83-
{{ if .Pack -}}
79+
{{ if eq .Wrapped "Value" -}}
80+
if x.v.CompareAndSwap({{ .Pack }}(old), {{ .Pack }}(new)) {
81+
return true
82+
}
83+
84+
if old == _zero{{ .Name }} {
85+
// If the old value is the empty value, then it's possible the
86+
// underlying Value hasn't been set and is nil, so retry with nil.
87+
return x.v.CompareAndSwap(nil, {{ .Pack }}(new))
88+
}
89+
90+
return false
91+
{{- else -}}
8492
return x.v.CompareAndSwap({{ .Pack }}(old), {{ .Pack }}(new))
85-
{{- else -}}{{- /* assume go.uber.org/atomic.Value */ -}}
86-
return x.v.CompareAndSwap(old, new)
8793
{{- end }}
8894
}
8995
{{- end }}
@@ -92,11 +98,7 @@ func (x *{{ .Name }}) Store(val {{ .Type }}) {
9298
// Swap atomically stores the given {{ .Type }} and returns the old
9399
// value.
94100
func (x *{{ .Name }}) Swap(val {{ .Type }}) (old {{ .Type }}) {
95-
{{ if .Pack -}}
96-
return {{ .Unpack }}(x.v.Swap({{ .Pack }}(val)))
97-
{{- else -}}{{- /* assume go.uber.org/atomic.Value */ -}}
98-
return x.v.Swap(val).({{ .Type }})
99-
{{- end }}
101+
return {{ .Unpack }}(x.v.Swap({{ .Pack }}(val)))
100102
}
101103
{{- end }}
102104

string.go

+15-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @generated Code generated by gen-atomicwrapper.
22

3-
// Copyright (c) 2020-2022 Uber Technologies, Inc.
3+
// Copyright (c) 2020-2023 Uber Technologies, Inc.
44
//
55
// Permission is hereby granted, free of charge, to any person obtaining a copy
66
// of this software and associated documentation files (the "Software"), to deal
@@ -42,24 +42,31 @@ func NewString(val string) *String {
4242

4343
// Load atomically loads the wrapped string.
4444
func (x *String) Load() string {
45-
if v := x.v.Load(); v != nil {
46-
return v.(string)
47-
}
48-
return _zeroString
45+
return unpackString(x.v.Load())
4946
}
5047

5148
// Store atomically stores the passed string.
5249
func (x *String) Store(val string) {
53-
x.v.Store(val)
50+
x.v.Store(packString(val))
5451
}
5552

5653
// CompareAndSwap is an atomic compare-and-swap for string values.
5754
func (x *String) CompareAndSwap(old, new string) (swapped bool) {
58-
return x.v.CompareAndSwap(old, new)
55+
if x.v.CompareAndSwap(packString(old), packString(new)) {
56+
return true
57+
}
58+
59+
if old == _zeroString {
60+
// If the old value is the empty value, then it's possible the
61+
// underlying Value hasn't been set and is nil, so retry with nil.
62+
return x.v.CompareAndSwap(nil, packString(new))
63+
}
64+
65+
return false
5966
}
6067

6168
// Swap atomically stores the given string and returns the old
6269
// value.
6370
func (x *String) Swap(val string) (old string) {
64-
return x.v.Swap(val).(string)
71+
return unpackString(x.v.Swap(packString(val)))
6572
}

string_ext.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2020-2022 Uber Technologies, Inc.
1+
// Copyright (c) 2020-2023 Uber Technologies, Inc.
22
//
33
// Permission is hereby granted, free of charge, to any person obtaining a copy
44
// of this software and associated documentation files (the "Software"), to deal
@@ -20,7 +20,18 @@
2020

2121
package atomic
2222

23-
//go:generate bin/gen-atomicwrapper -name=String -type=string -wrapped=Value -compareandswap -swap -file=string.go
23+
//go:generate bin/gen-atomicwrapper -name=String -type=string -wrapped Value -pack packString -unpack unpackString -compareandswap -swap -file=string.go
24+
25+
func packString(s string) interface{} {
26+
return s
27+
}
28+
29+
func unpackString(v interface{}) string {
30+
if s, ok := v.(string); ok {
31+
return s
32+
}
33+
return ""
34+
}
2435

2536
// String returns the wrapped value.
2637
func (s *String) String() string {

0 commit comments

Comments
 (0)