Skip to content

Commit be42de2

Browse files
committed
[Bug Fix] remove race condition when sending out metrics with EnableOptimizedFlush as true.
calling sync.Map.Range() on the same scope's metric maps simultaneously send scope batches instead of individual scopes
1 parent 7e20868 commit be42de2

2 files changed

Lines changed: 86 additions & 49 deletions

File tree

scope_registry.go

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -526,56 +526,36 @@ func (r *scopeRegistry) parallelProcess(processFn func(*scope)) {
526526
func (r *scopeRegistry) workerPoolProcess(processFn func(*scope), numWorkers int) {
527527
var wg sync.WaitGroup
528528

529-
// Collect all work items synchronously to avoid races
530-
var allWorkItems []workItem
529+
// Create a channel to distribute buckets to workers
530+
bucketChan := make(chan *scopeBucket, len(r.subscopes))
531+
532+
// Send all buckets to the channel
531533
for _, subscopeBucket := range r.subscopes {
532-
scopesToProcess := r.copyBucketScopes(subscopeBucket)
533-
for i, s := range scopesToProcess.scopes {
534-
allWorkItems = append(allWorkItems, workItem{
535-
scope: s,
536-
scopeKey: scopesToProcess.keys[i],
537-
bucket: subscopeBucket,
538-
})
539-
}
534+
bucketChan <- subscopeBucket
540535
}
536+
close(bucketChan)
541537

542-
// Create channels with exact capacity
543-
workChan := make(chan workItem, len(allWorkItems))
544-
closedScopesChan := make(chan workItem, len(allWorkItems))
545-
546-
// Fill work queue synchronously
547-
for _, item := range allWorkItems {
548-
workChan <- item
549-
}
550-
close(workChan)
538+
closedScopes := make(chan scopeClosureInfo, 100)
551539

552-
// Start worker goroutines
540+
// Start worker goroutines that process entire buckets
553541
wg.Add(numWorkers)
554542
for i := 0; i < numWorkers; i++ {
555543
go func() {
556544
defer wg.Done()
557-
for item := range workChan {
558-
processFn(item.scope)
559-
if atomic.LoadInt32(&item.scope.closed) != 0 {
560-
closedScopesChan <- item
561-
}
545+
for bucket := range bucketChan {
546+
r.processBucketWithClosures(bucket, processFn, closedScopes)
562547
}
563548
}()
564549
}
565550

566551
// Wait for workers and close the closed scopes channel
567552
go func() {
568553
wg.Wait()
569-
close(closedScopesChan)
554+
close(closedScopes)
570555
}()
571556

572557
// Handle closed scopes
573-
for item := range closedScopesChan {
574-
item.bucket.mu.Lock()
575-
delete(item.bucket.s, item.scopeKey)
576-
item.bucket.mu.Unlock()
577-
item.scope.clearMetrics()
578-
}
558+
r.handleClosedScopes(closedScopes)
579559
}
580560

581561
// Helper methods for scope processing

scope_registry_test.go

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -529,26 +529,83 @@ func BenchmarkSnapshotMapPooling(b *testing.B) {
529529

530530
// Benchmark the overall subscope creation with all Phase 4 optimizations
531531
func BenchmarkSubscopeCreationPhase4(b *testing.B) {
532-
root, closer := NewRootScope(ScopeOptions{
533-
Prefix: "test",
534-
Reporter: NullStatsReporter,
532+
// Test subscope creation with all optimizations enabled
533+
root := newRootScope(ScopeOptions{
534+
Prefix: "test",
535+
Tags: map[string]string{
536+
"service": "benchmark",
537+
},
535538
}, 0)
536-
defer closer.Close()
537539

538540
tags := map[string]string{
539-
"service": "test-service",
540-
"environment": "production",
541-
"region": "us-west-2",
541+
"endpoint": "/api/v1/test",
542+
"method": "GET",
543+
"status": "200",
542544
}
543545

544-
b.Run("SubscopeCreationWithPhase4", func(b *testing.B) {
545-
b.ResetTimer()
546-
for i := 0; i < b.N; i++ {
547-
subscope := root.Tagged(tags).SubScope("metrics")
548-
// Create some metrics to trigger slice allocations
549-
subscope.Counter("requests").Inc(1)
550-
subscope.Gauge("cpu_usage").Update(50.0)
551-
subscope.Histogram("latency", MustMakeLinearValueBuckets(0, 10, 10)).RecordValue(5.0)
552-
}
553-
})
546+
b.ResetTimer()
547+
for i := 0; i < b.N; i++ {
548+
// Create subscope with tags
549+
_ = root.Tagged(tags)
550+
}
551+
}
552+
553+
func TestOptimizedFlushReportsAllMetrics(t *testing.T) {
554+
// Enable optimized flush
555+
EnableOptimizedFlush()
556+
defer DisableOptimizedFlush()
557+
558+
// Create a test reporter to capture reported metrics
559+
r := newTestStatsReporter()
560+
561+
// Create root scope with the test reporter
562+
root, closer := NewRootScope(ScopeOptions{
563+
Reporter: r,
564+
OmitCardinalityMetrics: true, // Disable cardinality metrics to simplify test
565+
}, 50*time.Millisecond)
566+
defer closer.Close()
567+
568+
// Create multiple scopes and metrics to simulate high cardinality
569+
numScopes := 10
570+
for i := 0; i < numScopes; i++ {
571+
scope := root.Tagged(map[string]string{"instance": fmt.Sprintf("instance-%d", i)})
572+
573+
// Create different types of metrics with unique names
574+
r.cg.Add(1)
575+
counter := scope.Counter(fmt.Sprintf("test_counter_%d", i))
576+
counter.Inc(int64(i + 1))
577+
578+
r.gg.Add(1)
579+
gauge := scope.Gauge(fmt.Sprintf("test_gauge_%d", i))
580+
gauge.Update(float64(i * 2))
581+
582+
r.tg.Add(1)
583+
timer := scope.Timer(fmt.Sprintf("test_timer_%d", i))
584+
timer.Record(time.Duration(i) * time.Millisecond)
585+
586+
r.hg.Add(1)
587+
histogram := scope.Histogram(fmt.Sprintf("test_histogram_%d", i), ValueBuckets{1, 5, 10})
588+
histogram.RecordValue(float64(i))
589+
}
590+
591+
// Wait for all metrics to be reported
592+
r.WaitAll()
593+
594+
// Verify all metric types were reported by checking the maps
595+
counters := r.getCounters()
596+
gauges := r.getGauges()
597+
timers := r.getTimers()
598+
histograms := r.getHistograms()
599+
600+
// Verify all metric types were reported
601+
assert.True(t, len(counters) > 0, "Counters should be reported with optimized flush")
602+
assert.True(t, len(gauges) > 0, "Gauges should be reported with optimized flush")
603+
assert.True(t, len(timers) > 0, "Timers should be reported with optimized flush")
604+
assert.True(t, len(histograms) > 0, "Histograms should be reported with optimized flush")
605+
606+
// Verify we have the expected number of metrics (should be numScopes each)
607+
assert.Equal(t, numScopes, len(counters), "Should have %d counter metrics", numScopes)
608+
assert.Equal(t, numScopes, len(gauges), "Should have %d gauge metrics", numScopes)
609+
assert.Equal(t, numScopes, len(timers), "Should have %d timer metrics", numScopes)
610+
assert.Equal(t, numScopes, len(histograms), "Should have %d histogram metrics", numScopes)
554611
}

0 commit comments

Comments
 (0)