Skip to content

Commit 0bfda0d

Browse files
fix(planner): reject avg before solving and write configs atomically
- asap-optimizer-cli --output-dir rejects avg queries before the MILP solve; print-only runs still plan them. - Both YAML strings are serialized before either file is written. - Each item is visited once per deployment, not once per occurrence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 8a6e92e commit 0bfda0d

3 files changed

Lines changed: 33 additions & 21 deletions

File tree

‎asap-planner-rs/src/bin/optimizer_cli.rs‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use std::path::PathBuf;
88

99
use asap_planner::optimizer::{
1010
build_milp_workload, load_flat_atomic_cost_table, load_optional_selected_atomic_cost_table,
11-
load_workload_facts, plan_to_planner_output, run_greedy_pipeline, solve_milp, AtomicCostTable,
12-
LabelSetFacts, LabelSetFactsError,
11+
load_workload_facts, plan_to_planner_output, reject_avg_queries, run_greedy_pipeline,
12+
solve_milp, AtomicCostTable, LabelSetFacts, LabelSetFactsError,
1313
};
1414
use asap_planner::ControllerConfig;
1515
use clap::Parser;
@@ -200,6 +200,10 @@ fn run_milp(args: &Args, config: &ControllerConfig) -> anyhow::Result<()> {
200200
let objective = Objective::AUCCost { w_cpu, w_mem };
201201
tracing::debug!(?objective, cost_rows = costs.len(), "milp: inputs loaded");
202202

203+
// Fail before solving when the plan would be written but can't be.
204+
if args.output_dir.is_some() {
205+
reject_avg_queries(config)?;
206+
}
203207
let workload = build_milp_workload(config, &facts, args.data_ingestion_interval_ms)?;
204208
let solution = solve_milp(
205209
&workload,
@@ -259,15 +263,13 @@ fn run_milp(args: &Args, config: &ControllerConfig) -> anyhow::Result<()> {
259263

260264
if let Some(dir) = &args.output_dir {
261265
let output = plan_to_planner_output(config, &workload, &solution)?;
266+
// Serialize both before writing either, so a failure can't leave a
267+
// new streaming config next to a stale inference config.
268+
let streaming = output.to_streaming_yaml_string()?;
269+
let inference = output.to_inference_yaml_string()?;
262270
std::fs::create_dir_all(dir)?;
263-
std::fs::write(
264-
dir.join("streaming_config.yaml"),
265-
output.to_streaming_yaml_string()?,
266-
)?;
267-
std::fs::write(
268-
dir.join("inference_config.yaml"),
269-
output.to_inference_yaml_string()?,
270-
)?;
271+
std::fs::write(dir.join("streaming_config.yaml"), streaming)?;
272+
std::fs::write(dir.join("inference_config.yaml"), inference)?;
271273
println!("\nwrote configs to {}", dir.display());
272274
}
273275
Ok(())

‎asap-planner-rs/src/optimizer/milp_output.rs‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Turns a solved MILP plan into the planner's streaming and inference YAML.
22
3-
use std::collections::HashMap;
3+
use std::collections::{HashMap, HashSet};
44

55
use asap_types::enums::{CleanupPolicy, WindowType};
66
use indexmap::map::Entry;
@@ -47,6 +47,19 @@ pub enum MilpOutputError {
4747
Generator(#[from] ControllerError),
4848
}
4949

50+
/// Errors on the first avg query: its plan can be costed but not deployed.
51+
pub fn reject_avg_queries(config: &ControllerConfig) -> Result<(), MilpOutputError> {
52+
match config
53+
.query_groups
54+
.iter()
55+
.flat_map(|group| &group.queries)
56+
.find(|query| contains_avg(query))
57+
{
58+
Some(query) => Err(MilpOutputError::AvgQuery(query.clone())),
59+
None => Ok(()),
60+
}
61+
}
62+
5063
/// Streaming and inference YAML for `solution`. Aggregation ids follow the
5164
/// plan's deployment order, starting at 1. No cleanup policy yet, so the
5265
/// retained instance counts are not emitted.
@@ -55,19 +68,16 @@ pub fn plan_to_planner_output(
5568
workload: &MilpWorkload,
5669
solution: &MilpSolution,
5770
) -> Result<PlannerOutput, MilpOutputError> {
58-
if let Some(query) = config
59-
.query_groups
60-
.iter()
61-
.flat_map(|group| &group.queries)
62-
.find(|query| contains_avg(query))
63-
{
64-
return Err(MilpOutputError::AvgQuery(query.clone()));
65-
}
71+
reject_avg_queries(config)?;
6672

6773
let item_of = |raqe: usize| &workload.items[workload.raqe_items[raqe]];
74+
// Each item once per deployment, however many occurrences it has.
6875
let mut served: Vec<Vec<&OptimizerItem>> = vec![Vec::new(); solution.deployments.len()];
76+
let mut seen = HashSet::new();
6977
for (raqe, planned) in solution.raqes.iter().enumerate() {
70-
served[planned.deployment].push(item_of(raqe));
78+
if seen.insert((planned.deployment, workload.raqe_items[raqe])) {
79+
served[planned.deployment].push(item_of(raqe));
80+
}
7181
}
7282

7383
let mut aggregations: IndexMap<String, IntermediateAggConfig> = IndexMap::new();

‎asap-planner-rs/src/optimizer/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub use error::{OptimizerError, UnservableItem};
2626
pub use greedy::greedy_assign;
2727
pub use label_set_facts::{ItemFacts, LabelSetFacts, LabelSetFactsError, LabelSetKey, SeriesKey};
2828
pub use milp::{build_milp_workload, solve_milp, MilpError, MilpWorkload};
29-
pub use milp_output::{plan_to_planner_output, MilpOutputError};
29+
pub use milp_output::{plan_to_planner_output, reject_avg_queries, MilpOutputError};
3030
pub use pipeline::{run_greedy_pipeline, OptimizerPipelineError};
3131
pub use sketch_properties::{sketch_properties, SketchProperties};
3232
pub use solution::{AQEAssignment, OptimizerItem, OptimizerSolution, QueryMethod};

0 commit comments

Comments
 (0)