From a3ec113cd95c05599f5276537fdbcfd3a793cdcc Mon Sep 17 00:00:00 2001 From: Milind Srivastava Date: Mon, 2 Nov 2026 21:07:09 -0500 Subject: [PATCH 1/2] fix(plan-selection): name the pricing reason when the selection MILP is infeasible Candidates that cannot be priced (e.g. a source with no arrival rate) are dropped before the MILP. When that leaves it infeasible, the rejection said only "infeasible", hiding the missing source fact. The rejection now appends each distinct pricing error. Fixes the DAG viewer test test_a_plan_without_a_rate_fact_names_the_missing_fact, red on main since #1144. Co-Authored-By: Claude Opus 5.5 --- crates/plan-selection/src/select.rs | 35 +++++++++++++++-------- crates/plan-selection/src/select/tests.rs | 20 +++++++++++++ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/crates/plan-selection/src/select.rs b/crates/plan-selection/src/select.rs index 5c9628762..740362755 100644 --- a/crates/plan-selection/src/select.rs +++ b/crates/plan-selection/src/select.rs @@ -23,7 +23,7 @@ //! candidates' contexts apart), the timings, the warm start's cost, the //! objective, `plan_cost` and the dual bound (measurement only). -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::time::Instant; use asap_cost::{kept_tumbling_window_chains, node_map, plan_cost, CostInputs}; @@ -106,9 +106,10 @@ pub fn select_plan( split_into_parts: true, }, ) - .map_err(selection_error)?; + .map_err(|error| selection_error(error, &inputs))?; let build_ms = construction_started.elapsed().as_millis() as u64; - let solution = solve_selection_milp(milp, deadline, build_ms).map_err(selection_error)?; + let solution = solve_selection_milp(milp, deadline, build_ms) + .map_err(|error| selection_error(error, &inputs))?; let validation_started = Instant::now(); let chosen = ChosenCandidates { @@ -212,8 +213,10 @@ pub fn select_plan( }) } -/// The selection MILP's error as Stage 3's. -fn selection_error(error: SelectionMilpError) -> ErrorSelectingPlan { +/// The selection MILP's error as Stage 3's. An infeasible MILP also names +/// why candidates were left out unpriced: a missing source fact there is +/// usually the cause, and "infeasible" alone hides it. +fn selection_error(error: SelectionMilpError, inputs: &MilpInputs) -> ErrorSelectingPlan { let infeasible = |id: String, cause: RejectionCause, reason: String| { ErrorSelectingPlan::NoValidCandidate(vec![Rejection { id, cause, reason }]) }; @@ -226,13 +229,21 @@ fn selection_error(error: SelectionMilpError) -> ErrorSelectingPlan { roots, entries, }, - SelectionMilpError::Infeasible => infeasible( - "workload".into(), - RejectionCause::Infeasible, - "no plan meets every query's accuracy target, latency bound and the reader \ - constraints together (the selection MILP is infeasible)" - .into(), - ), + SelectionMilpError::Infeasible => { + let unpriced: BTreeSet<&str> = inputs + .candidate_data + .iter() + .filter_map(|data| data.usage.as_ref().err().map(String::as_str)) + .collect(); + let mut reason = "no plan meets every query's accuracy target, latency bound and \ + the reader constraints together (the selection MILP is infeasible)" + .to_string(); + if !unpriced.is_empty() { + reason += "; candidates left out because they could not be priced: "; + reason += &unpriced.into_iter().collect::>().join("; "); + } + infeasible("workload".into(), RejectionCause::Infeasible, reason) + } SelectionMilpError::NoSolution { status } => ErrorSelectingPlan::NoPlanByDeadline(format!( "HiGHS stopped ({status}) without an integral plan" )), diff --git a/crates/plan-selection/src/select/tests.rs b/crates/plan-selection/src/select/tests.rs index 4bc561ec1..bd2c4c0a7 100644 --- a/crates/plan-selection/src/select/tests.rs +++ b/crates/plan-selection/src/select/tests.rs @@ -173,3 +173,23 @@ fn cost_model_2_is_unsupported() { Err(ErrorSelectingPlan::UnsupportedCostModel(_)) )); } + +// With no fact bounding `m`'s rows, no candidate can be priced and the MILP +// is infeasible; the rejection names the missing fact, not only +// "infeasible" (the DAG viewer shows it). +#[test] +fn an_unpriceable_workload_names_the_missing_fact() { + let mut fixture = shared_sum(); + fixture.data = asap_types::workload::DataWorkload::default(); + let stage2 = realizer(&fixture); + match select(&fixture, &BuiltinAccuracyModel, &stage2) { + Err(ErrorSelectingPlan::NoValidCandidate(rejected)) => { + let reasons = format!("{rejected:?}"); + assert!( + reasons.contains("pricing source m needs an arrival rate"), + "{reasons}" + ) + } + other => panic!("{other:?}"), + } +} From 7aa529f590374de71830c62f2c167c2ba1b4796c Mon Sep 17 00:00:00 2001 From: Milind Srivastava Date: Mon, 2 Nov 2026 21:24:21 -0500 Subject: [PATCH 2/2] fix(plan-selection): import AnalyticalCostError from asap_cost in selector.rs #1164 imports crate::AnalyticalCostError, which #1187 moved into lib.rs's test module; the two merged in that order, so main does not compile. Co-Authored-By: Claude Opus 5.5 --- crates/plan-selection/src/selector.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/plan-selection/src/selector.rs b/crates/plan-selection/src/selector.rs index b3573dfbc..21a417487 100644 --- a/crates/plan-selection/src/selector.rs +++ b/crates/plan-selection/src/selector.rs @@ -16,7 +16,8 @@ use crate::checks::{accuracy_check, accuracy_targets, latency_violation}; use crate::report::{ AcceptedAccuracy, ErrorSelectingPlan, Rejection, RejectionCause, WholePlanSelection, }; -use crate::{AnalyticalCostError, SelectionModels}; +use crate::SelectionModels; +use asap_cost::analytical::AnalyticalCostError; use asap_cost::{ kept_tumbling_window_chains, node_map, plan_cost, query_reach, tumbling_window_retention_ms, };