From 000cb3d11fd260b818ff035292a321b1de1f6a6b Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Sat, 18 Apr 2026 17:07:29 -0700 Subject: [PATCH 1/2] Align yrate/yincrease/ydelta with Prometheus 3.x range semantics Flip the yrate-family range convention from [start, end) to (start, end] so that a sample whose timestamp lands exactly on a range boundary is attributed to the later range, never to both or neither. This matches the convention adopted upstream in Prometheus 3.x (see prometheus/prometheus#13213). The xrate family, whose legacy [start, end] convention predates this decision, is unaffected. Behavior change is confined to samples that land precisely on a range boundary; all existing yrate test cases use shifted eval times to avoid boundary alignment, so only the "Comparison of rate vs xrate" showcase (which intentionally evaluates on boundaries at 25s and 75s) observes a difference. The linearity property yIncrease(p0) + yIncrease(p1) == yIncrease(p0 + p1) is preserved under the new semantics. Made-with: Cursor --- promql/functions.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/promql/functions.go b/promql/functions.go index abdabda2645..43feab1ec12 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -338,7 +338,10 @@ func extendedRate(vals []parser.Value, args parser.Expressions, enh *EvalNodeHel // yIncrease is a utility function for yincrease/yrate/ydelta. // It calculates the increase of the range (allowing for counter resets if isCounter is true), // taking into account the sample at the end of the previous range (just before rangeStartMsec). -// It returns the result across the range [rangeStartMsec, rangeEndMsec). +// It returns the result across the range (rangeStartMsec, rangeEndMsec]. The left-open, +// right-closed convention matches the Prometheus 3.x range-selector semantics +// (see prometheus/prometheus#13213) so that a sample whose timestamp lands exactly on +// a range boundary is attributed to the later range, never to both or neither. // It always extends the preceding sample's value until the next sample, including the // unwritten origin sample value at the start of every time series. // @@ -357,8 +360,8 @@ func yIncrease(points []FPoint, rangeStartMsec, rangeEndMsec int64, isCounter bo // The points are in time order, so we can just walk the list once and remember the last values // seen "before" and "in" range. If there are no values in range, we use the last value before range // so that the increase is 0. - for i := 0; i < len(points) && points[i].T < rangeEndMsec; i++ { // Only consider points in [rangeStartMsec, rangeEndMsec). - if points[i].T >= rangeStartMsec { + for i := 0; i < len(points) && points[i].T <= rangeEndMsec; i++ { // Only consider points in (rangeStartMsec, rangeEndMsec]. + if points[i].T > rangeStartMsec { if isCounter && points[i].F < lastInRange { // Counter reset (process restart). inRangeRestartSkew += lastInRange } @@ -373,8 +376,8 @@ func yIncrease(points []FPoint, rangeStartMsec, rangeEndMsec int64, isCounter bo // rangeFromSelectors extracts points, rangeStartMsec, rangeEndMsec, and rangeSeconds // from the common (Matrix, MatrixSelector) arguments supplied to yincrease/yrate/ydelta. -// The range is [rangeStartMsec, rangeEndMsec). That is, every sample in range has the property: -// rangeStartMsec <= sample.T < rangeEndMsec. +// The range is (rangeStartMsec, rangeEndMsec]. That is, every sample in range has the property: +// rangeStartMsec < sample.T <= rangeEndMsec. func rangeFromSelectors(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) ([]FPoint, int64, int64, float64) { ms := args[0].(*parser.MatrixSelector) vs := ms.VectorSelector.(*parser.VectorSelector) From 8e94af7c22038768f6f3fac1ef7166a1b7afe67a Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Sat, 18 Apr 2026 17:07:29 -0700 Subject: [PATCH 2/2] Refresh yrate showcase expectations for (start, end] semantics Three values in the "Comparison of rate vs xrate" showcase change under the new yrate range convention, all on boundary-aligned evals: eval 25s yrate[50s] /bar: 0.1 -> 0.12 (sample at t=25 now in range) eval 75s yrate[50s] /foo: 0.06 -> 0.02 (sample at t=25 now attributed eval 75s yrate[50s] /bar: 0.22 -> 0.1 to the prior range) These changes are the intended effect of attributing a boundary sample to the later range rather than the earlier one. All other yrate/ yincrease/ydelta cases (big 1000+/2000+ block, counter-reset block, ydelta block) use shifted eval times and are unaffected. Made-with: Cursor Co-authored-by: Cursor --- promql/promqltest/testdata/functions.test | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/promql/promqltest/testdata/functions.test b/promql/promqltest/testdata/functions.test index 7695f36abcb..8739fc8824c 100644 --- a/promql/promqltest/testdata/functions.test +++ b/promql/promqltest/testdata/functions.test @@ -25,7 +25,7 @@ eval instant at 25s xrate(http_requests[50s]) eval instant at 25s yrate(http_requests[50s]) {path="/foo"} 0.04 - {path="/bar"} 0.42 + {path="/bar"} 0.52 # 2. Eval 1 second earlier compared to (1). # * path="/foo" rate should be same or fractionally higher ("shorter" sample, same actual increase); @@ -74,8 +74,8 @@ eval instant at 75s xrate(http_requests[50s]) {path="/bar"} 0.8 eval instant at 75s yrate(http_requests[50s]) - {path="/foo"} 0.06 - {path="/bar"} 1.32 + {path="/foo"} 0.02 + {path="/bar"} 0.8 # 5. Eval 1s earlier compared to (4). # * path="/foo" rate should be same or fractionally lower ("longer" sample, same actual increase).