query/promql/planner/at_modifier.rs
1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Anchored selector planning and replay for the PromQL `@` modifier.
16
17use std::sync::Arc;
18use std::time::{SystemTime, UNIX_EPOCH};
19
20use datafusion::logical_expr::{Extension, LogicalPlan, LogicalPlanBuilder};
21use datafusion::prelude::{Column, Expr as DfExpr};
22use promql::extension_plan::{InstantManipulate, Millisecond, SeriesDivide};
23use promql_parser::parser::{
24 AtModifier, Call, Expr as PromExpr, MatrixSelector, Offset, ParenExpr, SubqueryExpr,
25 VectorSelector,
26};
27use snafu::{OptionExt, ResultExt, ensure};
28
29use crate::promql::error::{
30 AtModifierTimestampOutOfRangeSnafu, DataFusionPlanningSnafu, Result, TimeIndexNotFoundSnafu,
31};
32use crate::promql::planner::PromPlanner;
33use crate::query_engine::QueryEngineState;
34
35impl PromPlanner {
36 /// Resolve the `@` modifier of a vector or matrix selector into the timestamp its sample
37 /// window is anchored at, in milliseconds since the Unix epoch.
38 ///
39 /// Prometheus semantics:
40 /// - `@ <unix_ts>` anchors at the given timestamp,
41 /// - `@ start()` / `@ end()` anchor at the evaluation range of the whole statement,
42 /// - `offset` shifts the anchor backwards: the window ends at `anchor - offset`.
43 ///
44 /// Returns `None` when the selector has no `@` modifier.
45 pub(crate) fn at_ref_time(
46 &self,
47 at: &Option<AtModifier>,
48 offset: &Option<Offset>,
49 ) -> Result<Option<Millisecond>> {
50 let anchor = match at {
51 None => return Ok(None),
52 Some(AtModifier::Start) => self.ctx.stmt_start,
53 Some(AtModifier::End) => self.ctx.stmt_end,
54 Some(AtModifier::At(time)) => Self::system_time_to_millis(time)?,
55 };
56 Ok(Some(Self::anchor_sub(anchor, Self::offset_millis(offset))?))
57 }
58
59 /// Subtracts `rhs` from `lhs` on the millisecond timeline of an `@` anchor.
60 ///
61 /// A negative result is valid: `@` and `offset` accept timestamps before the Unix epoch. A
62 /// result outside the representable millisecond range is rejected like an unrepresentable
63 /// anchor ([`Self::system_time_to_millis`]) instead of clamping it, so the same class of
64 /// input always gets the same answer.
65 pub(crate) fn anchor_sub(lhs: Millisecond, rhs: Millisecond) -> Result<Millisecond> {
66 lhs.checked_sub(rhs)
67 .with_context(|| AtModifierTimestampOutOfRangeSnafu {
68 timestamp: format!("{}ms - {}ms", lhs, rhs),
69 })
70 }
71
72 /// The offset a selector with an `@` modifier is evaluated with.
73 ///
74 /// Prometheus anchors such a selector by rewriting its offset to `eval_time - anchor`
75 /// (`setOffsetForAtModifier`), so that the selector always selects its samples around `anchor`
76 /// regardless of the step being evaluated. `eval_time` is the start of the evaluation the
77 /// selector belongs to, which is `ctx.start`.
78 ///
79 /// Returns `None` when the selector has no `@` modifier.
80 pub(crate) fn at_modifier_offset(
81 &self,
82 at: &Option<AtModifier>,
83 offset: &Option<Offset>,
84 ) -> Result<Option<Millisecond>> {
85 let Some(anchor) = self.at_ref_time(at, offset)? else {
86 return Ok(None);
87 };
88 Ok(Some(Self::anchor_sub(self.ctx.start, anchor)?))
89 }
90
91 /// Whether `expr` is a call that has to be evaluated once for the whole grid, because it folds
92 /// a range selector anchored by `@` — the range argument of the call's parser signature, which
93 /// is a [`MatrixSelector`] here; only a call with such an argument can take this path, so no
94 /// function-name registry is involved.
95 ///
96 /// This is the shape that needs Prometheus' `StepInvariantExpr` the most: a range function such
97 /// as `rate` derives its result from the evaluation instant it is called at, so folding the
98 /// window once per step would let the outer evaluation grid change the result of a window that
99 /// `@` fixed. Evaluated once, at the start of the grid, the rewritten offset of
100 /// [`Self::at_modifier_offset`] places the anchor at that instant, and [`Self::replay_over_grid`]
101 /// reports the result at every step.
102 ///
103 /// Unlike Prometheus, which wraps the whole step-invariant subtree (`preprocessExprHelper`),
104 /// only the call itself is promoted here. The operators above it are not: they are still planned
105 /// at every step over the replayed result, which is safe for the row-wise ones and keeps the
106 /// promotion root narrow. The promotion root has to stay a call over one range selector, because
107 /// the replay needs one series per batch ([`Self::series_divide_plan`]) and only such a call
108 /// guarantees that the rows it emits still describe the series it was divided by. The operators
109 /// left out — an aggregation, a join, or a label rewriting call such as `label_join` — mix or
110 /// re-label the rows of different series, so they are unsafe as promotion roots even though
111 /// evaluating them after the replay is fine. A call whose input is an anchored *instant*
112 /// selector (`abs(some_metric @ 300)`) needs no promotion either: the selector anchors and
113 /// replays its sample per series on its own, and the call above it is row-wise.
114 ///
115 /// `predict_linear` is the exception among the range functions: it predicts from the evaluation
116 /// instant of each step ([`Self::create_range_eval_ts_expr`]), so it has to stay outside the
117 /// promoted subtree and follow the grid. The remaining arguments of the call have to be
118 /// literals, since the replay of the promoted result has no second vector input to divide.
119 /// Parentheses around the range argument are transparent (`rate((m[5m] @ 300))`), so they are
120 /// looked through and the call is promoted as if they were absent. Nothing else of the subtree
121 /// is unwrapped, so an outer parenthesis promotes no operator above the call.
122 fn promotes_anchored_range_call(expr: &PromExpr) -> bool {
123 let PromExpr::Call(Call { func, args }) = expr else {
124 return false;
125 };
126 // See the doc comment: the regression of `predict_linear` follows the evaluation step.
127 if func.name == "predict_linear" {
128 return false;
129 }
130 let mut anchored_range = false;
131 for arg in &args.args {
132 // Parentheses around the range argument are transparent, so the call is promoted the
133 // same way for `rate((m[5m] @ 300))` as for `rate(m[5m] @ 300)`. Only the parentheses
134 // directly around this one argument are looked through here: the promotion stays
135 // confined to a call over one anchored range selector instead of descending into an
136 // arbitrary parenthesized subtree.
137 let mut arg = arg.as_ref();
138 while let PromExpr::Paren(ParenExpr { expr }) = arg {
139 arg = expr;
140 }
141 match arg {
142 // The window is pinned by `@`, so every step folds the same samples. That holds
143 // for a subquery with `@` too, which Prometheus treats as step-invariant whatever
144 // its inner expression is.
145 PromExpr::MatrixSelector(MatrixSelector {
146 vs: VectorSelector { at: Some(_), .. },
147 ..
148 })
149 | PromExpr::Subquery(SubqueryExpr { at: Some(_), .. }) => {
150 if anchored_range {
151 return false;
152 }
153 anchored_range = true;
154 }
155 // A literal argument is the same value at every step.
156 arg if Self::try_build_literal_expr(arg).is_some() => {}
157 _ => return false,
158 }
159 }
160 anchored_range
161 }
162
163 /// Plans the anchored range call `prom_expr` ([`Self::promotes_anchored_range_call`]) as a
164 /// step-invariant subtree: the call is evaluated on a single evaluation instant (`grid_start`,
165 /// the start of the outer evaluation) and its result is then reported at every step of
166 /// `[grid_start, ctx.end]` by [`Self::replay_over_grid`].
167 ///
168 /// This is the planner's counterpart of Prometheus' `StepInvariantExpr` for the one shape it
169 /// promotes. Evaluating the call once matters for the functions that derive their result from
170 /// the step being evaluated: `rate(m[5m] @ 300)` folds its window around the anchor once, and
171 /// the extrapolation boundaries of `rate` must be derived from that same window at every step
172 /// instead of following the outer evaluation timestamp.
173 ///
174 /// Only the call itself is promoted; the operators above it are planned as usual over the
175 /// replayed result. The result of the promoted call is split into one series per batch before it
176 /// is replayed ([`Self::series_divide_plan`]), because the row-wise projection of the call does
177 /// not preserve the batch layout of the selector.
178 ///
179 /// Returns `None` when `prom_expr` is not such a call, so that the caller plans it as usual.
180 /// The selector inside the promoted call keeps its own `@` anchoring (see
181 /// [`Self::at_modifier_offset`]), and planning it with `ctx.end == ctx.start` folds its window
182 /// once for that single instant instead of expanding it over the grid, which the replay of the
183 /// call result above already does.
184 pub(crate) async fn promote_anchored_range_call(
185 &mut self,
186 prom_expr: &PromExpr,
187 timestamp_fn: bool,
188 query_engine_state: &QueryEngineState,
189 ) -> Result<Option<LogicalPlan>> {
190 let grid_start = self.ctx.start;
191 let grid_end = self.ctx.end;
192 // An instant query evaluates a single step, so there is nothing to promote.
193 if grid_start == grid_end || !Self::promotes_anchored_range_call(prom_expr) {
194 return Ok(None);
195 }
196
197 // Plan the subtree on a single evaluation instant: every selector below still anchors its
198 // window through `@`, and the functions above them derive their result from that one
199 // instant. The planner is single-use, so `ctx.end` needs no restore-on-error.
200 self.ctx.end = grid_start;
201 let anchored = self
202 .prom_expr_to_plan_inner(prom_expr, timestamp_fn, query_engine_state)
203 .await?;
204 self.ctx.end = grid_end;
205
206 let time_index_column =
207 self.ctx
208 .time_index_column
209 .clone()
210 .with_context(|| TimeIndexNotFoundSnafu {
211 table: self.ctx.table_name.clone().unwrap_or_default(),
212 })?;
213 // The replay reads one series per batch; see [`Self::series_divide_plan`] for why the
214 // layout of the selector below the promoted call does not survive it.
215 let anchored = self.series_divide_plan(anchored, &time_index_column)?;
216 Ok(Some(self.replay_over_grid(
217 anchored,
218 grid_start,
219 grid_end,
220 time_index_column,
221 )))
222 }
223
224 /// Convert the timestamp of an `@` modifier into milliseconds since the Unix epoch.
225 fn system_time_to_millis(time: &SystemTime) -> Result<Millisecond> {
226 let (millis, negative) = match time.duration_since(UNIX_EPOCH) {
227 Ok(duration) => (duration.as_millis(), false),
228 // The `@` modifier accepts timestamps before the Unix epoch, e.g. `@ -1`.
229 Err(err) => (err.duration().as_millis(), true),
230 };
231 ensure!(
232 millis <= i64::MAX as u128,
233 AtModifierTimestampOutOfRangeSnafu {
234 timestamp: time
235 .duration_since(UNIX_EPOCH)
236 .map(|duration| format!("+{}ms", duration.as_millis()))
237 .unwrap_or_else(|err| format!("-{}ms", err.duration().as_millis())),
238 }
239 );
240 let millis = millis as Millisecond;
241 Ok(if negative { -millis } else { millis })
242 }
243
244 /// Report the samples of `anchored` at every step of the evaluation grid
245 /// `[grid_start, grid_end]`.
246 ///
247 /// A selector with an `@` modifier is anchored: the sample window is selected once, around the
248 /// anchor timestamp, and every evaluation step reports that same window. Prometheus does this
249 /// by rewriting the selector's offset to `eval_time - anchor` and only fetching the samples on
250 /// the first step (`setOffsetForAtModifier` plus the `refetch` shortcut in `rangeEval`).
251 ///
252 /// The expansion reuses [`InstantManipulate`] with a lookback that spans the whole grid: every
253 /// step then picks the same sample (or the same already computed value, when `anchored` ends
254 /// with a function call such as `rate`) and stamps it with the step's timestamp.
255 ///
256 /// Every input batch of `anchored` must hold exactly one series, because [`InstantManipulate`]
257 /// takes a batch as one timeline. A leaf-level replay (`m @ 300`) consumes the [`SeriesDivide`]
258 /// of its selector directly. A promoted call is guaranteed that layout by
259 /// [`Self::series_divide_plan`], which is why [`Self::promote_anchored_range_call`] splits
260 /// its result before calling this method.
261 pub(crate) fn replay_over_grid(
262 &self,
263 anchored: LogicalPlan,
264 grid_start: Millisecond,
265 grid_end: Millisecond,
266 time_index_column: String,
267 ) -> LogicalPlan {
268 if grid_start == grid_end {
269 // A single evaluation step: `anchored` is already stamped with that timestamp.
270 return anchored;
271 }
272
273 let series_key_columns = self.series_key_columns_for_schema(anchored.schema());
274 let replayed = InstantManipulate::new(
275 grid_start,
276 grid_end,
277 // The lookback must keep the single anchored sample eligible for every step.
278 grid_end - grid_start + 1,
279 self.ctx.interval,
280 0,
281 time_index_column,
282 series_key_columns,
283 self.ctx.field_columns.first().cloned(),
284 anchored,
285 );
286 LogicalPlan::Extension(Extension {
287 node: Arc::new(replayed),
288 })
289 }
290
291 /// Sorts `input` by its series key and time index and splits it into one batch per series.
292 ///
293 /// [`InstantManipulate`] reads every input batch as one series (it takes the timeline of the
294 /// batch and reports the row selected at every step), so a batch holding several series would
295 /// lose all but one of them. A selector establishes that layout with its own [`SeriesDivide`],
296 /// but the per-series distribution requirement does not reach a promoted subtree above it:
297 /// the row-wise projection of a call sits in between, so the batch boundaries of the selector
298 /// are not preserved — in a distributed plan the promoted result can be delivered as one batch
299 /// holding every series. Sorting and dividing here restores the layout, exactly like
300 /// [`Self::prom_matrix_selector_to_plan`] does for the input of a range function.
301 ///
302 /// Series keys that are not present in `input` are dropped, since `ctx.tag_columns` may have
303 /// drifted from the actual output schema. A plan without any series key column is returned
304 /// unchanged: there is nothing to divide by.
305 fn series_divide_plan(
306 &self,
307 input: LogicalPlan,
308 time_index_column: &str,
309 ) -> Result<LogicalPlan> {
310 let series_key_columns = self.series_key_columns_for_schema(input.schema());
311 if series_key_columns.is_empty() {
312 return Ok(input);
313 }
314
315 let mut sort_exprs = series_key_columns
316 .iter()
317 .map(|name| DfExpr::Column(Column::from_name(name)).sort(true, true))
318 .collect::<Vec<_>>();
319 sort_exprs.push(DfExpr::Column(Column::from_name(time_index_column)).sort(true, true));
320 let sort_plan = LogicalPlanBuilder::from(input)
321 .sort(sort_exprs)
322 .context(DataFusionPlanningSnafu)?
323 .build()
324 .context(DataFusionPlanningSnafu)?;
325 Ok(LogicalPlan::Extension(Extension {
326 node: Arc::new(SeriesDivide::new(
327 series_key_columns,
328 time_index_column.to_string(),
329 sort_plan,
330 )),
331 }))
332 }
333}