Issue #529: replaced SetConcurrency() with new parameter in New(). Moved semaphore into the cw struct and use defer when using it. Fixed data partitioning logic and separate batch creation from goroutines launching. Improved tests.
Alejandro Pedraza
6 years ago
11 | 11 | "github.com/go-kit/kit/log" |
12 | 12 | "github.com/go-kit/kit/metrics" |
13 | 13 | "github.com/go-kit/kit/metrics/generic" |
14 | ) | |
15 | ||
16 | const ( | |
17 | maxConcurrentRequests = 20 | |
14 | 18 | ) |
15 | 19 | |
16 | 20 | // CloudWatch receives metrics observations and forwards them to CloudWatch. |
20 | 24 | // To regularly report metrics to CloudWatch, use the WriteLoop helper method. |
21 | 25 | type CloudWatch struct { |
22 | 26 | mtx sync.RWMutex |
27 | sem chan struct{} | |
23 | 28 | namespace string |
24 | 29 | numConcurrentRequests int |
25 | 30 | svc cloudwatchiface.CloudWatchAPI |
31 | 36 | |
32 | 37 | // New returns a CloudWatch object that may be used to create metrics. |
33 | 38 | // Namespace is applied to all created metrics and maps to the CloudWatch namespace. |
39 | // NumConcurrent sets the number of simultaneous requests to Amazon. | |
40 | // A good default value is 10 and the maximum is 20. | |
34 | 41 | // Callers must ensure that regular calls to Send are performed, either |
35 | 42 | // manually or with one of the helper methods. |
36 | func New(namespace string, svc cloudwatchiface.CloudWatchAPI, logger log.Logger) *CloudWatch { | |
43 | func New(namespace string, svc cloudwatchiface.CloudWatchAPI, numConcurrent int, logger log.Logger) *CloudWatch { | |
44 | if numConcurrent > maxConcurrentRequests { | |
45 | numConcurrent = maxConcurrentRequests | |
46 | } | |
47 | ||
37 | 48 | return &CloudWatch{ |
49 | sem: make(chan struct{}, numConcurrent), | |
38 | 50 | namespace: namespace, |
39 | numConcurrentRequests: 10, | |
51 | numConcurrentRequests: numConcurrent, | |
40 | 52 | svc: svc, |
41 | 53 | counters: map[string]*counter{}, |
42 | 54 | gauges: map[string]*gauge{}, |
43 | 55 | histograms: map[string]*histogram{}, |
44 | 56 | logger: logger, |
45 | 57 | } |
46 | } | |
47 | ||
48 | // SetConcurrency overrides the default number (10) of concurrent requests sent to CloudWatch. | |
49 | // CloudWatch allows a maximum of 20 metrics to be sent per request, so when Send is called, | |
50 | // we partition the metrics and concurrently call their API. This parameter sets maximum number | |
51 | // of concurrent requests. | |
52 | func (cw *CloudWatch) SetConcurrency(numConcurrentRequests int) *CloudWatch { | |
53 | cw.numConcurrentRequests = numConcurrentRequests | |
54 | return cw | |
55 | 58 | } |
56 | 59 | |
57 | 60 | // NewCounter returns a counter. Observations are aggregated and emitted once |
143 | 146 | } |
144 | 147 | } |
145 | 148 | |
146 | var tokens = make(chan struct{}, cw.numConcurrentRequests) | |
147 | var errors = make(chan error) | |
148 | var n int | |
149 | ||
149 | var batches [][]*cloudwatch.MetricDatum | |
150 | 150 | for len(datums) > 0 { |
151 | 151 | var batch []*cloudwatch.MetricDatum |
152 | lim := min(len(datums), cw.numConcurrentRequests) | |
152 | lim := min(len(datums), maxConcurrentRequests) | |
153 | 153 | batch, datums = datums[:lim], datums[lim:] |
154 | n++ | |
154 | batches = append(batches, batch) | |
155 | } | |
156 | ||
157 | var errors = make(chan error, len(batches)) | |
158 | for _, batch := range batches { | |
155 | 159 | go func(batch []*cloudwatch.MetricDatum) { |
156 | tokens <- struct{}{} | |
160 | cw.sem <- struct{}{} | |
161 | defer func() { | |
162 | <-cw.sem | |
163 | }() | |
157 | 164 | _, err := cw.svc.PutMetricData(&cloudwatch.PutMetricDataInput{ |
158 | 165 | Namespace: aws.String(cw.namespace), |
159 | 166 | MetricData: batch, |
160 | 167 | }) |
161 | <-tokens | |
162 | 168 | errors <- err |
163 | 169 | }(batch) |
164 | 170 | } |
165 | ||
166 | 171 | var firstErr error |
167 | for ; n > 0; n-- { | |
172 | for i := 0; i < cap(errors); i++ { | |
168 | 173 | if err := <-errors; err != nil && firstErr != nil { |
169 | 174 | firstErr = err |
170 | 175 | } |
2 | 2 | import ( |
3 | 3 | "errors" |
4 | 4 | "fmt" |
5 | "strconv" | |
5 | 6 | "sync" |
6 | 7 | "testing" |
7 | 8 | |
64 | 65 | namespace, name := "abc", "def" |
65 | 66 | label, value := "label", "value" |
66 | 67 | svc := newMockCloudWatch() |
67 | cw := New(namespace, svc, log.NewNopLogger()) | |
68 | cw := New(namespace, svc, 10, log.NewNopLogger()) | |
68 | 69 | counter := cw.NewCounter(name).With(label, value) |
69 | 70 | valuef := func() float64 { |
70 | 71 | err := cw.Send() |
85 | 86 | |
86 | 87 | func TestCounterLowSendConcurrency(t *testing.T) { |
87 | 88 | namespace := "abc" |
88 | names := []string{"name1", "name2", "name3", "name4", "name5", "name6"} | |
89 | label, value := "label", "value" | |
89 | var names, labels, values []string | |
90 | for i := 1; i <= 45; i++ { | |
91 | num := strconv.Itoa(i) | |
92 | names = append(names, "name"+num) | |
93 | labels = append(labels, "label"+num) | |
94 | values = append(values, "value"+num) | |
95 | } | |
90 | 96 | svc := newMockCloudWatch() |
91 | cw := New(namespace, svc, log.NewNopLogger()) | |
92 | cw = cw.SetConcurrency(2) | |
97 | cw := New(namespace, svc, 2, log.NewNopLogger()) | |
93 | 98 | |
94 | 99 | counters := make(map[string]metrics.Counter) |
95 | for _, name := range names { | |
96 | counters[name] = cw.NewCounter(name).With(label, value) | |
100 | var wants []float64 | |
101 | for i, name := range names { | |
102 | counters[name] = cw.NewCounter(name).With(labels[i], values[i]) | |
103 | wants = append(wants, teststat.FillCounter(counters[name])) | |
97 | 104 | } |
98 | 105 | |
99 | valuef := func(name string) func() float64 { | |
100 | return func() float64 { | |
101 | err := cw.Send() | |
102 | if err != nil { | |
103 | t.Fatal(err) | |
104 | } | |
105 | svc.mtx.RLock() | |
106 | defer svc.mtx.RUnlock() | |
107 | return svc.valuesReceived[name] | |
108 | } | |
106 | err := cw.Send() | |
107 | if err != nil { | |
108 | t.Fatal(err) | |
109 | 109 | } |
110 | 110 | |
111 | for _, name := range names { | |
112 | if err := teststat.TestCounter(counters[name], valuef(name)); err != nil { | |
113 | t.Fatal(err) | |
111 | for i, name := range names { | |
112 | if svc.valuesReceived[name] != wants[i] { | |
113 | t.Fatal("want %f, have %f", wants[i], svc.valuesReceived[name]) | |
114 | 114 | } |
115 | if err := testDimensions(svc, name, label, value); err != nil { | |
115 | if err := testDimensions(svc, name, labels[i], values[i]); err != nil { | |
116 | 116 | t.Fatal(err) |
117 | 117 | } |
118 | 118 | } |
122 | 122 | namespace, name := "abc", "def" |
123 | 123 | label, value := "label", "value" |
124 | 124 | svc := newMockCloudWatch() |
125 | cw := New(namespace, svc, log.NewNopLogger()) | |
125 | cw := New(namespace, svc, 10, log.NewNopLogger()) | |
126 | 126 | gauge := cw.NewGauge(name).With(label, value) |
127 | 127 | valuef := func() float64 { |
128 | 128 | err := cw.Send() |
145 | 145 | namespace, name := "abc", "def" |
146 | 146 | label, value := "label", "value" |
147 | 147 | svc := newMockCloudWatch() |
148 | cw := New(namespace, svc, log.NewNopLogger()) | |
148 | cw := New(namespace, svc, 10, log.NewNopLogger()) | |
149 | 149 | histogram := cw.NewHistogram(name, 50).With(label, value) |
150 | 150 | n50 := fmt.Sprintf("%s_50", name) |
151 | 151 | n90 := fmt.Sprintf("%s_90", name) |