Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions asap-planner-rs/src/optimizer/aqe_extractor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ impl AQEKey {
///
/// Leaf queries that do not match any supported pattern (e.g. unsupported
/// functions, parse errors) are skipped with a warning.
pub fn extract_aqes(rqes: &[RQE], metric_schema: &PromQLSchema) -> Vec<AQE> {
pub fn extract_aqes(
rqes: &[RQE],
metric_schema: &PromQLSchema,
scrape_interval_ms: u64,
) -> Vec<AQE> {
// (key) -> (requirements, query_strings, sum_freq, min_t, gcd_t)
let mut acc: HashMap<AQEKey, (QueryRequirements, Vec<String>, f64, u64, u64)> = HashMap::new();

Expand All @@ -78,7 +82,12 @@ pub fn extract_aqes(rqes: &[RQE], metric_schema: &PromQLSchema) -> Vec<AQE> {

for leaf in leaves {
match extract_requirements(&leaf, metric_schema) {
Some(req) => {
Some(mut req) => {
// Spatial-only queries have no range vector; treat them as a
// single scrape interval so downstream window logic is explicit.
if req.data_range_ms.is_none() {
req.data_range_ms = Some(scrape_interval_ms);
}
let key = AQEKey::from_requirements(&req);
let entry = acc
.entry(key)
Expand Down Expand Up @@ -192,7 +201,7 @@ mod tests {
#[test]
fn single_temporal_query() {
let rqes = vec![rqe("sum_over_time(metric[5m])", 60_000)];
let aqes = extract_aqes(&rqes, &empty_schema());
let aqes = extract_aqes(&rqes, &empty_schema(), 15_000);
assert_eq!(aqes.len(), 1);
assert!((aqes[0].query_frequency_hz - 1.0 / 60.0).abs() < 1e-9);
assert_eq!(aqes[0].requirements.metric, "metric");
Expand All @@ -205,14 +214,14 @@ mod tests {
"sum_over_time(metric_a[5m]) / sum_over_time(metric_b[5m])",
60_000,
)];
let aqes = extract_aqes(&rqes, &empty_schema());
let aqes = extract_aqes(&rqes, &empty_schema(), 15_000);
assert_eq!(aqes.len(), 2);
}

#[test]
fn binary_with_scalar_produces_one_aqe() {
let rqes = vec![rqe("sum_over_time(metric[5m]) * 100", 60_000)];
let aqes = extract_aqes(&rqes, &empty_schema());
let aqes = extract_aqes(&rqes, &empty_schema(), 15_000);
assert_eq!(aqes.len(), 1);
}

Expand All @@ -222,7 +231,7 @@ mod tests {
rqe("sum_over_time(metric[5m])", 60_000),
rqe("sum_over_time(metric[5m])", 30_000),
];
let aqes = extract_aqes(&rqes, &empty_schema());
let aqes = extract_aqes(&rqes, &empty_schema(), 15_000);
assert_eq!(aqes.len(), 1);
// query_frequency_hz = sum of rates (total query load for the MIP objective)
let expected_freq = 1.0 / 60.0 + 1.0 / 30.0;
Expand All @@ -236,7 +245,15 @@ mod tests {
#[test]
fn unsupported_query_is_skipped() {
let rqes = vec![rqe("not_a_real_function(metric[5m])", 60_000)];
let aqes = extract_aqes(&rqes, &empty_schema());
let aqes = extract_aqes(&rqes, &empty_schema(), 15_000);
assert_eq!(aqes.len(), 0);
}

#[test]
fn spatial_only_query_sets_range_to_scrape_interval() {
let rqes = vec![rqe("sum(metric)", 60_000)];
let aqes = extract_aqes(&rqes, &empty_schema(), 15_000);
assert_eq!(aqes.len(), 1);
assert_eq!(aqes[0].requirements.data_range_ms, Some(15_000));
}
}
40 changes: 15 additions & 25 deletions asap-planner-rs/src/optimizer/candidate_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub fn enumerate_candidates(aqe: &AQE, scrape_interval_ms: u64) -> Vec<Candidate
}

let stat = aqe.requirements.statistics[0];
let range_a_ms = aqe.requirements.data_range_ms;
let range_a_ms = aqe.requirements.data_range_ms.unwrap_or(scrape_interval_ms);

for &agg_type in compatible_agg_types(stat) {
let props = sketch_properties(agg_type);
Expand Down Expand Up @@ -88,30 +88,20 @@ fn exact_candidate() -> CandidateConfig {

/// Window candidates: (WindowType, W_ms, slide_interval_ms, n_windows).
///
/// For spatial-only queries (range = None): one tumbling window of scrape_interval.
/// For range-based queries:
/// Tumbling: all W that divide range_a, are multiples of scrape_interval, and ≤ min_t_repeat.
/// Slide interval = W (a tumbling window "slides" by its own width).
/// Sliding: W = range_a exactly (overlapping sliding windows can't merge/subtract to cover
/// a larger range — only mutually-exclusive tumbling windows compose). The slide
/// interval S is still a free choice ≤ W: smaller S means more concurrent
/// overlapping sub-windows maintained internally (⌈W/S⌉), which costs more
/// ingest CPU/mem but gives fresher results. Doubling from scrape_interval up to
/// W keeps the grid small while covering that tradeoff.
/// Tumbling: all W that divide range_a, are multiples of scrape_interval, and ≤ min_t_repeat.
/// Slide interval = W (a tumbling window "slides" by its own width).
/// Sliding: W = range_a exactly (overlapping sliding windows can't merge/subtract to cover
/// a larger range — only mutually-exclusive tumbling windows compose). The slide
/// interval S is still a free choice ≤ W: smaller S means more concurrent
/// overlapping sub-windows maintained internally (⌈W/S⌉), which costs more
/// ingest CPU/mem but gives fresher results. Doubling from scrape_interval up to
/// W keeps the grid small while covering that tradeoff.
fn window_candidates(
range_a_ms: Option<u64>,
range_a_ms: u64,
min_t_repeat_ms: u64,
scrape_interval_ms: u64,
) -> Vec<(WindowType, u64, u64, u64)> {
let Some(range_a) = range_a_ms else {
// Spatial-only: any window works (window_compatible returns true for None range).
return vec![(
WindowType::Tumbling,
scrape_interval_ms,
scrape_interval_ms,
1,
)];
};
let range_a = range_a_ms;
if range_a == 0 || scrape_interval_ms == 0 {
return vec![];
}
Expand All @@ -122,7 +112,7 @@ fn window_candidates(
// Tumbling: W divides range_a, is a multiple of scrape_interval, and ≤ max_w.
let mut w = scrape_interval_ms;
while w <= max_w {
if range_a % w == 0 {
if range_a.is_multiple_of(w) {
let n = range_a / w;
out.push((WindowType::Tumbling, w, w, n));
}
Expand Down Expand Up @@ -329,10 +319,10 @@ mod tests {
}

#[test]
fn spatial_only_produces_neither_candidates() {
let aqe = make_aqe(Statistic::Sum, None, 60_000);
fn spatial_only_produces_direct_candidates() {
// Spatial-only: range = scrape_interval (set by extract_aqes). One Direct window.
let aqe = make_aqe(Statistic::Sum, Some(15_000), 60_000);
let candidates = enumerate_candidates(&aqe, 15_000);
// All non-EXACT candidates should be Direct (no temporal range to cover).
for c in candidates.iter().filter(|c| c.config.is_some()) {
assert_eq!(c.query_method, QueryMethod::Direct);
}
Expand Down
25 changes: 21 additions & 4 deletions asap-planner-rs/src/optimizer/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ use super::translator::{translate, TranslationSummary};
fn run_pipeline(
config: &ControllerConfig,
schema: &PromQLSchema,
scrape_interval_ms: u64,
solver_name: &str,
solve: impl FnOnce(Vec<AQE>) -> OptimizerSolution,
) -> (StreamingConfig, InferenceConfig) {
let rqes = config_to_rqes(config);
let aqes = extract_aqes(&rqes, schema);
let aqes = extract_aqes(&rqes, schema, scrape_interval_ms);
let solution = solve(aqes);

let summary = TranslationSummary::from_solution(&solution);
Expand Down Expand Up @@ -47,8 +48,15 @@ fn run_pipeline(
pub fn run_all_exact_pipeline(
config: &ControllerConfig,
schema: &PromQLSchema,
scrape_interval_ms: u64,
) -> (StreamingConfig, InferenceConfig) {
run_pipeline(config, schema, "all-EXACT", OptimizerSolution::all_exact)
run_pipeline(
config,
schema,
scrape_interval_ms,
"all-EXACT",
OptimizerSolution::all_exact,
)
}

/// Run the greedy optimizer pipeline (Phase 2): each AQE is assigned, independently,
Expand All @@ -66,7 +74,7 @@ pub fn run_greedy_pipeline(
scrape_interval_ms: u64,
arrival_rate_hz: f64,
) -> (StreamingConfig, InferenceConfig) {
run_pipeline(config, schema, "greedy", |aqes| {
run_pipeline(config, schema, scrape_interval_ms, "greedy", |aqes| {
greedy_assign(
aqes,
scrape_interval_ms,
Expand Down Expand Up @@ -128,7 +136,7 @@ mod tests {
("sum(other_metric)", 30_000),
]);
let schema = PromQLSchema::new();
let (streaming, _inference) = run_all_exact_pipeline(&config, &schema);
let (streaming, _inference) = run_all_exact_pipeline(&config, &schema, 15_000);
// All-EXACT: no streaming configs deployed.
assert!(streaming.get_all_aggregation_configs().is_empty());
}
Expand All @@ -142,6 +150,15 @@ mod tests {
assert!(!inference.query_configs.is_empty());
}

#[test]
fn spatial_only_aqe_gets_explicit_range_from_pipeline() {
let config = make_config(&[("sum(metric)", 60_000)]);
let rqes = config_to_rqes(&config);
let aqes = extract_aqes(&rqes, &PromQLSchema::new(), 15_000);
assert_eq!(aqes.len(), 1);
assert_eq!(aqes[0].requirements.data_range_ms, Some(15_000));
}

#[test]
fn config_to_rqes_flattens_groups() {
let config = make_config(&[
Expand Down
Loading