Skip to content

Commit 1e59134

Browse files
committed
[Bug fix] fix the sync.Map.Range() iterations on the Report call.
Improve metric reporting and caching in scope to avoid race conditions. Adjusted tests to reflect new initialization patterns.
1 parent be42de2 commit 1e59134

4 files changed

Lines changed: 108 additions & 48 deletions

File tree

scope.go

Lines changed: 93 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -238,47 +238,99 @@ func newRootScope(opts ScopeOptions, interval time.Duration) *scope {
238238

239239
// report dumps all aggregated stats into the reporter. Should be called automatically by the root scope periodically.
240240
func (s *scope) report(r StatsReporter) {
241-
s.counters.Range(func(key, value interface{}) bool {
242-
c := value.(*counter)
243-
c.report(s.fullyQualifiedName(key.(string)), s.tags, r)
244-
return true
245-
})
241+
// Copy slices under their individual mutexes to avoid race with clearMetrics
242+
s.countersSliceMux.Lock()
243+
var countersCopy []*counter
244+
if s.countersSlice != nil {
245+
countersCopy = make([]*counter, len(s.countersSlice))
246+
copy(countersCopy, s.countersSlice)
247+
}
248+
s.countersSliceMux.Unlock()
246249

247-
s.gauges.Range(func(key, value interface{}) bool {
248-
g := value.(*gauge)
249-
g.report(s.fullyQualifiedName(key.(string)), s.tags, r)
250-
return true
251-
})
250+
s.gaugesSliceMux.Lock()
251+
var gaugesCopy []*gauge
252+
if s.gaugesSlice != nil {
253+
gaugesCopy = make([]*gauge, len(s.gaugesSlice))
254+
copy(gaugesCopy, s.gaugesSlice)
255+
}
256+
s.gaugesSliceMux.Unlock()
257+
258+
s.histogramsSliceMux.Lock()
259+
var histogramsCopy []*histogram
260+
if s.histogramsSlice != nil {
261+
histogramsCopy = make([]*histogram, len(s.histogramsSlice))
262+
copy(histogramsCopy, s.histogramsSlice)
263+
}
264+
s.histogramsSliceMux.Unlock()
265+
266+
// Now iterate over the copies without holding any mutexes
267+
for _, c := range countersCopy {
268+
if c != nil {
269+
c.report(c.name, s.tags, r)
270+
}
271+
}
272+
273+
for _, g := range gaugesCopy {
274+
if g != nil {
275+
g.report(g.name, s.tags, r)
276+
}
277+
}
252278

253279
// We do nothing for timers here because timers report directly to the StatsReporter without buffering
254280

255-
s.histograms.Range(func(key, value interface{}) bool {
256-
h := value.(*histogram)
257-
h.report(s.fullyQualifiedName(key.(string)), s.tags, r)
258-
return true
259-
})
281+
for _, h := range histogramsCopy {
282+
if h != nil {
283+
h.report(h.name, s.tags, r)
284+
}
285+
}
260286
}
261287

262288
func (s *scope) cachedReport() {
263-
s.counters.Range(func(key, value interface{}) bool {
264-
c := value.(*counter)
265-
c.cachedReport()
266-
return true
267-
})
289+
// Copy slices under their individual mutexes to avoid race with clearMetrics
290+
s.countersSliceMux.Lock()
291+
var countersCopy []*counter
292+
if s.countersSlice != nil {
293+
countersCopy = make([]*counter, len(s.countersSlice))
294+
copy(countersCopy, s.countersSlice)
295+
}
296+
s.countersSliceMux.Unlock()
268297

269-
s.gauges.Range(func(key, value interface{}) bool {
270-
g := value.(*gauge)
271-
g.cachedReport()
272-
return true
273-
})
298+
s.gaugesSliceMux.Lock()
299+
var gaugesCopy []*gauge
300+
if s.gaugesSlice != nil {
301+
gaugesCopy = make([]*gauge, len(s.gaugesSlice))
302+
copy(gaugesCopy, s.gaugesSlice)
303+
}
304+
s.gaugesSliceMux.Unlock()
305+
306+
s.histogramsSliceMux.Lock()
307+
var histogramsCopy []*histogram
308+
if s.histogramsSlice != nil {
309+
histogramsCopy = make([]*histogram, len(s.histogramsSlice))
310+
copy(histogramsCopy, s.histogramsSlice)
311+
}
312+
s.histogramsSliceMux.Unlock()
313+
314+
// Now iterate over the copies without holding any mutexes
315+
for _, c := range countersCopy {
316+
if c != nil {
317+
c.cachedReport()
318+
}
319+
}
320+
321+
for _, g := range gaugesCopy {
322+
if g != nil {
323+
g.cachedReport()
324+
}
325+
}
274326

275327
// We do nothing for timers here because timers report directly to the StatsReporter without buffering
276328

277-
s.histograms.Range(func(key, value interface{}) bool {
278-
h := value.(*histogram)
279-
h.cachedReport()
280-
return true
281-
})
329+
for _, h := range histogramsCopy {
330+
if h != nil {
331+
h.cachedReport()
332+
}
333+
}
282334
}
283335

284336
// reportLoop is used by the root scope for periodic reporting
@@ -333,7 +385,7 @@ func (s *scope) Counter(name string) Counter {
333385
)
334386
}
335387

336-
c := newCounter(cachedCounter)
388+
c := newCounter(s.fullyQualifiedName(name), cachedCounter)
337389

338390
// Attempt to store it, or get the existing one if another goroutine created it first
339391
actual, loaded := s.counters.LoadOrStore(name, c)
@@ -380,7 +432,7 @@ func (s *scope) Gauge(name string) Gauge {
380432
)
381433
}
382434

383-
g := newGauge(cachedGauge)
435+
g := newGauge(s.fullyQualifiedName(name), cachedGauge)
384436

385437
// Attempt to store it, or get the existing one if another goroutine created it first
386438
actual, loaded := s.gauges.LoadOrStore(name, g)
@@ -691,24 +743,27 @@ func (s *scope) Close() error {
691743
}
692744

693745
func (s *scope) clearMetrics() {
694-
s.clearMux.Lock()
695-
defer s.clearMux.Unlock()
696-
697746
var numCounters, numGauges, numHistograms int64
698747

699748
s.counters.Range(func(key, value interface{}) bool {
700749
numCounters++
701750
s.counters.Delete(key)
702751
return true
703752
})
753+
754+
s.countersSliceMux.Lock()
704755
s.countersSlice = nil
756+
s.countersSliceMux.Unlock()
705757

706758
s.gauges.Range(func(key, value interface{}) bool {
707759
numGauges++
708760
s.gauges.Delete(key)
709761
return true
710762
})
763+
764+
s.gaugesSliceMux.Lock()
711765
s.gaugesSlice = nil
766+
s.gaugesSliceMux.Unlock()
712767

713768
s.timers.Range(func(key, value interface{}) bool {
714769
s.timers.Delete(key)
@@ -720,7 +775,10 @@ func (s *scope) clearMetrics() {
720775
s.histograms.Delete(key)
721776
return true
722777
})
778+
779+
s.histogramsSliceMux.Lock()
723780
s.histogramsSlice = nil
781+
s.histogramsSliceMux.Unlock()
724782

725783
// Atomically decrement the cardinality counters in the registry.
726784
if s.registry != nil {

stats.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,14 @@ func (c *capabilities) Tagging() bool {
6060
}
6161

6262
type counter struct {
63+
name string
6364
prev int64
6465
curr int64
6566
cachedCount CachedCount
6667
}
6768

68-
func newCounter(cachedCount CachedCount) *counter {
69-
return &counter{cachedCount: cachedCount}
69+
func newCounter(name string, cachedCount CachedCount) *counter {
70+
return &counter{name: name, cachedCount: cachedCount}
7071
}
7172

7273
func (c *counter) Inc(v int64) {
@@ -107,13 +108,14 @@ func (c *counter) snapshot() int64 {
107108
}
108109

109110
type gauge struct {
111+
name string
110112
updated uint64
111113
curr uint64
112114
cachedGauge CachedGauge
113115
}
114116

115-
func newGauge(cachedGauge CachedGauge) *gauge {
116-
return &gauge{cachedGauge: cachedGauge}
117+
func newGauge(name string, cachedGauge CachedGauge) *gauge {
118+
return &gauge{name: name, cachedGauge: cachedGauge}
117119
}
118120

119121
func (g *gauge) Update(v float64) {
@@ -297,7 +299,7 @@ func newHistogram(
297299
}
298300

299301
for i := range h.samples {
300-
h.samples[i].counter = newCounter(nil)
302+
h.samples[i].counter = newCounter(name, nil)
301303

302304
if cachedHistogram != nil {
303305
switch htype {

stats_benchmark_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,43 +26,43 @@ import (
2626
)
2727

2828
func BenchmarkCounterInc(b *testing.B) {
29-
c := &counter{}
29+
c := newCounter("foo", nil)
3030
for n := 0; n < b.N; n++ {
3131
c.Inc(1)
3232
}
3333
}
3434

3535
func BenchmarkReportCounterNoData(b *testing.B) {
36-
c := &counter{}
36+
c := newCounter("foo", nil)
3737
for n := 0; n < b.N; n++ {
3838
c.report("foo", nil, NullStatsReporter)
3939
}
4040
}
4141

4242
func BenchmarkReportCounterWithData(b *testing.B) {
43-
c := &counter{}
43+
c := newCounter("foo", nil)
4444
for n := 0; n < b.N; n++ {
4545
c.Inc(1)
4646
c.report("foo", nil, NullStatsReporter)
4747
}
4848
}
4949

5050
func BenchmarkGaugeSet(b *testing.B) {
51-
g := &gauge{}
51+
g := newGauge("bar", nil)
5252
for n := 0; n < b.N; n++ {
5353
g.Update(42)
5454
}
5555
}
5656

5757
func BenchmarkReportGaugeNoData(b *testing.B) {
58-
g := &gauge{}
58+
g := newGauge("bar", nil)
5959
for n := 0; n < b.N; n++ {
6060
g.report("bar", nil, NullStatsReporter)
6161
}
6262
}
6363

6464
func BenchmarkReportGaugeWithData(b *testing.B) {
65-
g := &gauge{}
65+
g := newGauge("bar", nil)
6666
for n := 0; n < b.N; n++ {
6767
g.Update(73)
6868
g.report("bar", nil, NullStatsReporter)

stats_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func (r *statsTestReporter) Capabilities() Capabilities {
8484
func (r *statsTestReporter) Flush() {}
8585

8686
func TestCounter(t *testing.T) {
87-
counter := newCounter(nil)
87+
counter := newCounter("test", nil)
8888
r := newStatsTestReporter()
8989

9090
counter.Inc(1)
@@ -101,7 +101,7 @@ func TestCounter(t *testing.T) {
101101
}
102102

103103
func TestGauge(t *testing.T) {
104-
gauge := newGauge(nil)
104+
gauge := newGauge("test", nil)
105105
r := newStatsTestReporter()
106106

107107
gauge.Update(42)

0 commit comments

Comments
 (0)