Repository navigation
feat: declare non-negative gauges so per-series quantile sketches compose through avg (#772) - #780
Merged
Merged
Conversation
…s compose through avg (#772) - SourceFacts::values_non_negative declares every sample of a metric non-negative, for a gauge as well as a counter (default false, omitted from JSON when false). - Plan selection binds the sign of a metric's samples (values_non_negative[m]) to proven when the metric is declared a counter or declared non-negative. - A quantile estimate over a metric's raw samples now carries that metric's sign statistic, so a per-series DDSketch's relative error composes through sum and avg once the sign is declared. - Test: {avg,sum,max} by (region) (quantile_over_time(0.99, x[100s])) with x a gauge, with and without the declaration. - Docs: the new field in the library API and the input/output workflow.
samples_of_metric now passes through an offset-only TimeShift: it moves the window, not the samples, so their sign is the metric's. With it, topk(10, sum by (job) (rate(data_total[5m] offset 1h))) over a declared counter keeps its non-negative Count-Min weights, and a per-series DDSketch quantile over `x[100s] offset 1h` of a non-negative gauge keeps its relative bound through avg. An `@` anchor still ends it. Tests: the offset forms of both acceptance tests.
…773 review) `topk(k, irate(m[w]))` lowered to an exact sort and limit while `topk(k, rate(m[w]))` got the heavy-hitter TopK intent. irate now takes the same direct path as rate and increase (and the matching direct_counter_ranking check). Tests: the lowering of topk(3, rate|increase|irate(...)) and a plan-selection check that the direct irate ranking plans.
…o sample sign (#772 review) PromQL answers quantile_over_time(phi, ...) with phi < 0 or phi > 1 as -Inf or +Inf, which no sketch estimates and the executor's DDSketch and KLL refuse at run time. - Pass 1 prunes every sketch candidate of such a quantile with the new PruneReason::QuantileOutsideUnitInterval, so only the exact plan is offered and selected (this also fixes bare `quantile_over_time(2, x)` and `max by (...) (quantile_over_time(-1, x))`, which selected a sketch plan before). - estimates_samples_of follows a quantile estimate only for phi in [0, 1], so such an estimate never carries its samples' sign. Tests: the four reviewer probes plan exactly, also over a gauge declared non-negative; the sign rule for phi = -1 and 2.
This was referenced Oct 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
avg by (region) (quantile_over_time(0.99, data[100s]))over a gauge that is never negative (a latency, say) can only be planned exactly. A per-series DDSketch quantile is within a relative error, but that bound carries throughavgandsumonly over non-negative values. That sign,values_non_negative[data], was never bound for a quantile estimate, and a data workload could not declare a gauge non-negative. So Stage 3 rejected every sketch candidate.What
SourceFactscan declare every sample of a metric non-negative (values_non_negative), for a gauge as well as a counter.avgandsumand the candidate is valid. Without it, the candidate is still rejected. Throughmaxit was already valid, sincemaxneeds no sign.offset. Anoffsetkeeps a metric's sample sign.topk(10, sum by (job) (rate(data_total[5m] offset 1h)))over a declared counter keeps its non-negative Count-Min weights. The gauge sign above also holds underoffset.irate.topk(k, irate(m[w]))is a heavy-hitter ranking, astopk(k, rate(m[w]))is. It used to be an exact sort and limit.avg by (region) (quantile_over_time(-1, x[100s]))over a declared non-negative gauge selected a DDSketch, and so did barequantile_over_time(2, x[100s])andmax by (...) (quantile_over_time(-1, ...))even without a declaration. All of them now select the exact plan.How
values_non_negative[m]to proven (1) whenmis declared a counter or declared non-negative (declared_non_negative). Before, only a declared counter counted.estimates_samples_of, which feeds only the sign statistic of a fold, also follows a quantile estimate whose update reads the sample value of raw samples. A quantile of some values is one of them, or lies between two of them, so it has their sign. The existingrelative_foldrule then gates the bound on that metric's sign instead of an anonymous unknown.offset.samples_of_metricpasses through aTimeShiftthat carries only an offset. An@anchor still ends it, as the review asked.irate. The frontend's direct per-series ranking check and the counter-ranking check now includeIRate.PruneReason::QuantileOutsideUnitInterval, andestimates_samples_offollows a quantile estimate only for φ in [0, 1].Key code interfaces added / modified
crates/types/src/workload/mod.rsJSON:
"sources": {"data": {"metric_type": "gauge", "values_non_negative": true}}.Usage (the test
a_non_negative_gauge_carries_a_ddsketch_relative_bound_through_avg):Before this PR
avg|sum by (region) (quantile_over_time(0.99, x[100s]))over a gauge: the DDSketch candidate was rejected (AccuracyTargetMissed, relative bound unknown), and the gauge could not be declared non-negative.topk(10, sum by (job) (rate(data_total[5m] offset 1h)))over a declared counter: the Count-Min-with-heap candidate was rejected withCountMinWeightsNotProvenNonNegative.topk(3, irate(m[5m]))lowered toLimit(Sort(...)).After this PR
values_non_negative: true, the DDSketch candidate is valid with a relative-error guarantee throughavgandsum, also underoffset. Without the declaration it is rejected as before.ratetop-k over a counter passes the weight check, and is still rejected over a gauge.topk(3, irate(m[5m]))lowers to aTopKaggregate and plans.Verification
Regression tests. Each new test, or new case, failed before its fix and passes now.
plan-selectiona_non_negative_gauge_carries_a_ddsketch_relative_bound_through_avgcoversavg,sumandmax, plusavgwithoffset 1h, each with and without the declaration.plan-selectioncount_min_over_a_counter_derivative_needs_a_declared_counteradds therate(... offset 1h)case.frontend-promqltopk_over_a_per_series_counter_derivative_is_a_heavy_hitter_rankingcoversrate,increaseandirate.plan-selectiona_direct_irate_ranking_planschecks only thattopk(10, rate|irate(...))composes and selects a plan. The heavy-hitterTopKintent is checked by the frontend test above.plan-selectiona_quantile_outside_the_unit_interval_is_answered_exactlycovers the four probes for φ = −1 and 2 (avg by, a bare quantile, andmax by). It checks that no sketch candidate is offered and that the exact plan is selected over a gauge declared non-negative. On the previous head it failed: a sketch candidate was offered.logical-asap-optimizationa_quantile_estimate_shares_its_samples_sign_only_inside_the_unit_intervalchecks the sign rule for φ = −1 and 2. It fails with the φ check removed.types: theSourceFactsJSON round trip includesvalues_non_negative.Commands run. Each was run with
CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 CARGO_INCREMENTAL=0, which only reduces disk use. All passed.cargo test --workspace --lockedat the head: exit 0, 150 test binaries, 0 failures.cargo fmt --all -- --checkcargo clippy -p asap-types -p asap-plan-selection -p asap-logical-asap-optimization -p asap-frontend-promql --all-targets --all-features --locked -- -D warnings(re-run for the last commit onasap-plan-selectionandasap-logical-asap-optimization, the crates it touches)cargo check -p asap-planner --tests --lockedNot applicable: performance measurements.
Limitations.
values_non_negativefeeds only the sign statistic. Count-Min weight confirmation still requires a declared counter, so a Count-Min heap over a non-negative gauge's samples stays rejected. That would need a proof variant that does not say "counter", and is left as a follow-up.Closes #772