Skip to content

feat(query): answer planned instant queries by running the plan's roots - #821

Merged
zzylol merged 5 commits into
mainfrom
feat/query-engine
Oct 21, 2026
Merged

zzylol merged 5 commits into
mainfrom
feat/query-engine

Conversation

@zzylol

@zzylol zzylol commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Base: main (#838 merged). Next: #824 (stored outputs built in a QueryRun).

Why

Every query was forwarded to Prometheus, so the selected plan never answered anything.

What

A planned instant query is answered by running its root of the DAG at evaluation time T:

  • Stored inputs: each reads the pane of its definition ending at T − offset.
  • Query-time raw inputs: the plan's query_time_raw_windows. For each one, the data plane fetches (T − end_offset − width, T − end_offset] from Prometheus and selects rows with the window's RawSampleSelector (the Scan's label matchers, and for an instant selector each series' latest sample).
  • Fetch selector: the window's Scan label matchers that PromQL can express (a label compared with =, !=, =~ or !~ to a text literal) go into the range selector, so Prometheus returns only the series the plan keeps. The metric and values are escaped as PromQL strings. The RawSampleSelector still applies every matcher, so a matcher left out only widens the fetch. Raw windows are fetched concurrently.
  • Evaluation time: a time that is not finite or beyond MAX_TIME_MS (i64::MAX / 4 ms) gets 400 bad_data, as Prometheus answers. Window and pane binding use checked arithmetic, so no evaluation time overflows.
  • Headers on raw reads: each raw-window fetch carries the caller's end-to-end headers, the same set query forwarding keeps, such as Authorization and X-Scope-OrgID. Prometheus then authorizes and scopes the read as it would the caller's own query. Content-Type is dropped, because the read is a GET without the caller's body.
  • Answer labels: from asap_executor::series_labels_of_output_row.
  • Responses: an answered query returns a Prometheus vector with x-asap-execution: plan. Any other query, including range queries, is forwarded with x-asap-execution: forwarded. A planned query that can't be answered gives its reason in x-asap-forward-reason: a missing pane, T off the pane grid, or an unsupported shape.
  • MatAtQuery nodes run like NotMaterialized ones: each instant query is its own evaluation, so there is nothing to share within it.

How

Each root (InstallablePlan.queries[i].root_node_id) is compiled once at install time with asap_executor::physical_planner::compile, with its stored outputs and the plan's raw windows as bounded input contracts. A TimeRange the plan lists no window for makes the root unsupported, and its query is forwarded with that reason. At query time the engine thread checks the panes, the HTTP side fetches the bound raw-sample windows, and the engine instantiates the compiled root over both and runs it under query scope.

Key code interfaces added / modified

// data_plane/src/query.rs
pub struct BoundRawSampleWindow {
    /// The `TimeRange` node the samples feed.
    pub node: u64,
    /// The `Scan`'s metric.
    pub metric: String,
    /// Exclusive start.
    pub start_ms: i64,
    /// Inclusive end.
    pub end_ms: i64,
}

pub struct ResultSample {
    /// Series labels, from the series identity and the other text columns.
    pub labels: BTreeMap<String, String>,
    /// The one `Float64` column.
    pub value: f64,
}

/// Why a query is not answered; it is then forwarded.
pub enum Unanswered {
    /// The query text is not one of the plan's.
    NotPlanned(String),
    /// The root does not compile or its result rows are not a vector.
    Unsupported(String),
    /// A stored pane is missing or `T` is off the pane grid.
    MissingPane(String),
    /// Fetching raw samples or execution failed.
    Execution(String),
}

pub struct QueryEngine { /* private: each query with its compiled root */ }
impl QueryEngine {
    /// Compiles every root of `plan` against the stored outputs of `sds`.
    pub fn new(plan: &InstallablePlan, sds: &Sds) -> Self;
    /// The root answering `query` at `time_ms` and the raw samples it needs.
    pub fn bound_raw_sample_windows(&self, query: &str, time_ms: i64, store: &SummaryStore<Batch>)
        -> Result<(usize, Vec<BoundRawSampleWindow>), Unanswered>;
    /// Runs `root` at `time_ms` over stored panes and `raw` (samples by `BoundRawSampleWindow::node`).
    pub fn evaluate(&self, root: usize, time_ms: i64, store: &SummaryStore<Batch>,
        raw: &BTreeMap<u64, Vec<RawSample>>) -> Result<Vec<ResultSample>, Unanswered>;
}

// data_plane/src/lib.rs
/// `plan` or `forwarded`.
pub const EXECUTION_HEADER: &str = "x-asap-execution";
/// Why a planned query was forwarded.
pub const FORWARD_REASON_HEADER: &str = "x-asap-forward-reason";

Before this PR

Every query was forwarded to Prometheus.

After this PR

Example 4b without raw data answers quantile_over_time(0.99, latency_ms[60m]) at 70 min from six stored panes, reading no bound raw-sample window; Example 1 answers both queries from bound raw-sample windows fetched from Prometheus.

Usage

// The plan, SDS and precompute engine of #838's usage (Example 4b, no raw data).
let query = QueryEngine::new(&plan, &sds);
// Evaluation time: 70 min, on the 10-minute pane grid.
let t = 70 * 60_000;
// The root answering the query and the bound raw-sample windows it needs (none: all panes are stored).
let (root, ranges) = query.bound_raw_sample_windows(&plan.queries[0].query, t, &precompute.store)?;
// Run the root over the stored panes.
let result = query.evaluate(root, t, &precompute.store, &BTreeMap::new())?;
// bound_raw_sample_windows=0 result=[ResultSample { labels: {"job": "a"}, value: 237.0 }]

Over HTTP the same query returns a Prometheus vector with x-asap-execution: plan (data_plane/tests/answers.rs).

Limitations

  • Only instant queries are answered.
  • Stored panes need T on the pane grid.
  • The SDS is in memory only.
  • Queries run on the ingestion engine thread: a slow query delays ingestion behind it, and vice versa. A separate query pool comes with the concurrent engine later in the stack.
  • Each raw fetch has the forward timeout (120 s by default). Fetches for one query run concurrently, so a query waits for at most one timeout.
  • Each fetched sample's labels are copied into the executor's Sample, because RawSampleSelector::window_batch takes owned label maps. This is the same ASAPPlanner follow-up noted in feat(precompute): build stored summary panes from Remote Write samples #820.
  • Stored outputs built in a QueryRun (StoredOutputBuild::QueryRun) are not built yet: a query that reads one is forwarded, and the data plane logs a warning at startup when the plan has any. feat(query): build stored outputs built in a QueryRun on first use #824 builds them.
  • Standalone scalar roots aren't supported (the control plane rejects them).

Verification

  • data_plane/tests/query.rs (6 tests):
    • examples_answer_like_the_query_time_reference: every query of examples 1, 3b and 4b, with and without raw data, compared against InstallablePlan::query_time_reference over the same samples.
    • extreme_evaluation_times_are_not_answered: i64::MIN is refused, and i64::MAX does not panic.
    • bound_raw_sample_windows_carry_the_scans_label_matchers.
    • The earlier three: stored panes, bound raw-sample windows, and unanswerable queries.
  • data_plane/tests/answers.rs: rejects_evaluation_times_out_of_range covers -1e300, 1e300, NaN and inf, each answered with 400 bad_data, with the engine still running. Before this fix, -1e300 panicked the engine thread, so the response was 502 and ingestion stopped.
  • Unit tests: parses_instant_query_parameters (out-of-range times), builds_escaped_range_selectors, sanitizes_header_values.
  • data_plane/tests/answers.rs (4 tests) over HTTP against a stand-in Prometheus: Example 1 from raw samples, including the left-open range start; Example 4b from panes built through Remote Write; and raw_range_reads_forward_auth_and_tenancy_headers. In that last test the stand-in Prometheus answers 401 without Authorization and X-Scope-OrgID. With the headers the plan answers; without them the raw read fails and the query is forwarded with the reason.
  • Ran the MVP CI steps on this tree (Rust 1.99.1, --locked). All passed. The Usage output is from a scratch run on this tree.

@zzylol
zzylol force-pushed the feat/precompute-engine branch from eb8a314 to f9c3343 Compare October 18, 2026 15:40
@zzylol
zzylol force-pushed the feat/query-engine branch from 6f7b53c to b1b1e5a Compare October 18, 2026 15:40
@zzylol zzylol added area: data plane Runtime ingestion, storage, and query-serving data paths area: query engine PromQL lowering, execution, result shaping, and fallback behavior enhancement New feature or request feature: PromQL PromQL compatibility and execution semantics labels Oct 18, 2026
@zzylol
zzylol force-pushed the feat/precompute-engine branch from f9c3343 to b23b37e Compare October 20, 2026 16:09
@zzylol
zzylol force-pushed the feat/query-engine branch from b1b1e5a to 2142441 Compare October 20, 2026 20:06
@zzylol
zzylol changed the base branch from feat/precompute-engine to feat/installable-plan October 20, 2026 20:06
@zzylol
zzylol force-pushed the feat/query-engine branch from 2142441 to 1bdf7e9 Compare October 20, 2026 23:26
Each plan root (InstallablePlan.queries) is compiled once with two kinds
of inputs: stored outputs, bound at evaluation time T to their
definition's summary ending at T - offset, and the plan's query-time raw
windows (query_time_raw_windows), bound to the metric's samples in
(T - offset - width, T - offset] fetched from Prometheus and selected by
the window's RawSampleSelector. An instant query whose text is a planned
query is answered with a Prometheus vector and x-asap-execution: plan.
Unplanned queries are forwarded; planned ones that cannot be answered
(summary missing or off the coverage grid, fetch or execution failure)
are forwarded with x-asap-forward-reason.

Raw-window reads carry the caller's end-to-end headers (Authorization,
X-Scope-OrgID and the rest query forwarding keeps), so Prometheus
authorizes and scopes them as it would the caller's own query. Answer
labels come from series_labels_of_output_row.
- A query whose time is not finite or beyond MAX_TIME_MS gets 400
  bad_data, as Prometheus answers; window and pane binding use checked
  arithmetic, so no evaluation time can overflow (time=-1e300 panicked the
  engine thread in debug builds).
- Raw-window fetches select only the series the plan keeps: the Scan's
  label matchers PromQL can express go into the range selector, with
  metric and values escaped. The selector still applies every matcher.
- Raw windows are fetched concurrently.
- A forward reason with characters a header cannot carry is sanitized
  instead of dropped.
A plan that keeps query-run summaries installs, but this data plane does
not build them yet, so queries reading them are forwarded. Say so in the
log at startup.
@zzylol
zzylol force-pushed the feat/query-engine branch from 1bdf7e9 to afa5bc6 Compare October 20, 2026 23:46
@zzylol
zzylol changed the base branch from feat/installable-plan to main October 20, 2026 23:46
@zzylol
zzylol marked this pull request as ready for review October 20, 2026 23:46
Stored outputs built in a QueryRun (MatAtQueryKeptAcrossEvaluations) and
in an IngestionRun are named as the Planner names them, in the startup
warning, errors, docs and test names.
… bound raw-sample window

RawRange becomes BoundRawSampleWindow and QueryEngine::raw_ranges becomes
bound_raw_sample_windows, the design doc's term for a plan RawSampleWindow
bound to one evaluation time.
@zzylol
zzylol merged commit 48bfe2e into main Oct 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: data plane Runtime ingestion, storage, and query-serving data paths area: query engine PromQL lowering, execution, result shaping, and fallback behavior enhancement New feature or request feature: PromQL PromQL compatibility and execution semantics

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant