Skip to content

[Refactor] Time-series TimeRange as a native operator; bind raw data only at Scan; remove the raw-sample selectors #969

Closed

Description

@zzylol

Example

rate(http_requests_total{job="api"}[5m]) and http_requests_total{job="api"} (an instant selector with the default 5m lookback).

The DAG has Scan(Source::TimeSeries { metric: "http_requests_total" }, predicate job = "api") under TimeRange(Range, 5m) or TimeRange(Instant, 5m).

Today the executor has no operator for a time-series TimeRange. A runtime computes the window rows outside the DAG and binds them at the TimeRange node (crates/executor/src/samples.rs):

let selector = RawDataSampleSelector::new(&plan.dag.dag, window)?;
// the runtime routes samples whose __name__ == selector.metric() here
let rows = selector.window_batch(window_end_ms, &samples)?; // samples: &[RawDataSample]

window_batch does all of the following itself:

  • keeps samples in (end - 5m, end];
  • evaluates the Scan's label matchers;
  • for an instant selector, keeps each series' latest sample;
  • drops Prometheus stale markers, so a series whose latest sample is a stale marker has no row.

A SQL table window goes through TableRowSelector::window_batch, which repeats the table TimeRange filter that the executor already lowers natively (table_time_operator, physical_planner/mod.rs:725).

After this issue, both are bound at the Scan through TableProvider (#966). A time series is a table (ts, value, *labels), and the DAG's own TimeRange gives these answers:

Query at T Rows the TimeRange outputs
[5m] range every sample with ts in (T - 5m, T], stale markers dropped
instant, 5m lookback per series, the latest sample in (T - 5m, T], or no row when that sample is a stale marker

Concrete maintenance problem

  • Window and label-matcher semantics have three implementations: the table TimeRange lowering, TableRowSelector, and RawDataSampleSelector. A fix to the (end - width, end] boundary has to be made in all three.
  • PromQL semantics (instant lookback, stale markers) live outside the DAG. PlannerExecutorCompileCheck cuts at the TimeRange, so it never compiles them and cannot reject an unsupported time-series window.
  • samples.rs (615 lines) puts a Prometheus sample model in the executor's public API: RawDataSample { labels, timestamp_ms, value }, routing by __name__, stale markers, and series_labels_of_output_row.
  • RawDataSampleWindow binds at time_range_node_id, a node above the Scan, so raw data enters the DAG in two places.

Behavior that must remain unchanged

Every PromQL and SQL query returns the same answers, including:

  • range-window boundaries (end - width, end];
  • an ordinary NaN sample is kept, and a stale marker is never a row;
  • instant-selector lookback, where an instant whose latest sample is stale has no row;
  • TimeShift offsets, where a @ anchor is still refused for tables.

Before and after structure

  • TimeRange over a time-series Scan lowers to a native operator.

    • A range window filters ts and drops stale markers.
    • An instant window filters the lookback, keeps each series' latest row, and drops it if it is a stale marker.
    • Series identity is the label columns, as series_identity computes today.
  • The table TimeRange lowering and the time-series one share the window computation, which takes the shifts below the TimeRange and T.

  • One scan-window function, in asap-types. "Which window of a Scan its readers read, back from the evaluation time" is computed once, on PhysicalASAPDAG. For example: fn scan_window(&self, scan: NodeId) -> ScanWindow, where ScanWindow is Within { end_offset_ms, width_ms } (the rows (T - end_offset - width, T - end_offset], the union over every reader) or Unbounded (a root or non-window reader, or an @ anchor). Its callers:

    This fixes example 1 of Retained raw data as concrete state groups per lookback: offsets, @ and a retention limit #955. Stage 3 keeps and prices 65 minutes of m for rate(m[5m] offset 1h) at query time, not 5, because today windows_read drops the TimeShift offset ("moves a window without widening it"). The executor already reads (T - 65m, T - 60m]. @ anchors, the RawRetention capability and the raw window(S, H) memo groups stay in Retained raw data as concrete state groups per lookback: offsets, @ and a retention limit #955.

  • Raw data is bound only at Scan, through BindingDataSources.tables.

  • Removed: RawDataSampleSelector, TableRowSelector, RawDataSample, series_identity's public export, and series_labels_of_output_row. Answer labels are read from the output schema's label columns.

  • RawDataSampleWindow no longer names a binding node. Whether InstallablePlan still needs it is decided in the next PR of [Tracking] Physical DAG → deployment boundary: InstallablePlan, input slots, TableProvider, BindingDataSources #968.

Why the proposed structure is minimally complex

One window implementation replaces three. PromQL semantics move into the operator that the compile check already compiles. No new interface is added: a runtime that buffers Prometheus samples implements a TimeSeries TableProvider over its buffer.

Existing unit and end-to-end protection

  • Selector tests to rewrite against a provider bound at the Scan, asserting the same rows:
    • crates/planner/tests/{runtime_install,per_sample_updater,columnar_batches,sql_per_group_topk,sql_table_scans}.rs;
    • crates/integration-tests/tests/per_series_exact_execution.rs;
    • crates/executor/tests/thread_safety.rs.
  • Add tests:
    • an instant selector whose latest sample is a stale marker (no row);
    • a range window holding an ordinary NaN (kept);
    • a time-series window the executor cannot run, which the compile check now rejects;
    • failing first: Stage 3 prices rate(m[5m] offset 1h) at query time with a 65-minute raw lookback (Retained raw data as concrete state groups per lookback: offsets, @ and a retention limit #955 example 1);
    • scan_window on a Scan read by [5m] and [1h] offset 10m is (T - 70m, T]; with an @ anchor it is Unbounded.
  • Stage 3 regressions: the raw_data_retained tests in plan-selection, stored_data_without_raw_data_retained_is_bad_input, and materialization_e2e. Plans without offsets keep the same costs.

Compatibility or migration impact

This is a public API change in asap_executor: the samples items are removed. Stage 3 raw-data costs change only for plans that read a shifted window at query time (the #955 fix). ASAPQuery-backend main does not depend on asap_executor yet. The backend's sample ingestion becomes a TimeSeries TableProvider.

Parent: #968 (PR 5 of 6; after #966). Related: #955 (this PR provides its lookback function and fixes its example 1), #951 (a shared scan reads the union scan_window returns).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bindingLogical-to-physical binding and implementation selectioncost-modelCost formulas, statistics, ranking, and selectionpromqlPromQL front-end lowering (L1→L2)refactorsqlSQL front-end lowering (DataFusion → L2)windowWindow functions, frames, panes, or temporal semantics

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions