Skip to content

Commit eb28005

Browse files
jmacdMrAlias
andauthored
Metric Accumulator fix for SumObservers (open-telemetry#1381)
* Let SynchronizedMove(nil) reset and discard * Add common test for SynchronizedMove(nil) * End-to-end test for the Processor and SumObserver * Implement SynchronizedMove(nil) six ways * Lint * Changelog * Test no reset for wrong aggregator type; Fix four Aggregators * Cleanup * imports Co-authored-by: Tyler Yahn <[email protected]>
1 parent 970755b commit eb28005

File tree

18 files changed

+333
-64
lines changed

18 files changed

+333
-64
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1919
- `NewExporter` and `Start` functions in `go.opentelemetry.io/otel/exporters/otlp` now receive `context.Context` as a first parameter. (#1357)
2020
- Zipkin exporter relies on the status code for success rather than body read but still read the response body. (#1328)
2121

22+
### Fixed
23+
24+
- Metric SDK `SumObserver` and `UpDownSumObserver` instruments correctness fixes. (#1381)
25+
2226
## [0.14.0] - 2020-11-19
2327

2428
### Added

sdk/export/metric/metric.go

+3
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ type Aggregator interface {
174174
//
175175
// This call has no Context argument because it is expected to
176176
// perform only computation.
177+
//
178+
// When called with a nil `destination`, this Aggregator is reset
179+
// and the current value is discarded.
177180
SynchronizedMove(destination Aggregator, descriptor *metric.Descriptor) error
178181

179182
// Merge combines the checkpointed state from the argument

sdk/metric/aggregator/aggregatortest/test.go

+118
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,20 @@ package aggregatortest // import "go.opentelemetry.io/otel/sdk/metric/aggregator
1616

1717
import (
1818
"context"
19+
"errors"
1920
"math/rand"
2021
"os"
2122
"sort"
2223
"testing"
2324
"unsafe"
2425

26+
"github.com/stretchr/testify/require"
27+
2528
ottest "go.opentelemetry.io/otel/internal/testing"
2629
"go.opentelemetry.io/otel/metric"
2730
"go.opentelemetry.io/otel/metric/number"
2831
export "go.opentelemetry.io/otel/sdk/export/metric"
32+
"go.opentelemetry.io/otel/sdk/export/metric/aggregation"
2933
"go.opentelemetry.io/otel/sdk/metric/aggregator"
3034
)
3135

@@ -36,6 +40,12 @@ type Profile struct {
3640
Random func(sign int) number.Number
3741
}
3842

43+
type NoopAggregator struct{}
44+
type NoopAggregation struct{}
45+
46+
var _ export.Aggregator = NoopAggregator{}
47+
var _ aggregation.Aggregation = NoopAggregation{}
48+
3949
func newProfiles() []Profile {
4050
rnd := rand.New(rand.NewSource(rand.Int63()))
4151
return []Profile{
@@ -172,3 +182,111 @@ func CheckedMerge(t *testing.T, aggInto, aggFrom export.Aggregator, descriptor *
172182
t.Error("Unexpected Merge failure", err)
173183
}
174184
}
185+
186+
func (NoopAggregation) Kind() aggregation.Kind {
187+
return aggregation.Kind("Noop")
188+
}
189+
190+
func (NoopAggregator) Aggregation() aggregation.Aggregation {
191+
return NoopAggregation{}
192+
}
193+
194+
func (NoopAggregator) Update(context.Context, number.Number, *metric.Descriptor) error {
195+
return nil
196+
}
197+
198+
func (NoopAggregator) SynchronizedMove(export.Aggregator, *metric.Descriptor) error {
199+
return nil
200+
}
201+
202+
func (NoopAggregator) Merge(export.Aggregator, *metric.Descriptor) error {
203+
return nil
204+
}
205+
206+
func SynchronizedMoveResetTest(t *testing.T, mkind metric.InstrumentKind, nf func(*metric.Descriptor) export.Aggregator) {
207+
t.Run("reset on nil", func(t *testing.T) {
208+
// Ensures that SynchronizedMove(nil, descriptor) discards and
209+
// resets the aggregator.
210+
RunProfiles(t, func(t *testing.T, profile Profile) {
211+
descriptor := NewAggregatorTest(
212+
mkind,
213+
profile.NumberKind,
214+
)
215+
agg := nf(descriptor)
216+
217+
for i := 0; i < 10; i++ {
218+
x1 := profile.Random(+1)
219+
CheckedUpdate(t, agg, x1, descriptor)
220+
}
221+
222+
require.NoError(t, agg.SynchronizedMove(nil, descriptor))
223+
224+
if count, ok := agg.(aggregation.Count); ok {
225+
c, err := count.Count()
226+
require.Equal(t, int64(0), c)
227+
require.NoError(t, err)
228+
}
229+
230+
if sum, ok := agg.(aggregation.Sum); ok {
231+
s, err := sum.Sum()
232+
require.Equal(t, number.Number(0), s)
233+
require.NoError(t, err)
234+
}
235+
236+
if lv, ok := agg.(aggregation.LastValue); ok {
237+
v, _, err := lv.LastValue()
238+
require.Equal(t, number.Number(0), v)
239+
require.Error(t, err)
240+
require.True(t, errors.Is(err, aggregation.ErrNoData))
241+
}
242+
})
243+
})
244+
245+
t.Run("no reset on incorrect type", func(t *testing.T) {
246+
// Ensures that SynchronizedMove(wrong_type, descriptor) does not
247+
// reset the aggregator.
248+
RunProfiles(t, func(t *testing.T, profile Profile) {
249+
descriptor := NewAggregatorTest(
250+
mkind,
251+
profile.NumberKind,
252+
)
253+
agg := nf(descriptor)
254+
255+
var input number.Number
256+
const inval = 100
257+
if profile.NumberKind == number.Int64Kind {
258+
input = number.NewInt64Number(inval)
259+
} else {
260+
input = number.NewFloat64Number(inval)
261+
}
262+
263+
CheckedUpdate(t, agg, input, descriptor)
264+
265+
err := agg.SynchronizedMove(NoopAggregator{}, descriptor)
266+
require.Error(t, err)
267+
require.True(t, errors.Is(err, aggregation.ErrInconsistentType))
268+
269+
// Test that the aggregator was not reset
270+
271+
if count, ok := agg.(aggregation.Count); ok {
272+
c, err := count.Count()
273+
require.Equal(t, int64(1), c)
274+
require.NoError(t, err)
275+
}
276+
277+
if sum, ok := agg.(aggregation.Sum); ok {
278+
s, err := sum.Sum()
279+
require.Equal(t, input, s)
280+
require.NoError(t, err)
281+
}
282+
283+
if lv, ok := agg.(aggregation.LastValue); ok {
284+
v, _, err := lv.LastValue()
285+
require.Equal(t, input, v)
286+
require.NoError(t, err)
287+
}
288+
289+
})
290+
})
291+
292+
}

sdk/metric/aggregator/array/array.go

+11-4
Original file line numberDiff line numberDiff line change
@@ -97,20 +97,27 @@ func (c *Aggregator) Points() ([]number.Number, error) {
9797
// the empty set, taking a lock to prevent concurrent Update() calls.
9898
func (c *Aggregator) SynchronizedMove(oa export.Aggregator, desc *metric.Descriptor) error {
9999
o, _ := oa.(*Aggregator)
100-
if o == nil {
100+
101+
if oa != nil && o == nil {
101102
return aggregator.NewInconsistentAggregatorError(c, oa)
102103
}
103104

104105
c.lock.Lock()
105-
o.points, c.points = c.points, nil
106-
o.sum, c.sum = c.sum, 0
106+
if o != nil {
107+
o.points = c.points
108+
o.sum = c.sum
109+
}
110+
c.points = nil
111+
c.sum = 0
107112
c.lock.Unlock()
108113

109114
// TODO: This sort should be done lazily, only when quantiles
110115
// are requested. The SDK specification says you can use this
111116
// aggregator to simply list values in the order they were
112117
// received as an alternative to requesting quantile information.
113-
o.sort(desc.NumberKind())
118+
if o != nil {
119+
o.sort(desc.NumberKind())
120+
}
114121
return nil
115122
}
116123

sdk/metric/aggregator/array/array_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"go.opentelemetry.io/otel/metric"
2626
"go.opentelemetry.io/otel/metric/number"
27+
export "go.opentelemetry.io/otel/sdk/export/metric"
2728
"go.opentelemetry.io/otel/sdk/export/metric/aggregation"
2829
"go.opentelemetry.io/otel/sdk/metric/aggregator/aggregatortest"
2930
)
@@ -329,3 +330,13 @@ func TestArrayFloat64(t *testing.T) {
329330
require.Equal(t, all.Points()[i], po[i], "Wrong point at position %d", i)
330331
}
331332
}
333+
334+
func TestSynchronizedMoveReset(t *testing.T) {
335+
aggregatortest.SynchronizedMoveResetTest(
336+
t,
337+
metric.ValueRecorderInstrumentKind,
338+
func(desc *metric.Descriptor) export.Aggregator {
339+
return &New(1)[0]
340+
},
341+
)
342+
}

sdk/metric/aggregator/ddsketch/ddsketch.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,17 @@ func (c *Aggregator) toNumber(f float64) number.Number {
117117
// a new sketch, taking a lock to prevent concurrent Update() calls.
118118
func (c *Aggregator) SynchronizedMove(oa export.Aggregator, _ *metric.Descriptor) error {
119119
o, _ := oa.(*Aggregator)
120-
if o == nil {
120+
121+
if oa != nil && o == nil {
121122
return aggregator.NewInconsistentAggregatorError(c, oa)
122123
}
123-
replace := sdk.NewDDSketch(c.cfg)
124124

125+
replace := sdk.NewDDSketch(c.cfg)
125126
c.lock.Lock()
126-
o.sketch, c.sketch = c.sketch, replace
127+
if o != nil {
128+
o.sketch = c.sketch
129+
}
130+
c.sketch = replace
127131
c.lock.Unlock()
128132

129133
return nil

sdk/metric/aggregator/ddsketch/ddsketch_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/stretchr/testify/require"
2323

2424
"go.opentelemetry.io/otel/metric"
25+
export "go.opentelemetry.io/otel/sdk/export/metric"
2526
"go.opentelemetry.io/otel/sdk/export/metric/aggregation"
2627
"go.opentelemetry.io/otel/sdk/metric/aggregator/aggregatortest"
2728
)
@@ -208,3 +209,13 @@ func TestDDSketchMerge(t *testing.T) {
208209
})
209210
}
210211
}
212+
213+
func TestSynchronizedMoveReset(t *testing.T) {
214+
aggregatortest.SynchronizedMoveResetTest(
215+
t,
216+
metric.ValueRecorderInstrumentKind,
217+
func(desc *metric.Descriptor) export.Aggregator {
218+
return &New(1, desc, NewDefaultConfig())[0]
219+
},
220+
)
221+
}

sdk/metric/aggregator/histogram/histogram.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,18 @@ func (c *Aggregator) Histogram() (aggregation.Buckets, error) {
118118
// other.
119119
func (c *Aggregator) SynchronizedMove(oa export.Aggregator, desc *metric.Descriptor) error {
120120
o, _ := oa.(*Aggregator)
121-
if o == nil {
121+
122+
if oa != nil && o == nil {
122123
return aggregator.NewInconsistentAggregatorError(c, oa)
123124
}
124125

125126
c.lock.Lock()
126-
o.state, c.state = c.state, emptyState(c.boundaries)
127+
if o != nil {
128+
o.state = c.state
129+
}
130+
c.state = emptyState(c.boundaries)
127131
c.lock.Unlock()
132+
128133
return nil
129134
}
130135

sdk/metric/aggregator/histogram/histogram_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"go.opentelemetry.io/otel/metric"
2626
"go.opentelemetry.io/otel/metric/number"
27+
export "go.opentelemetry.io/otel/sdk/export/metric"
2728
"go.opentelemetry.io/otel/sdk/metric/aggregator/aggregatortest"
2829
"go.opentelemetry.io/otel/sdk/metric/aggregator/histogram"
2930
)
@@ -249,3 +250,13 @@ func calcBuckets(points []number.Number, profile aggregatortest.Profile) []uint6
249250

250251
return counts
251252
}
253+
254+
func TestSynchronizedMoveReset(t *testing.T) {
255+
aggregatortest.SynchronizedMoveResetTest(
256+
t,
257+
metric.ValueRecorderInstrumentKind,
258+
func(desc *metric.Descriptor) export.Aggregator {
259+
return &histogram.New(1, desc, boundaries)[0]
260+
},
261+
)
262+
}

sdk/metric/aggregator/lastvalue/lastvalue.go

+4
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ func (g *Aggregator) LastValue() (number.Number, time.Time, error) {
9393

9494
// SynchronizedMove atomically saves the current value.
9595
func (g *Aggregator) SynchronizedMove(oa export.Aggregator, _ *metric.Descriptor) error {
96+
if oa == nil {
97+
atomic.StorePointer(&g.value, unsafe.Pointer(unsetLastValue))
98+
return nil
99+
}
96100
o, _ := oa.(*Aggregator)
97101
if o == nil {
98102
return aggregator.NewInconsistentAggregatorError(g, oa)

sdk/metric/aggregator/lastvalue/lastvalue_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,13 @@ func TestLastValueNotSet(t *testing.T) {
132132

133133
checkZero(t, g)
134134
}
135+
136+
func TestSynchronizedMoveReset(t *testing.T) {
137+
aggregatortest.SynchronizedMoveResetTest(
138+
t,
139+
metric.ValueObserverInstrumentKind,
140+
func(desc *metric.Descriptor) export.Aggregator {
141+
return &New(1)[0]
142+
},
143+
)
144+
}

sdk/metric/aggregator/minmaxsumcount/mmsc.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,15 @@ func (c *Aggregator) Max() (number.Number, error) {
106106
// the empty set.
107107
func (c *Aggregator) SynchronizedMove(oa export.Aggregator, desc *metric.Descriptor) error {
108108
o, _ := oa.(*Aggregator)
109-
if o == nil {
109+
110+
if oa != nil && o == nil {
110111
return aggregator.NewInconsistentAggregatorError(c, oa)
111112
}
112-
113-
// TODO: It is incorrect to use an Aggregator of different
114-
// kind. Should we test that o.kind == c.kind? (The same question
115-
// occurs for several of the other aggregators in ../*.)
116113
c.lock.Lock()
117-
o.state, c.state = c.state, emptyState(c.kind)
114+
if o != nil {
115+
o.state = c.state
116+
}
117+
c.state = emptyState(c.kind)
118118
c.lock.Unlock()
119119

120120
return nil

sdk/metric/aggregator/minmaxsumcount/mmsc_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"go.opentelemetry.io/otel/metric"
2626
"go.opentelemetry.io/otel/metric/number"
27+
export "go.opentelemetry.io/otel/sdk/export/metric"
2728
"go.opentelemetry.io/otel/sdk/export/metric/aggregation"
2829
"go.opentelemetry.io/otel/sdk/metric/aggregator/aggregatortest"
2930
)
@@ -235,3 +236,13 @@ func TestMaxSumCountNotSet(t *testing.T) {
235236
require.Equal(t, number.Number(0), max)
236237
})
237238
}
239+
240+
func TestSynchronizedMoveReset(t *testing.T) {
241+
aggregatortest.SynchronizedMoveResetTest(
242+
t,
243+
metric.ValueRecorderInstrumentKind,
244+
func(desc *metric.Descriptor) export.Aggregator {
245+
return &New(1, desc)[0]
246+
},
247+
)
248+
}

sdk/metric/aggregator/sum/sum.go

+4
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ func (c *Aggregator) Sum() (number.Number, error) {
6161
// SynchronizedMove atomically saves the current value into oa and resets the
6262
// current sum to zero.
6363
func (c *Aggregator) SynchronizedMove(oa export.Aggregator, _ *metric.Descriptor) error {
64+
if oa == nil {
65+
c.value.SetRawAtomic(0)
66+
return nil
67+
}
6468
o, _ := oa.(*Aggregator)
6569
if o == nil {
6670
return aggregator.NewInconsistentAggregatorError(c, oa)

0 commit comments

Comments
 (0)