fix(query-engine): counter-reset correction + Prometheus extrapolation for rate()/increase() (#476)#492
Conversation
Fixes the format-and-lint CI check on the #476 branch — the prior commit was not rustfmt-clean. Formatting only; no logic change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
This updates only the range query execution path, but not the instant query execution path. Note that Prometheus "instant" queries also operate on a range of data. Range queries are simply a way to express multiple instant queries executing at different timesteps.
When you send a query request to Prometheus, it can be an instant query, evaluated at one point in time, or a range query at equally-spaced steps between a start and an end time. PromQL works exactly the same in each case; the range query is just like an instant query run multiple times at different timestamps.
https://prometheus.io/docs/prometheus/latest/querying/basics/
There was a problem hiding this comment.
There are actually three execution paths for rate()/increase(), not two:
- Single-metric instant (handle_query_promql → execute_context → execute_query_pipeline) — gets RANGE_START_MS_KWARG/RANGE_END_MS_KWARG via build_query_kwargs_promql's new Statistic::Rate | Statistic::Increase arm (promql.rs:144-101).
- Range query, single or binary (handle_range_query_promql / handle_binary_expr_range_promql → execute_range_query_pipeline) — gets the new step_kwargs with per-step RANGE_START_MS_KWARG/RANGE_END_MS_KWARG (mod.rs:1511-1550). ✅ Fixed.
- Binary-arithmetic instant (handle_binary_expr_promql → build_arm_logical_plan → DataFusion physical plan → SummaryInferExec) — this is a completely separate code path, and SummaryInferExec::infer_op_to_kwargs (engines/physical/summary_infer_exec.rs:167-187) only
builds kwargs for Quantile/Median/TopK; everything else, including InferOperation::ExtractIncrease/ExtractRate, falls through to _ => None. That None propagates to IncreaseAccumulator::query(statistic, None), which hits the fallback branch — reset-corrected but not
extrapolated.
There was a problem hiding this comment.
Using my phone, will double check. But I don’t think counter-reset problem happens on a single-metric instant query as all points are in one timestamp? Oh, with some lookback windows.
There was a problem hiding this comment.
@zaoxing This is confusing about Prometheus but instant query != instant vector.
Instant vector - all data points at 1 timestamp
Range vector - data points across multiple timestamp
Instant query - a query evaluated at 1 timestamp, but may span data points across multiple timestamps. For e.g. avg_over_time(data[5m]) @ t=10:00:00 is an instant query evaluated at 10am that consider data from 9:55am to 10am
Range query - an instant query that is evaluated multiple times, at multiple timestamps.
| last_seen_measurement, | ||
| last_seen_timestamp, | ||
| counter_reset_correction: 0.0, | ||
| sample_count: 1, |
There was a problem hiding this comment.
Why 1 and not 0?
There was a problem hiding this comment.
Claude says:
1 is correct for new()'s actual contract — it's only ever called (in production code) with starting == last_seen, i.e. exactly one real sample has been observed, so 0 would be wrong. I checked every production call site (accumulator_factory.rs) and they all respect
this.
But the question exposes that the contract is unenforced: new()'s signature happily accepts two distinct starting/last_seen values and will still silently record sample_count: 1. Several existing tests already construct accumulators this way (e.g.
plan_execution_tests.rs:311-312, store_correctness_tests.rs:845, accumulator_serde_tests.rs:115/119) — treating new() as a generic "two-endpoint" constructor. Luckily I checked: the ones that would actually break (test_old_vs_new_increase) are already #[ignore]d, and
the others never call .query()/.extrapolated(), so nothing currently fails — but it's a latent trap. If any future caller uses new() the same way with two distinct values and then calls rate()/increase() with range kwargs, extrapolated() will wrongly return
sample_count < 2's "insufficient samples" error path is actually fine there (1 < 2 → None, correctly conservative) — the worse case is average_duration_between_samples = sampled_interval / 0 if someone passes sample_count explicitly wrong via new_full, or just
generally undercounts real sample density for extrapolation math on synthetic fixtures.
…se() across all execution paths
fix(query-engine): Prometheus counter-reset correction + extrapolation for rate()/increase() across all execution pathsCloses #476. Completes the work started in #492. ProblemASAP's accelerated
There are three execution paths for
Path #3 is a separate DataFusion code path: the range boundaries were dropped at What changedAccumulator (counter-reset correction + extrapolation)
Path #1 & #2 (pipeline)
Path #3 (binary-arithmetic instant / DataFusion) — the remaining gap
Single-population Increase on the DataFusion path (arroyo serde)
Backward compatibility
Correctness verificationBeyond unit tests, this PR adds cross-path and synthetic differential
Full suite: 550 lib tests + integration binaries pass, 0 failures; Out of scope / follow-ups
|
…t-correction Resolve conflicts from main's promql.rs dedup + QueryPatternType removal (#511/#515/#517/#518) against the rate()/increase() counter-reset correction + extrapolation work: - promql.rs: keep main's refactored build_query_kwargs_promql signature (&Statistic, match_result); thread `timestamps` through it and retain the Rate|Increase range-boundary arm; drop now-unused imports (QueryPatternType, get_is_collapsable, AggregationOperator, PromQLFunction, get_spatial_aggregation_output_labels, get_statistics_to_compute). - simple_engine/mod.rs: keep RANGE_*_MS_KWARG imports (per-step range boundaries), drop removed QueryPatternType. - tests: keep the single-pop Increase extrapolation test (arroyo serde now makes it deserializable) over main's obsolete not_implemented assertion; keep all new rate/increase test modules alongside main's range_query_arithmetic_tests. Verified: workspace builds; 560 lib tests pass (0 failures); fmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Fixes #476. ASAPQuery's accelerated
rate()/increase()returned wrong values because the increase precompute did no counter-reset correction. The accumulator stored only the first and last sample per series and computedlast − start, so any counter reset inside the window was silently lost — e.g. a stream10 → 100 → 5 → 30returned20instead of the correct120.This PR brings the precompute path to full Prometheus
rate/increasesemantics across both ingestion engines (nativeprecomputeand Arroyo):counterCorrection), since only the window endpoints are retained and a reset between them is unrecoverable at query time.ratedivides by the range duration (not the sampled interval) — matchingextrapolatedRatein Prometheuspromql/functions.go.The
extrapolated()implementation was verified line-for-line against the current Prometheus source (float counter path), including the exact clamp order (threshold clamp → counter zero-point clamp) and thefactor = 1.0fallback when the sampled interval is zero. The histogram,anchored/smoothed, and created-timestamp branches are intentionally not reproduced (not applicable to this codebase).Changes
IncreaseAccumulator(increase_accumulator.rs): addedcounter_reset_correctionandsample_countfields; reset detection inupdate(); Prometheus extrapolation inquery()with a reset-corrected fallback for non-PromQL callers (SQL/Elastic); boundary-awaremerge_pair()used by both merge paths; serde (JSON + binary) carries the new fields with tolerant legacy decode.MultipleIncreaseAccumulator(multiple_increase_accumulator.rs): extended theMeasurementDataMessagePack wire struct with#[serde(default)]fields (back-compatible), fixed the manual byte-length math, routed per-key merges throughmerge_pair, and forwardedquery_kwargs.templates/udfs/multipleincrease_.rs,templates/hashed_key_udfs/multipleincrease_.rs): same reset/count logic, sorting per-key samples by timestamp first.query_kwargsfor the instant path (promql.rs) and per-step in the range path (mod.rs); shared key constants inenums.rs.Testing
cargo test -p query_engine_rust --lib— 532 passed, 0 failed; clippy clean; full workspace builds.validate_udfs.pyneeds a running Arroyo server).Follow-up
No automated numeric parity test against a real Prometheus exists yet. Recommend a manual check on a resetting counter via the asap-quickstart stack (Grafana shows ASAPQuery vs Prometheus side-by-side) before relying on it in production.
It is worth additional checking if this implementation is capatiable with VictoriaMetrics extrapolation.