Skip to main content

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}