Skip to content

Commit a240a9e

Browse files
committed
awsapplicationsignals: fix concurrency issues in processor
1 parent f51a244 commit a240a9e

4 files changed

Lines changed: 123 additions & 80 deletions

File tree

Makefile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,6 @@ PKG_WITH_DATA_RACE += internal/tls
211211
PKG_WITH_DATA_RACE += plugins/inputs/logfile
212212
PKG_WITH_DATA_RACE += plugins/inputs/logfile/tail
213213
PKG_WITH_DATA_RACE += plugins/outputs/cloudwatch$$
214-
PKG_WITH_DATA_RACE += plugins/processors/awsapplicationsignals
215214
PKG_WITH_DATA_RACE += plugins/processors/ec2tagger
216215
PKG_WITH_DATA_RACE_PATTERN := $(shell echo '$(PKG_WITH_DATA_RACE)' | tr ' ' '|')
217216
test-data-race:

plugins/processors/awsapplicationsignals/internal/cardinalitycontrol/metrics_limiter.go

Lines changed: 77 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ type MetricsLimiter struct {
5151

5252
logger *zap.Logger
5353
ctx context.Context
54-
mapLock sync.RWMutex
55-
services map[string]*service
54+
services sync.Map
5655
}
5756

5857
func NewMetricsLimiter(config *config.LimiterConfig, logger *zap.Logger) Limiter {
@@ -70,7 +69,7 @@ func NewMetricsLimiter(config *config.LimiterConfig, logger *zap.Logger) Limiter
7069

7170
logger: logger,
7271
ctx: ctx,
73-
services: map[string]*service{},
72+
services: sync.Map{},
7473
}
7574

7675
go func() {
@@ -97,18 +96,16 @@ func (m *MetricsLimiter) Admit(metricName string, attributes, resourceAttributes
9796
}
9897
admitted := true
9998

100-
m.mapLock.RLock()
101-
svc := m.services[serviceName]
102-
m.mapLock.RUnlock()
103-
if svc == nil {
104-
m.mapLock.Lock()
105-
svc = m.services[serviceName]
106-
if svc == nil {
107-
svc = newService(serviceName, m.DropThreshold, m.RotationInterval, m.ctx, m.logger)
108-
m.services[serviceName] = svc
99+
val, loaded := m.services.Load(serviceName)
100+
if !loaded {
101+
valToStore := newService(serviceName, m.DropThreshold, m.RotationInterval, m.ctx, m.logger)
102+
val, loaded = m.services.LoadOrStore(serviceName, valToStore)
103+
if loaded {
104+
valToStore.cancelFunc()
105+
m.logger.Info(fmt.Sprintf("[%s] cancel newly created service entry as an existing one is found", serviceName))
109106
}
110-
m.mapLock.Unlock()
111107
}
108+
svc := val.(*service)
112109

113110
metricData := newMetricData(serviceName, metricName, labels)
114111

@@ -118,8 +115,10 @@ func (m *MetricsLimiter) Admit(metricName string, attributes, resourceAttributes
118115
return true, nil
119116
}
120117

121-
if !svc.admitMetricData(metricData) {
122-
svc.rollupMetricData(attributes)
118+
svc.rwLock.Lock()
119+
defer svc.rwLock.Unlock()
120+
if !svc.admitMetricDataLocked(metricData) {
121+
svc.rollupMetricDataLocked(attributes)
123122

124123
svc.totalRollup++
125124
admitted = false
@@ -130,10 +129,6 @@ func (m *MetricsLimiter) Admit(metricName string, attributes, resourceAttributes
130129
}
131130

132131
svc.totalMetricSent++
133-
134-
svc.rwLock.RLock()
135-
defer svc.rwLock.RUnlock()
136-
137132
svc.totalCount++
138133
svc.InsertMetricDataToPrimary(metricData)
139134
svc.InsertMetricDataToSecondary(metricData)
@@ -156,23 +151,15 @@ func (m *MetricsLimiter) filterAWSDeclaredAttributes(attributes, resourceAttribu
156151
}
157152

158153
func (m *MetricsLimiter) removeStaleServices() {
159-
var svcToRemove []string
160-
for name, svc := range m.services {
161-
if svc.rotations > 3 {
162-
if svc.countSnapshot[0] == svc.countSnapshot[1] && svc.countSnapshot[1] == svc.countSnapshot[2] {
163-
svc.cancelFunc()
164-
svcToRemove = append(svcToRemove, name)
165-
}
154+
m.services.Range(func(key, value any) bool {
155+
svc := value.(*service)
156+
if svc.isStale() {
157+
svc.cancelFunc()
158+
m.logger.Info("remove stale service " + key.(string) + ".")
159+
m.services.Delete(key)
166160
}
167-
}
168-
169-
m.mapLock.Lock()
170-
defer m.mapLock.Unlock()
171-
172-
for _, name := range svcToRemove {
173-
m.logger.Info("remove stale service " + name + ".")
174-
delete(m.services, name)
175-
}
161+
return true
162+
})
176163
}
177164

178165
type service struct {
@@ -290,7 +277,7 @@ func (t *topKMetrics) Push(oldMetric, newMetric *MetricData) {
290277
// Check if this oldMetric is the new minimum, find the new minMetric after the updates
291278
if t.minMetric.hashKey == hashValue {
292279
// Find the new minMetrics after update the frequency
293-
t.minMetric = t.findMinMetric()
280+
t.minMetric = t.findMinMetricLocked()
294281
}
295282
return
296283
}
@@ -300,7 +287,7 @@ func (t *topKMetrics) Push(oldMetric, newMetric *MetricData) {
300287
if newMetric.frequency > t.minMetric.frequency {
301288
delete(t.metricMap, t.minMetric.hashKey)
302289
t.metricMap[hashValue] = newMetric
303-
t.minMetric = t.findMinMetric()
290+
t.minMetric = t.findMinMetricLocked()
304291
}
305292
} else {
306293
// Check if this newMetric is the new minimum.
@@ -311,8 +298,17 @@ func (t *topKMetrics) Push(oldMetric, newMetric *MetricData) {
311298
}
312299
}
313300

314-
// findMinMetric removes and returns the key-value pair with the minimum value.
315-
func (t *topKMetrics) findMinMetric() *MetricData {
301+
func (t *topKMetrics) Admit(metric *MetricData) bool {
302+
_, found := t.metricMap[metric.hashKey]
303+
if len(t.metricMap) < t.sizeLimit || found {
304+
return true
305+
}
306+
return false
307+
}
308+
309+
// findMinMetricLocked removes and returns the key-value pair with the minimum value.
310+
// It assumes the caller already holds the read/write lock.
311+
func (t *topKMetrics) findMinMetricLocked() *MetricData {
316312
// Find the new minimum metric and smallest frequency.
317313
var newMinMetric *MetricData
318314
smallestFrequency := int(^uint(0) >> 1) // Initialize with the maximum possible integer value
@@ -326,15 +322,11 @@ func (t *topKMetrics) findMinMetric() *MetricData {
326322
return newMinMetric
327323
}
328324

329-
func (s *service) admitMetricData(metric *MetricData) bool {
330-
_, found := s.primaryTopK.metricMap[metric.hashKey]
331-
if len(s.primaryTopK.metricMap) < s.primaryTopK.sizeLimit || found {
332-
return true
333-
}
334-
return false
325+
func (s *service) admitMetricDataLocked(metric *MetricData) bool {
326+
return s.primaryTopK.Admit(metric)
335327
}
336328

337-
func (s *service) rollupMetricData(attributes pcommon.Map) {
329+
func (s *service) rollupMetricDataLocked(attributes pcommon.Map) {
338330
for _, indexAttr := range awsDeclaredMetricAttributes {
339331
if (indexAttr == common.CWMetricAttributeEnvironment) || (indexAttr == common.CWMetricAttributeLocalService) || (indexAttr == common.CWMetricAttributeRemoteService) {
340332
continue
@@ -349,6 +341,44 @@ func (s *service) rollupMetricData(attributes pcommon.Map) {
349341
}
350342
}
351343

344+
func (s *service) rotateVisitRecords() error {
345+
s.rwLock.Lock()
346+
defer s.rwLock.Unlock()
347+
348+
cmsDepth := s.primaryCMS.depth
349+
cmsWidth := s.primaryCMS.width
350+
topKLimit := s.primaryTopK.sizeLimit
351+
352+
nextPrimaryCMS := s.secondaryCMS
353+
nextPrimaryTopK := s.secondaryTopK
354+
355+
s.secondaryCMS = NewCountMinSketch(cmsDepth, cmsWidth)
356+
s.secondaryTopK = newTopKMetrics(topKLimit)
357+
358+
if nextPrimaryCMS != nil && nextPrimaryTopK != nil {
359+
s.primaryCMS = nextPrimaryCMS
360+
s.primaryTopK = nextPrimaryTopK
361+
} else {
362+
s.logger.Info(fmt.Sprintf("[%s] secondary visit records are nil.", s.name))
363+
}
364+
365+
s.countSnapshot[s.rotations%3] = s.totalCount
366+
s.rotations++
367+
368+
return nil
369+
}
370+
371+
func (s *service) isStale() bool {
372+
s.rwLock.RLock()
373+
defer s.rwLock.RUnlock()
374+
if s.rotations > 3 {
375+
if s.countSnapshot[0] == s.countSnapshot[1] && s.countSnapshot[1] == s.countSnapshot[2] {
376+
return true
377+
}
378+
}
379+
return false
380+
}
381+
352382
// As a starting point, you can use rules of thumb, such as setting the depth to be around 4-6 times the logarithm of the expected number of distinct items and the width based on your memory constraints. However, these are rough guidelines, and the optimal size will depend on your unique application and requirements.
353383
func newService(name string, limit int, rotationInterval time.Duration, parentCtx context.Context, logger *zap.Logger) *service {
354384
depth := defaultCMSDepth
@@ -374,7 +404,7 @@ func newService(name string, limit int, rotationInterval time.Duration, parentCt
374404
select {
375405
case <-rotationTicker.C:
376406
svc.logger.Info(fmt.Sprintf("[%s] rotating visit records, current rotation %d", name, svc.rotations))
377-
if err := rotateVisitRecords(svc); err != nil {
407+
if err := svc.rotateVisitRecords(); err != nil {
378408
svc.logger.Error(fmt.Sprintf("[%s] failed to rotate visit records.", name), zap.Error(err))
379409
}
380410
case <-ctx.Done():
@@ -389,30 +419,3 @@ func newService(name string, limit int, rotationInterval time.Duration, parentCt
389419
svc.logger.Info(fmt.Sprintf("[%s] service entry is created.\n", name))
390420
return svc
391421
}
392-
393-
func rotateVisitRecords(svc *service) error {
394-
svc.rwLock.Lock()
395-
defer svc.rwLock.Unlock()
396-
397-
cmsDepth := svc.primaryCMS.depth
398-
cmsWidth := svc.primaryCMS.width
399-
topKLimit := svc.primaryTopK.sizeLimit
400-
401-
nextPrimaryCMS := svc.secondaryCMS
402-
nextPrimaryTopK := svc.secondaryTopK
403-
404-
svc.secondaryCMS = NewCountMinSketch(cmsDepth, cmsWidth)
405-
svc.secondaryTopK = newTopKMetrics(topKLimit)
406-
407-
if nextPrimaryCMS != nil && nextPrimaryTopK != nil {
408-
svc.primaryCMS = nextPrimaryCMS
409-
svc.primaryTopK = nextPrimaryTopK
410-
} else {
411-
svc.logger.Info(fmt.Sprintf("[%s] secondary visit records are nil.", svc.name))
412-
}
413-
414-
svc.countSnapshot[svc.rotations%3] = svc.totalCount
415-
svc.rotations++
416-
417-
return nil
418-
}

plugins/processors/awsapplicationsignals/internal/cardinalitycontrol/metrics_limiter_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,12 @@ func TestClearStaleService(t *testing.T) {
164164
cancel()
165165

166166
metricsLimiter := limiter.(*MetricsLimiter)
167-
assert.Equal(t, 0, len(metricsLimiter.services))
167+
serviceCount := 0
168+
metricsLimiter.services.Range(func(k, v interface{}) bool {
169+
serviceCount++
170+
return true
171+
})
172+
assert.Equal(t, 0, serviceCount)
168173
}
169174

170175
func TestInheritanceAfterRotation(t *testing.T) {
@@ -220,14 +225,18 @@ func TestRotationInterval(t *testing.T) {
220225
// wait for secondary to be created
221226
time.Sleep(7 * time.Second)
222227
for i := 0; i < 5; i++ {
228+
svc.rwLock.Lock()
223229
svc.secondaryCMS.matrix[0][0] = 1
230+
svc.rwLock.Unlock()
224231

225232
// wait for rotation
226233
time.Sleep(5 * time.Second)
227234

228235
// verify secondary is promoted to primary
236+
svc.rwLock.Lock()
229237
assert.Equal(t, 0, svc.secondaryCMS.matrix[0][0])
230238
assert.Equal(t, 1, svc.primaryCMS.matrix[0][0])
239+
svc.rwLock.Unlock()
231240
}
232241
}
233242

plugins/processors/awsapplicationsignals/processor_test.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ package awsapplicationsignals
55

66
import (
77
"context"
8+
"fmt"
89
"sync"
910
"testing"
11+
"time"
1012

11-
"github.com/stretchr/testify/assert"
1213
"go.opentelemetry.io/collector/pdata/pmetric"
1314
"go.opentelemetry.io/collector/pdata/ptrace"
1415
"go.uber.org/zap"
16+
"golang.org/x/exp/rand"
17+
18+
"github.com/stretchr/testify/assert"
1519

20+
"github.com/aws/amazon-cloudwatch-agent/plugins/processors/awsapplicationsignals/common"
1621
"github.com/aws/amazon-cloudwatch-agent/plugins/processors/awsapplicationsignals/config"
1722
"github.com/aws/amazon-cloudwatch-agent/plugins/processors/awsapplicationsignals/rules"
1823
)
@@ -134,31 +139,58 @@ func TestProcessMetricsLowercase(t *testing.T) {
134139

135140
func TestProcessMetricsWithConcurrency(t *testing.T) {
136141
logger, _ := zap.NewDevelopment()
142+
ctx := context.Background()
137143
ap := &awsapplicationsignalsprocessor{
138144
logger: logger,
139145
config: &config.Config{
140146
Resolvers: []config.Resolver{config.NewGenericResolver("")},
141-
Rules: testRules,
147+
Rules: []rules.Rule{},
148+
Limiter: &config.LimiterConfig{
149+
Threshold: 2,
150+
Disabled: false,
151+
LogDroppedMetrics: false,
152+
RotationInterval: 10 * time.Millisecond,
153+
GarbageCollectionInterval: 20 * time.Millisecond,
154+
ParentContext: ctx,
155+
},
142156
},
143157
}
144158

145-
ctx := context.Background()
146159
ap.StartMetrics(ctx, nil)
147160

148161
var wg sync.WaitGroup
149-
for i := 0; i < 100; i++ {
162+
for i := 0; i < 10000; i++ {
150163
wg.Add(1)
151164
go func() {
152165
defer wg.Done()
166+
167+
time.Sleep(time.Duration(rand.Intn(50)*100) * time.Millisecond)
168+
153169
lowercaseMetrics := pmetric.NewMetrics()
154170
errorMetric := lowercaseMetrics.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty()
155171
errorMetric.SetName("error")
172+
errorGauge := errorMetric.SetEmptyGauge().DataPoints().AppendEmpty()
173+
errorGauge.SetIntValue(1)
174+
errorGauge.Attributes().PutStr("Telemetry.Source", "UnitTest")
175+
errorGauge.Attributes().PutStr(common.CWMetricAttributeLocalService, fmt.Sprintf("UnitTest%d", rand.Intn(200)))
156176
latencyMetric := lowercaseMetrics.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty()
157177
latencyMetric.SetName("latency")
178+
histogram := latencyMetric.SetEmptyExponentialHistogram().DataPoints().AppendEmpty()
179+
histogram.SetSum(1)
180+
histogram.SetCount(1)
181+
histogram.SetMin(0)
182+
histogram.SetMax(1)
183+
histogram.Attributes().PutStr("Telemetry.Source", "UnitTest")
184+
histogram.Attributes().PutStr(common.CWMetricAttributeLocalService, fmt.Sprintf("UnitTest%d", rand.Intn(200)))
158185
faultMetric := lowercaseMetrics.ResourceMetrics().AppendEmpty().ScopeMetrics().AppendEmpty().Metrics().AppendEmpty()
186+
faultGauge := faultMetric.SetEmptyGauge().DataPoints().AppendEmpty()
187+
faultGauge.SetIntValue(1)
188+
faultGauge.Attributes().PutStr("Telemetry.Source", "UnitTest")
189+
faultGauge.Attributes().PutStr(common.CWMetricAttributeLocalService, fmt.Sprintf("UnitTest%d", rand.Intn(200)))
159190
faultMetric.SetName("fault")
160191

161192
ap.processMetrics(ctx, lowercaseMetrics)
193+
162194
assert.Equal(t, "Error", lowercaseMetrics.ResourceMetrics().At(0).ScopeMetrics().At(0).Metrics().At(0).Name())
163195
assert.Equal(t, "Latency", lowercaseMetrics.ResourceMetrics().At(1).ScopeMetrics().At(0).Metrics().At(0).Name())
164196
assert.Equal(t, "Fault", lowercaseMetrics.ResourceMetrics().At(2).ScopeMetrics().At(0).Metrics().At(0).Name())

0 commit comments

Comments
 (0)