From 55193c30585147e53f96033a888fb9bd4551f195 Mon Sep 17 00:00:00 2001 From: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:54:35 +0100 Subject: [PATCH] promql: fix smoothed interpolation across counter resets Fix incorrect interpolation when counter resets occur in smoothed range selector evaluation. Previously, the asymmetric handling of counter resets (y1=0 on left edge, y2+=y1 on right edge) produced wrong values. Now uniformly set y1=0 when a counter reset is detected, correctly modeling the counter as starting from 0 post-reset. This fixes rate calculations across counter resets. For example, rate(metric[10s] smoothed) where metric goes from 100 to 10 (a reset) now correctly computes 0.666... by treating the counter as resetting to 0 rather than producing inflated values from the old behavior. Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> --- promql/engine.go | 2 +- promql/functions.go | 17 ++++++----------- promql/functions_internal_test.go | 10 +++++----- .../promqltest/testdata/extended_vectors.test | 8 ++++++++ 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index b609dc4f0a..afe82bc38f 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1667,7 +1667,7 @@ func (ev *evaluator) smoothSeries(series []storage.Series, offset time.Duration) // Interpolate between prev and next. // TODO: detect if the sample is a counter, based on __type__ or metadata. prev, next := floats[i-1], floats[i] - val := interpolate(prev, next, ts, false, false) + val := interpolate(prev, next, ts, false) ss.Floats = append(ss.Floats, FPoint{F: val, T: ts}) case i > 0: diff --git a/promql/functions.go b/promql/functions.go index 9c04392232..aad02370f8 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -70,7 +70,7 @@ func funcTime(_ []Vector, _ Matrix, _ parser.Expressions, enh *EvalNodeHelper) ( // it returns the interpolated value at the left boundary; otherwise, it returns the first sample's value. func pickOrInterpolateLeft(floats []FPoint, first int, rangeStart int64, smoothed, isCounter bool) float64 { if smoothed && floats[first].T < rangeStart { - return interpolate(floats[first], floats[first+1], rangeStart, isCounter, true) + return interpolate(floats[first], floats[first+1], rangeStart, isCounter) } return floats[first].F } @@ -80,25 +80,20 @@ func pickOrInterpolateLeft(floats []FPoint, first int, rangeStart int64, smoothe // it returns the interpolated value at the right boundary; otherwise, it returns the last sample's value. func pickOrInterpolateRight(floats []FPoint, last int, rangeEnd int64, smoothed, isCounter bool) float64 { if smoothed && last > 0 && floats[last].T > rangeEnd { - return interpolate(floats[last-1], floats[last], rangeEnd, isCounter, false) + return interpolate(floats[last-1], floats[last], rangeEnd, isCounter) } return floats[last].F } // interpolate performs linear interpolation between two points. -// If isCounter is true and there is a counter reset: -// - on the left edge, it sets the value to 0. -// - on the right edge, it adds the left value to the right value. +// If isCounter is true and there is a counter reset, it models the counter +// as starting from 0 (post-reset) by setting y1 to 0. // It then calculates the interpolated value at the given timestamp. -func interpolate(p1, p2 FPoint, t int64, isCounter, leftEdge bool) float64 { +func interpolate(p1, p2 FPoint, t int64, isCounter bool) float64 { y1 := p1.F y2 := p2.F if isCounter && y2 < y1 { - if leftEdge { - y1 = 0 - } else { - y2 += y1 - } + y1 = 0 } return y1 + (y2-y1)*float64(t-p1.T)/float64(p2.T-p1.T) diff --git a/promql/functions_internal_test.go b/promql/functions_internal_test.go index bb52e4976b..9efd9c3c2e 100644 --- a/promql/functions_internal_test.go +++ b/promql/functions_internal_test.go @@ -108,13 +108,13 @@ func TestInterpolate(t *testing.T) { {FPoint{T: 1, F: 100}, FPoint{T: 2, F: 200}, 1, false, 100}, {FPoint{T: 0, F: 100}, FPoint{T: 2, F: 200}, 1, false, 150}, {FPoint{T: 0, F: 200}, FPoint{T: 2, F: 100}, 1, false, 150}, - {FPoint{T: 0, F: 200}, FPoint{T: 2, F: 0}, 1, true, 200}, - {FPoint{T: 0, F: 200}, FPoint{T: 2, F: 100}, 1, true, 250}, - {FPoint{T: 0, F: 500}, FPoint{T: 2, F: 100}, 1, true, 550}, - {FPoint{T: 0, F: 500}, FPoint{T: 10, F: 0}, 1, true, 500}, + {FPoint{T: 0, F: 200}, FPoint{T: 2, F: 0}, 1, true, 0}, + {FPoint{T: 0, F: 200}, FPoint{T: 2, F: 100}, 1, true, 50}, + {FPoint{T: 0, F: 500}, FPoint{T: 2, F: 100}, 1, true, 50}, + {FPoint{T: 0, F: 500}, FPoint{T: 10, F: 0}, 1, true, 0}, } for _, test := range tests { - result := interpolate(test.p1, test.p2, test.t, test.isCounter, false) + result := interpolate(test.p1, test.p2, test.t, test.isCounter) require.Equal(t, test.expected, result) } } diff --git a/promql/promqltest/testdata/extended_vectors.test b/promql/promqltest/testdata/extended_vectors.test index 8f431dcfd3..0bc1140522 100644 --- a/promql/promqltest/testdata/extended_vectors.test +++ b/promql/promqltest/testdata/extended_vectors.test @@ -358,6 +358,14 @@ load 1m eval instant at 2m15s increase(metric[2m] smoothed) {} 12 +# Smoothed rate interpolation across a counter reset. +clear +load 15s + metric 100 10 + +eval instant at 12s rate(metric[10s] smoothed) + {} 0.666666666666667 + clear eval instant at 1m deriv(foo[3m] smoothed) expect fail msg: smoothed modifier can only be used with: delta, increase, rate - not with deriv