Skip to content

Commit 10b7865

Browse files
authored
monitoring: fix race condition on New* variable functions (#339)
1 parent e412635 commit 10b7865

File tree

3 files changed

+190
-12
lines changed

3 files changed

+190
-12
lines changed

monitoring/metrics.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ type Int struct{ i atomic.Int64 }
4343
// variable will be available via expvars package as well, but can not be removed
4444
// anymore.
4545
func NewInt(r *Registry, name string, opts ...Option) *Int {
46+
rr := r
47+
if rr == nil {
48+
rr = Default
49+
}
50+
rr.txMu.Lock()
51+
defer rr.txMu.Unlock()
52+
4653
existingVar, r := setupMetric(r, name, opts)
4754
if existingVar != nil {
4855
cast, ok := existingVar.(*Int)
@@ -77,6 +84,13 @@ type Uint struct{ u atomic.Uint64 }
7784
// variable will be available via expvars package as well, but can not be removed
7885
// anymore.
7986
func NewUint(r *Registry, name string, opts ...Option) *Uint {
87+
rr := r
88+
if rr == nil {
89+
rr = Default
90+
}
91+
rr.txMu.Lock()
92+
defer rr.txMu.Unlock()
93+
8094
existingVar, r := setupMetric(r, name, opts)
8195
if existingVar != nil {
8296
cast, ok := existingVar.(*Uint)
@@ -114,6 +128,13 @@ type Float struct{ f atomic.Uint64 }
114128
// variable will be available via expvars package as well, but can not be removed
115129
// anymore.
116130
func NewFloat(r *Registry, name string, opts ...Option) *Float {
131+
rr := r
132+
if rr == nil {
133+
rr = Default
134+
}
135+
rr.txMu.Lock()
136+
defer rr.txMu.Unlock()
137+
117138
existingVar, r := setupMetric(r, name, opts)
118139
if existingVar != nil {
119140
cast, ok := existingVar.(*Float)
@@ -155,6 +176,13 @@ type Bool struct{ f atomic.Bool }
155176
// variable will be available via expvars package as well, but can not be removed
156177
// anymore.
157178
func NewBool(r *Registry, name string, opts ...Option) *Bool {
179+
rr := r
180+
if rr == nil {
181+
rr = Default
182+
}
183+
rr.txMu.Lock()
184+
defer rr.txMu.Unlock()
185+
158186
existingVar, r := setupMetric(r, name, opts)
159187
if existingVar != nil {
160188
cast, ok := existingVar.(*Bool)
@@ -188,6 +216,13 @@ type String struct {
188216
// variable will be available via expvars package as well, but can not be removed
189217
// anymore.
190218
func NewString(r *Registry, name string, opts ...Option) *String {
219+
rr := r
220+
if rr == nil {
221+
rr = Default
222+
}
223+
rr.txMu.Lock()
224+
defer rr.txMu.Unlock()
225+
191226
existingVar, r := setupMetric(r, name, opts)
192227
if existingVar != nil {
193228
cast, ok := existingVar.(*String)
@@ -239,6 +274,13 @@ type Func struct {
239274
}
240275

241276
func NewFunc(r *Registry, name string, f func(Mode, Visitor), opts ...Option) *Func {
277+
rr := r
278+
if rr == nil {
279+
rr = Default
280+
}
281+
rr.txMu.Lock()
282+
defer rr.txMu.Unlock()
283+
242284
existingVar, r := setupMetric(r, name, opts)
243285
if existingVar != nil {
244286
cast, ok := existingVar.(*Func)
@@ -282,6 +324,13 @@ type Timestamp struct {
282324

283325
// NewTimestamp creates and registers a new timestamp variable.
284326
func NewTimestamp(r *Registry, name string, opts ...Option) *Timestamp {
327+
rr := r
328+
if rr == nil {
329+
rr = Default
330+
}
331+
rr.txMu.Lock()
332+
defer rr.txMu.Unlock()
333+
285334
existingVar, r := setupMetric(r, name, opts)
286335
if existingVar != nil {
287336
cast, ok := existingVar.(*Timestamp)

monitoring/metrics_test.go

Lines changed: 137 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,150 @@
1818
package monitoring
1919

2020
import (
21+
"sync"
2122
"testing"
2223

24+
"github.com/stretchr/testify/assert"
2325
"github.com/stretchr/testify/require"
2426
)
2527

2628
func TestSafeVars(t *testing.T) {
27-
uintValName := "testUint"
28-
testReg := Default.NewRegistry("safe_registry")
29-
testUint := NewUint(testReg, uintValName)
30-
testUint.Set(5)
31-
// Add the first time
32-
require.NotNil(t, testUint)
29+
t.Run("no concurrency", func(t *testing.T) {
30+
uintValName := "testUint"
31+
testReg := NewRegistry().NewRegistry("safe_registry")
32+
testUint := NewUint(testReg, uintValName)
33+
testUint.Set(5)
34+
// Add the first time
35+
require.NotNil(t, testUint)
36+
37+
// Add the metric a second time
38+
testSecondUint := NewUint(testReg, uintValName)
39+
require.NotNil(t, testSecondUint)
40+
// make sure we fetch the same unit
41+
require.Equal(t, uint64(5), testSecondUint.Get())
42+
})
43+
44+
t.Run("with concurrency", func(t *testing.T) {
45+
t.Run("NewInt", func(t *testing.T) {
46+
reg := NewRegistry()
47+
name := "foo"
48+
49+
wg := sync.WaitGroup{}
50+
assert.NotPanics(t, func() {
51+
for i := 0; i < 1000; i++ {
52+
wg.Add(1)
53+
go func() {
54+
defer wg.Done()
55+
NewInt(reg, name)
56+
}()
57+
}
58+
})
59+
wg.Wait()
60+
})
61+
62+
t.Run("NewUint", func(t *testing.T) {
63+
reg := NewRegistry()
64+
name := "foo"
65+
66+
wg := sync.WaitGroup{}
67+
assert.NotPanics(t, func() {
68+
for i := 0; i < 1000; i++ {
69+
wg.Add(1)
70+
go func() {
71+
defer wg.Done()
72+
NewUint(reg, name)
73+
}()
74+
}
75+
})
76+
wg.Wait()
77+
})
78+
79+
t.Run("NewFloat", func(t *testing.T) {
80+
reg := NewRegistry()
81+
name := "foo"
3382

34-
// Add the metric a second time
35-
testSecondUint := NewUint(testReg, uintValName)
36-
require.NotNil(t, testSecondUint)
37-
// make sure we fetch the same unit
38-
require.Equal(t, uint64(5), testSecondUint.Get())
83+
wg := sync.WaitGroup{}
84+
assert.NotPanics(t, func() {
85+
for i := 0; i < 1000; i++ {
86+
wg.Add(1)
87+
go func() {
88+
defer wg.Done()
89+
NewFloat(reg, name)
90+
}()
91+
}
92+
})
93+
wg.Wait()
94+
})
95+
96+
t.Run("NewBool", func(t *testing.T) {
97+
reg := NewRegistry()
98+
name := "foo"
99+
100+
wg := sync.WaitGroup{}
101+
assert.NotPanics(t, func() {
102+
for i := 0; i < 1000; i++ {
103+
wg.Add(1)
104+
go func() {
105+
defer wg.Done()
106+
NewBool(reg, name)
107+
}()
108+
}
109+
})
110+
wg.Wait()
111+
})
112+
113+
t.Run("NewString", func(t *testing.T) {
114+
reg := NewRegistry()
115+
name := "foo"
116+
117+
wg := sync.WaitGroup{}
118+
assert.NotPanics(t, func() {
119+
for i := 0; i < 1000; i++ {
120+
wg.Add(1)
121+
go func() {
122+
defer wg.Done()
123+
NewString(reg, name)
124+
}()
125+
}
126+
})
127+
wg.Wait()
128+
})
129+
130+
t.Run("NewFunc", func(t *testing.T) {
131+
reg := NewRegistry()
132+
name := "foo"
133+
dummyFunc := func(m Mode, v Visitor) {}
134+
135+
wg := sync.WaitGroup{}
136+
assert.NotPanics(t, func() {
137+
for i := 0; i < 1000; i++ {
138+
wg.Add(1)
139+
go func() {
140+
defer wg.Done()
141+
NewFunc(reg, name, dummyFunc)
142+
}()
143+
}
144+
})
145+
wg.Wait()
146+
})
147+
148+
t.Run("NewTimestamp", func(t *testing.T) {
149+
reg := NewRegistry()
150+
name := "foo"
151+
152+
wg := sync.WaitGroup{}
153+
assert.NotPanics(t, func() {
154+
for i := 0; i < 1000; i++ {
155+
wg.Add(1)
156+
go func() {
157+
defer wg.Done()
158+
NewTimestamp(reg, name)
159+
}()
160+
}
161+
})
162+
wg.Wait()
163+
})
164+
})
39165
}
40166

41167
func TestVarsTypes(t *testing.T) {

monitoring/registry.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ import (
2828
// When adding or retrieving variables, all names are split on the `.`-symbol and
2929
// intermediate registries will be generated.
3030
type Registry struct {
31-
mu sync.RWMutex
31+
// txMu is a transaction mutex for the New* functions which create new
32+
// variables on a registry as they are not goroutine safe.
33+
txMu sync.Mutex
34+
mu sync.RWMutex
3235

3336
name string
3437
entries map[string]entry

0 commit comments

Comments
 (0)