Skip to content

Commit 612b416

Browse files
Merge remote-tracking branch 'origin/main' into 444-refactor-unify-prometheus_scrape_interval-and-data_ingestion_interval-into-a-single-concept
2 parents 54b479f + 23ab8f9 commit 612b416

2 files changed

Lines changed: 89 additions & 24 deletions

File tree

asap-planner-rs/src/optimizer/candidate_gen.rs

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub fn enumerate_candidates(aqe: &AQE, scrape_interval_ms: u64) -> Vec<Candidate
5151
for (window_type, w, slide_interval, n) in
5252
window_candidates(range_a_ms, aqe.min_t_repeat_ms, scrape_interval_ms)
5353
{
54-
let Some(qm) = determine_query_method(window_type, n, &props) else {
54+
let Some(qm) = determine_query_method(n, &props) else {
5555
continue;
5656
};
5757

@@ -90,12 +90,11 @@ fn exact_candidate() -> CandidateConfig {
9090
///
9191
/// Tumbling: all W that divide range_a, are multiples of scrape_interval, and ≤ min_t_repeat.
9292
/// Slide interval = W (a tumbling window "slides" by its own width).
93-
/// Sliding: W = range_a exactly (overlapping sliding windows can't merge/subtract to cover
94-
/// a larger range — only mutually-exclusive tumbling windows compose). The slide
95-
/// interval S is still a free choice ≤ W: smaller S means more concurrent
96-
/// overlapping sub-windows maintained internally (⌈W/S⌉), which costs more
97-
/// ingest CPU/mem but gives fresher results. Doubling from scrape_interval up to
98-
/// W keeps the grid small while covering that tradeoff.
93+
/// Sliding: W = range_a / k for each W that is a multiple of scrape_interval and divides
94+
/// range_a (k = range_a / W). At query time k staggered readings are merged or
95+
/// subtracted to cover range_a. Freshness is bounded by S (the slide interval),
96+
/// not by W — so the guard is S ≤ min_t_repeat_ms, not W ≤ min_t_repeat_ms.
97+
/// S doubles from scrape_interval up to min(W, min_t_repeat_ms).
9998
fn window_candidates(
10099
range_a_ms: u64,
101100
min_t_repeat_ms: u64,
@@ -119,35 +118,38 @@ fn window_candidates(
119118
w += scrape_interval_ms;
120119
}
121120

122-
// Sliding: W = range_a exactly; S doubles from scrape_interval up to W.
123-
if range_a <= min_t_repeat_ms {
124-
let mut s = scrape_interval_ms;
125-
while s <= range_a {
126-
out.push((WindowType::Sliding, range_a, s, 1));
127-
s *= 2;
121+
// Sliding: W = range_a / k for each valid W (multiple of scrape_interval, divides range_a).
122+
// S doubles from scrape_interval up to min(W, min_t_repeat_ms). n_windows = k.
123+
let mut w = scrape_interval_ms;
124+
while w <= range_a {
125+
if range_a.is_multiple_of(w) {
126+
let k = range_a / w;
127+
let mut s = scrape_interval_ms;
128+
while s <= w.min(min_t_repeat_ms) {
129+
out.push((WindowType::Sliding, w, s, k));
130+
s *= 2;
131+
}
128132
}
133+
w += scrape_interval_ms;
129134
}
130135

131136
out
132137
}
133138

134-
/// Determine query method from (window_type, n_windows, sketch algebra).
135-
/// Returns None when the combination is infeasible (tumbling + W < range_a + neither property).
139+
/// Determine query method from (n_windows, sketch algebra).
140+
/// Returns None when the combination is infeasible (W < range_a + neither merge nor subtract).
136141
fn determine_query_method(
137-
window_type: WindowType,
138142
n_windows: u64,
139143
props: &super::sketch_properties::SketchProperties,
140144
) -> Option<QueryMethod> {
141145
if n_windows == 1 {
142146
// W = range_a (or spatial-only): one completed window covers the query range exactly.
143147
return Some(QueryMethod::Direct);
144148
}
145-
// n > 1 means W < range_a; sliding is excluded (sliding enforces W = range_a above).
146-
debug_assert_eq!(window_type, WindowType::Tumbling);
149+
// n > 1: partial-width windows (W < range_a); valid for both Tumbling and Sliding.
147150
if props.subtractable {
148151
Some(QueryMethod::Subtract)
149152
} else if props.mergeable {
150-
debug_assert!(n_windows > 1, "Merge requires merging >1 window");
151153
Some(QueryMethod::Merge {
152154
num_windows: n_windows,
153155
})
@@ -289,6 +291,7 @@ fn param_grid(
289291
#[cfg(test)]
290292
mod tests {
291293
use super::*;
294+
use asap_types::enums::WindowType;
292295
use promql_utilities::data_model::KeyByLabelNames;
293296

294297
fn make_aqe(stat: Statistic, range_ms: Option<u64>, min_t: u64) -> AQE {
@@ -367,4 +370,64 @@ mod tests {
367370
);
368371
}
369372
}
373+
374+
#[test]
375+
fn partial_width_sliding_candidates_are_generated() {
376+
// range_a=600_000ms, min_t=30_000ms, scrape=30_000ms.
377+
// W=300_000 (k=2) with S=30_000 should be emitted alongside the full-width W=600_000.
378+
let aqe = make_aqe(Statistic::Min, Some(600_000), 30_000);
379+
let candidates = enumerate_candidates(&aqe, 30_000);
380+
let partial = candidates.iter().find(|c| {
381+
c.config.as_ref().is_some_and(|cfg| {
382+
cfg.window_type == WindowType::Sliding
383+
&& cfg.window_size_ms == 300_000
384+
&& c.n_windows == 2
385+
})
386+
});
387+
assert!(
388+
partial.is_some(),
389+
"expected a partial-width Sliding candidate with W=300_000ms, k=2"
390+
);
391+
assert!(
392+
matches!(
393+
partial.unwrap().query_method,
394+
QueryMethod::Merge { num_windows: 2 }
395+
),
396+
"partial Sliding with a mergeable-only sketch should produce Merge{{2}}"
397+
);
398+
}
399+
400+
#[test]
401+
fn sliding_freshness_guard_uses_slide_not_width() {
402+
// range_a=600_000ms > min_t_repeat=30_000ms. The old guard (range_a <= min_t_repeat)
403+
// incorrectly rejected all Sliding candidates here. New guard: S ≤ min_t_repeat.
404+
// W=600_000, S=30_000 ≤ min_t_repeat → full-width Direct Sliding should be present.
405+
let aqe = make_aqe(Statistic::Sum, Some(600_000), 30_000);
406+
let candidates = enumerate_candidates(&aqe, 30_000);
407+
assert!(
408+
candidates.iter().any(|c| {
409+
c.config.as_ref().is_some_and(|cfg| {
410+
cfg.window_type == WindowType::Sliding && cfg.window_size_ms == 600_000
411+
}) && c.query_method == QueryMethod::Direct
412+
&& c.n_windows == 1
413+
}),
414+
"full-width Sliding Direct should be generated even when range_a > min_t_repeat"
415+
);
416+
}
417+
418+
#[test]
419+
fn partial_sliding_subtractable_sketch_gets_subtract() {
420+
// Sum → CMS (subtractable): partial Sliding with k=2 should produce Subtract.
421+
let aqe = make_aqe(Statistic::Sum, Some(600_000), 30_000);
422+
let candidates = enumerate_candidates(&aqe, 30_000);
423+
assert!(
424+
candidates.iter().any(|c| {
425+
c.config
426+
.as_ref()
427+
.is_some_and(|cfg| cfg.window_type == WindowType::Sliding && c.n_windows == 2)
428+
&& c.query_method == QueryMethod::Subtract
429+
}),
430+
"partial Sliding with subtractable sketch should produce Subtract"
431+
);
432+
}
370433
}

asap-planner-rs/src/optimizer/solution.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ pub struct AQE {
2222
pub query_frequency_hz: f64,
2323

2424
/// Minimum repeat interval across all RQEs that reference this AQE (ms).
25-
/// Determines the freshness constraint on the window size: W ≤ min_t_repeat
26-
/// ensures a completed window is available for every dashboard's cycle.
27-
/// When multiple RQEs share this AQE, the fastest dashboard is the binding
28-
/// constraint.
25+
/// Determines the freshness constraint on the streaming config: for Tumbling,
26+
/// W ≤ min_t_repeat ensures a completed window is available every cycle; for
27+
/// Sliding, S ≤ min_t_repeat is the binding constraint (fresh answers arrive
28+
/// every slide interval, not every W). When multiple RQEs share this AQE,
29+
/// the fastest dashboard is the binding constraint.
2930
pub min_t_repeat_ms: u64,
3031

3132
/// GCD of all repeat intervals across RQEs that reference this AQE (ms).
@@ -46,7 +47,8 @@ pub enum QueryMethod {
4647
Direct,
4748

4849
/// W < range_a, sketch is mergeable: combine `num_windows` retained
49-
/// tumbling sub-windows at query time. Cost scales linearly with num_windows.
50+
/// sub-windows at query time (Tumbling or partial-width Sliding).
51+
/// Cost scales linearly with num_windows.
5052
Merge { num_windows: u64 },
5153

5254
/// W < range_a, sketch is subtractable: subtract two prefix-sum checkpoints.

0 commit comments

Comments
 (0)