@@ -435,12 +435,25 @@ impl Worker {
435435 . map ( |( _, ts, _) | * ts)
436436 . max ( )
437437 . unwrap_or ( i64:: MIN ) ;
438+ let batch_min_ts = samples
439+ . iter ( )
440+ . map ( |( _, ts, _) | * ts)
441+ . min ( )
442+ . unwrap_or ( batch_max_ts) ;
438443 let previous_wm = state. previous_watermark_ms ;
439444 let current_wm = if batch_max_ts > previous_wm {
440445 batch_max_ts
441446 } else {
442447 previous_wm
443448 } ;
449+ // On the first batch there is no prior watermark. Use the earliest
450+ // sample as the closure baseline so a batch spanning multiple windows
451+ // can close windows older than that sample after all samples are routed.
452+ let closure_previous_wm = if previous_wm == i64:: MIN {
453+ batch_min_ts
454+ } else {
455+ previous_wm
456+ } ;
444457
445458 let mut emit_batch: Vec < ( PrecomputedOutput , Box < dyn AggregateCore > ) > = Vec :: new ( ) ;
446459
@@ -460,7 +473,7 @@ impl Worker {
460473
461474 // Check if pane was already evicted (late data for a closed window)
462475 if !state. active_panes . contains_key ( & pane_start)
463- && current_wm >= pane_start + state. window_manager . window_size_ms ( )
476+ && previous_wm >= pane_start + state. window_manager . window_size_ms ( )
464477 {
465478 let window_start = pane_start;
466479 let window_end = pane_start + state. window_manager . window_size_ms ( ) ;
@@ -523,7 +536,9 @@ impl Worker {
523536 }
524537
525538 // Check for closed windows
526- let closed = state. window_manager . closed_windows ( previous_wm, current_wm) ;
539+ let closed = state
540+ . window_manager
541+ . closed_windows ( closure_previous_wm, current_wm) ;
527542
528543 for window_start in & closed {
529544 let ( _, window_end) = state. window_manager . window_bounds ( * window_start) ;
@@ -2529,6 +2544,61 @@ mod tests {
25292544 ) ;
25302545 }
25312546
2547+ #[ test]
2548+ fn test_delta_set_aggregator_keeps_on_time_samples_in_first_multi_window_batch ( ) {
2549+ let config = make_agg_config_full (
2550+ 7 ,
2551+ "cpu" ,
2552+ AggregationType :: DeltaSetAggregator ,
2553+ "" ,
2554+ 10_000 ,
2555+ 0 ,
2556+ vec ! [ ] ,
2557+ vec ! [ "host" ] ,
2558+ ) ;
2559+ let mut agg_configs = HashMap :: new ( ) ;
2560+ agg_configs. insert ( 7 , config) ;
2561+
2562+ let sink = Arc :: new ( CapturingOutputSink :: new ( ) ) ;
2563+ let mut worker = make_worker (
2564+ arc_configs ( agg_configs) ,
2565+ sink. clone ( ) ,
2566+ false ,
2567+ 0 ,
2568+ LateDataPolicy :: ForwardToStore ,
2569+ ) ;
2570+
2571+ worker
2572+ . process_group_samples (
2573+ 7 ,
2574+ "" ,
2575+ vec ! [
2576+ ( "cpu{host=\" a\" }" . to_string( ) , 500 , 1.0 ) ,
2577+ ( "cpu{host=\" b\" }" . to_string( ) , 20_000 , 1.0 ) ,
2578+ ] ,
2579+ )
2580+ . unwrap ( ) ;
2581+ worker
2582+ . process_group_samples ( 7 , "" , vec ! [ ( "cpu{host=\" b\" }" . to_string( ) , 30_000 , 1.0 ) ] )
2583+ . unwrap ( ) ;
2584+
2585+ let first_window = sink
2586+ . drain ( )
2587+ . into_iter ( )
2588+ . find ( |( output, _) | output. start_timestamp == 0 )
2589+ . expect ( "first DeltaSetAggregator window should be emitted" ) ;
2590+ let first_delta = first_window
2591+ . 1
2592+ . as_any ( )
2593+ . downcast_ref :: < DeltaSetAggregatorAccumulator > ( )
2594+ . expect ( "first output should be DeltaSetAggregatorAccumulator" ) ;
2595+ let key_a = KeyByLabelValues :: new_with_labels ( vec ! [ "a" . to_string( ) ] ) ;
2596+ assert ! (
2597+ first_delta. added. contains( & key_a) ,
2598+ "on-time key in the first batch must not be treated as late"
2599+ ) ;
2600+ }
2601+
25322602 // -----------------------------------------------------------------------
25332603 // Test: worker from streaming_config YAML
25342604 // -----------------------------------------------------------------------
0 commit comments