Skip to content

fix(planner): Stage 3 rejects plans the executor cannot compile; no PromQL top-k heap over its instant vector - #805

Merged
zzylol merged 1 commit into
mainfrom
fix/stage3-executor-compile-check
Oct 21, 2026
Merged

zzylol merged 1 commit into
mainfrom
fix/stage3-executor-compile-check

Conversation

@zzylol

@zzylol zzylol commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Why

#799 lets the MVP benchmark compare plans on cost alone. With guarantees ignored and no raw data retained, all 60 counter top-k and rate queries in the ASAPQuery random workloads (q08-topk-count, q09-rate, e.g. topk(k, sum by (G) (increase(data_total[W])))) select a plan the executor refuses to compile. Those plans keep exact Increase/Rate panes, then at query time build a CountSketchWithHeap and read its top-k estimate. The executor fails with "invalid keyed evaluation shape or capacity" or "summary update column missing or ambiguous".

What

  1. Stage 3 rejects a plan the executor cannot compile. The reject cause is the new RejectionCause::ExecutorCannotCompile, and the reason is the executor's own error. plan_installable runs the Planner executor's own compile, so the planner and the executor can't drift. It compiles each stored output's build and the query run, exactly as a runtime installs the plan.
  2. The real bug: Pass 1 offered a pointless sketch. A PromQL top-k ranks an instant vector, which has one row per series or group. A heap over those rows must read every row anyway, and an exact top-k over the same rows keeps only k of them. So the exact plan is no costlier and is exact. Pass 1 now prunes these heaps with the new PruneReason::TopKHeapOverOneRowPerItem. The fused whole-expression heap over the inner aggregate's input, where an item's rows repeat, is still offered. SQL top-k keeps its heaps.
  3. Exact top-k answers keep their labels. Over PromQL rows without a series identity, such as the rows of sum by (job), the exact top-k now keeps the label columns. Before, topk(10, sum by (job) (...)) answered values with no job label.

How

  • PlanSelector::offer asks models.executor_compile_check only for a candidate that could still be selected, after pricing. Compiling is the costliest check, so it runs last. A rejected candidate drops out, and the next best is selected.
  • PlannerExecutorCompileCheck rebuilds what the runtime installs: it applies timings, expands pane chains, and builds an InstallablePlan. Then InstallablePlan::check_compiles runs asap_executor::physical_planner::compile. It compiles each stored output over its raw-sample windows and stored inputs, then the query run over the stored outputs it reads and the query-time windows. A plan that isn't installable for another reason, such as a scalar root, passes the check, so plan_installable still reports that reason as before.
  • prune_topk_heaps_over_one_row_per_item runs after add_whole_expression_targets. It applies only when every scan below the top-k reads a time series.
  • implement_topk appends the child's other Utf8 label columns after [keys.., topk_k] when PromQL rows have no series identity.

Key code interfaces added / modified

crates/plan-selection/src/pass/mod.rs

/// New. The deployment executor's compiler, asked by Stage 3.
pub trait ExecutorCompileCheck {
    /// Err with the executor's reason when it cannot compile the plan of these timed roots.
    fn compiles(&self, roots: &[QueryRoot]) -> Result<(), String>;
}
pub struct PlanningModels<'a> {
    // accuracy, evidence, plan_selection, deployment_capabilities: unchanged
    /// The executor compile check; None (default) checks nothing.
    pub executor_compile_check: Option<&'a dyn ExecutorCompileCheck>, // new
}
impl<'a> PlanningModels<'a> {
    /// These models with that check.
    pub fn with_executor_compile_check(self, check: &'a dyn ExecutorCompileCheck) -> Self; // new
}

crates/plan-selection/src/lib.rs

pub enum RejectionCause {
    // every existing variant: unchanged
    /// The executor cannot compile the plan; the reason is its error. A failed check.
    ExecutorCannotCompile, // new
}

crates/planner/src/installable.rs

/// New. The Planner's executor as Stage 3's check; plan_installable sets it when the caller leaves it None.
pub struct PlannerExecutorCompileCheck;
impl InstallablePlan {
    /// New. Whether the Planner's executor compiles this plan as a runtime runs it.
    pub fn check_compiles(&self) -> Result<(), String>;
}

crates/logical-asap-optimization/src/per_computation_candidates.rs

pub enum PruneReason {
    // RequiresExact, NullableAvgInput, UnsupportedByDeployment, QuantileOutsideUnitInterval: unchanged
    /// A PromQL top-k's own heap over its instant vector (one row per item); exact dominates.
    TopKHeapOverOneRowPerItem, // new
}

Usage

// The user's query: the top 10 jobs by 5-minute increase, ε 0.05 at δ 0.3, every minute,
// over 10,000 counter series sampled every 100 ms.
let workload = counter_topk_workload("increase");
// No raw samples kept; guarantees ignored (#799).
let capabilities = DeploymentCapabilities { raw_data_retained: false, ..Default::default() };
let settings = PlanSelectionSettings { ignore_accuracy_guarantees: true, ..Default::default() };
// Pass 1 prunes the top-k's heaps (TopKHeapOverOneRowPerItem). Stage 3 compiles each
// candidate it could select with the Planner's executor before keeping it.
let plan = plan_installable(UserInput::new(&workload, FrontendInput::Promql { histograms: None },
    PlanningModels::builtin().with_deployment_capabilities(&capabilities).with_plan_selection(&settings))).await?;
// Exact increase panes, then Sort + Limit + Project [topk_10, job]. It compiles and runs:
// each job's sum equals the query-time reference and the hand-computed exact top 10.
plan.check_compiles()?;

Before this PR

With guarantees ignored, 60 of 60 counter top-k and rate queries selected a query-time heap over exact per-series values, and the executor refused to compile the plan. Stage 3 had no way to know that. Exact topk(k, sum by (G) (...)) answers also lost their G labels.

After this PR

Those queries select the exact plan, which compiles and answers exactly. Any future candidate the executor can't compile is rejected with a named cause instead of being selected.

Verification

  • cargo test --workspace --locked --no-fail-fast on the head, rebased on main at ee26f24: 151 test binaries, 1,822 passed, 0 failed, 36 ignored.
  • New tests:
    • runtime_install::ignoring_guarantees_counter_topk_plans_compile_and_answer_exactly: topk(10, sum by (job) (increase|rate(data_total[5m]))) with the setting on and no raw data. Before the fix, planning selected the query-time heap and the executor refused it. Now the selected plan has no sketch, check_compiles passes, and the answers equal query_time_reference and the exact top 10 jobs, with their job labels, to 1e-9.
    • runtime_install::check_compiles_reports_the_executors_error: a plan with a changed answer type is refused with the executor's reason.
    • plan_selection::tests::a_plan_the_executor_cannot_compile_is_rejected: the refused candidate is rejected as ExecutorCannotCompile with the executor's reason, and another is selected.
    • stage_pipeline::a_topk_heap_over_one_row_per_item_is_pruned_with_its_reason: replaces uncomposable_candidate_is_recorded_with_its_error. That test's uncomposable choice was exactly such a heap, and no PromQL candidate is uncomposable any more.
  • Updated tests:
    • default_candidate_limit_is_ten_thousand now uses quantile queries, three candidates each, because top-k now has one Pass 1 candidate.
    • promql_topk_over_grouped_aggregate_sorts_by_its_measure now expects the job column.
  • Compile check on its own: with the prune disabled, topk(10, sum by (job) (increase(data_total[5m]))) rejected P4-m1 as ExecutorCannotCompile ("summary update column missing or ambiguous") and selected exact P2-m1.
  • Real cases: I ran the ASAPQuery converter (asapquery_random_workload from the joint-search branch) on all 60 q08-topk-count and q09-rate queries across seed-862, seed-862-long and seed-862-x5. It used --no-raw --failure-probability 0.3 --check with the setting on. All 60 planned, compiled and ran. Every evaluation matched the exact answer: recall 1.0, max relative error 0.

Follow-ups, not in this PR:

  • The fused whole-expression heap's SummaryEstimate{TopK} output shape still doesn't match the executor's keyed evaluation (the ignored weighted_topk_binding test). plan_installable now rejects it whenever it would be selected.
  • SQL top-k still offers heaps over its per-key result.

@zzylol zzylol added binding Logical-to-physical binding and implementation selection bug Something isn't working optimizer Workload optimization and plan selection promql PromQL front-end lowering (L1→L2) labels Oct 21, 2026
@zzylol
zzylol changed the base branch from feat/ignore-accuracy-guarantees to main October 21, 2026 06:06
…QL top-k offers no heap over its instant vector

With accuracy guarantees ignored, counter top-k and rate queries selected
exact per-series panes followed by a query-time CountSketchWithHeap the
executor refuses to compile. Stage 3 now asks the deployment executor's
compiler (ExecutorCompileCheck; plan_installable defaults to the Planner's
executor) and rejects such a candidate as ExecutorCannotCompile. Pass 1
no longer offers a PromQL top-k's own heap over its instant vector, one
row per item, which an exact top-k dominates
(TopKHeapOverOneRowPerItem); the fused whole-expression heap stays. Exact
top-k answers over PromQL rows without a series identity keep their label
columns, so topk(k, sum by (job) (...)) answers each job.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

binding Logical-to-physical binding and implementation selection bug Something isn't working optimizer Workload optimization and plan selection promql PromQL front-end lowering (L1→L2)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant