@@ -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).
9998fn 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 ).
136141fn 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) ]
290292mod 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}
0 commit comments