Skip to main content

llm_optimizer_processor/window/
trigger.rs

1//! Window triggers
2//!
3//! This module provides different strategies for determining when windows
4//! should be evaluated and their results emitted.
5
6use super::types::Window;
7use chrono::{DateTime, Duration, Utc};
8use std::collections::HashMap;
9use std::fmt;
10
11/// Result of trigger evaluation
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum TriggerResult {
14    /// Continue processing, don't fire
15    Continue,
16    /// Fire the window and emit results
17    Fire,
18    /// Fire the window and purge all state
19    FireAndPurge,
20    /// Purge the window without firing
21    Purge,
22}
23
24/// Context provided to triggers for evaluation
25#[derive(Debug, Clone)]
26pub struct TriggerContext {
27    /// The window being evaluated
28    pub window: Window,
29    /// Current event timestamp (if triggered by event)
30    pub event_time: Option<DateTime<Utc>>,
31    /// Current processing time
32    pub processing_time: DateTime<Utc>,
33    /// Current watermark
34    pub watermark: Option<DateTime<Utc>>,
35    /// Number of elements in the window
36    pub element_count: usize,
37}
38
39/// Trait for window triggers
40pub trait WindowTrigger: Send + Sync + fmt::Debug {
41    /// Evaluate when an element is added to the window
42    fn on_element(&mut self, ctx: &TriggerContext) -> TriggerResult {
43        let _ = ctx;
44        TriggerResult::Continue
45    }
46
47    /// Evaluate when the watermark advances
48    fn on_watermark(&mut self, ctx: &TriggerContext) -> TriggerResult {
49        let _ = ctx;
50        TriggerResult::Continue
51    }
52
53    /// Evaluate on processing time timer
54    fn on_processing_time(&mut self, ctx: &TriggerContext) -> TriggerResult {
55        let _ = ctx;
56        TriggerResult::Continue
57    }
58
59    /// Clear any state associated with the window
60    fn clear(&mut self, window: &Window) {
61        let _ = window;
62    }
63}
64
65/// Trigger that fires when the watermark passes the end of the window
66///
67/// This is the standard trigger for event-time windows. It fires when
68/// the watermark (which represents the progress of event time) passes
69/// the end of the window, indicating that all events for that window
70/// have likely been received.
71#[derive(Debug, Clone)]
72pub struct OnWatermarkTrigger;
73
74impl OnWatermarkTrigger {
75    pub fn new() -> Self {
76        Self
77    }
78}
79
80impl Default for OnWatermarkTrigger {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86impl WindowTrigger for OnWatermarkTrigger {
87    fn on_watermark(&mut self, ctx: &TriggerContext) -> TriggerResult {
88        if let Some(watermark) = ctx.watermark {
89            if watermark >= ctx.window.bounds.end {
90                return TriggerResult::Fire;
91            }
92        }
93        TriggerResult::Continue
94    }
95}
96
97/// Trigger that fires after a specified processing time delay
98///
99/// This trigger fires when processing time reaches the window end
100/// plus a configured delay. Useful for tumbling and sliding windows
101/// based on processing time.
102#[derive(Debug, Clone)]
103pub struct ProcessingTimeTrigger {
104    /// Delay after window end before firing
105    delay: Duration,
106}
107
108impl ProcessingTimeTrigger {
109    pub fn new() -> Self {
110        Self {
111            delay: Duration::zero(),
112        }
113    }
114
115    pub fn with_delay(delay: Duration) -> Self {
116        Self { delay }
117    }
118}
119
120impl Default for ProcessingTimeTrigger {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126impl WindowTrigger for ProcessingTimeTrigger {
127    fn on_element(&mut self, ctx: &TriggerContext) -> TriggerResult {
128        let fire_time = ctx.window.bounds.end + self.delay;
129        if ctx.processing_time >= fire_time {
130            TriggerResult::Fire
131        } else {
132            TriggerResult::Continue
133        }
134    }
135
136    fn on_processing_time(&mut self, ctx: &TriggerContext) -> TriggerResult {
137        let fire_time = ctx.window.bounds.end + self.delay;
138        if ctx.processing_time >= fire_time {
139            TriggerResult::Fire
140        } else {
141            TriggerResult::Continue
142        }
143    }
144}
145
146/// Trigger that fires when a window contains a specified number of elements
147///
148/// This trigger maintains a count per window and fires when the count
149/// reaches the threshold. Useful for micro-batching and early window
150/// evaluation.
151#[derive(Debug)]
152pub struct CountTrigger {
153    /// Number of elements required to trigger
154    threshold: usize,
155    /// Count per window
156    counts: HashMap<String, usize>,
157}
158
159impl CountTrigger {
160    pub fn new(threshold: usize) -> Self {
161        assert!(threshold > 0, "Count threshold must be positive");
162        Self {
163            threshold,
164            counts: HashMap::new(),
165        }
166    }
167
168    fn increment_count(&mut self, window_id: &str) -> usize {
169        let count = self.counts.entry(window_id.to_string()).or_insert(0);
170        *count += 1;
171        *count
172    }
173}
174
175impl WindowTrigger for CountTrigger {
176    fn on_element(&mut self, ctx: &TriggerContext) -> TriggerResult {
177        let count = self.increment_count(&ctx.window.id);
178        if count >= self.threshold {
179            TriggerResult::Fire
180        } else {
181            TriggerResult::Continue
182        }
183    }
184
185    fn clear(&mut self, window: &Window) {
186        self.counts.remove(&window.id);
187    }
188}
189
190/// Composite trigger that combines multiple triggers with AND/OR logic
191#[derive(Debug)]
192pub struct CompositeTrigger {
193    triggers: Vec<Box<dyn WindowTrigger>>,
194    mode: CompositeMode,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub enum CompositeMode {
199    /// Fire when ANY trigger fires
200    Any,
201    /// Fire when ALL triggers fire
202    All,
203}
204
205impl CompositeTrigger {
206    pub fn any(triggers: Vec<Box<dyn WindowTrigger>>) -> Self {
207        assert!(!triggers.is_empty(), "Composite trigger must have at least one trigger");
208        Self {
209            triggers,
210            mode: CompositeMode::Any,
211        }
212    }
213
214    pub fn all(triggers: Vec<Box<dyn WindowTrigger>>) -> Self {
215        assert!(!triggers.is_empty(), "Composite trigger must have at least one trigger");
216        Self {
217            triggers,
218            mode: CompositeMode::All,
219        }
220    }
221
222    fn evaluate<F>(&mut self, ctx: &TriggerContext, f: F) -> TriggerResult
223    where
224        F: Fn(&mut Box<dyn WindowTrigger>, &TriggerContext) -> TriggerResult,
225    {
226        let mut results = Vec::new();
227
228        for trigger in &mut self.triggers {
229            let result = f(trigger, ctx);
230            results.push(result);
231        }
232
233        match self.mode {
234            CompositeMode::Any => {
235                // Fire if any trigger fires
236                if results.iter().any(|r| matches!(r, TriggerResult::Fire | TriggerResult::FireAndPurge)) {
237                    TriggerResult::Fire
238                } else {
239                    TriggerResult::Continue
240                }
241            }
242            CompositeMode::All => {
243                // Fire only if all triggers fire
244                if results.iter().all(|r| matches!(r, TriggerResult::Fire | TriggerResult::FireAndPurge)) {
245                    TriggerResult::Fire
246                } else {
247                    TriggerResult::Continue
248                }
249            }
250        }
251    }
252}
253
254impl WindowTrigger for CompositeTrigger {
255    fn on_element(&mut self, ctx: &TriggerContext) -> TriggerResult {
256        self.evaluate(ctx, |trigger, ctx| trigger.on_element(ctx))
257    }
258
259    fn on_watermark(&mut self, ctx: &TriggerContext) -> TriggerResult {
260        self.evaluate(ctx, |trigger, ctx| trigger.on_watermark(ctx))
261    }
262
263    fn on_processing_time(&mut self, ctx: &TriggerContext) -> TriggerResult {
264        self.evaluate(ctx, |trigger, ctx| trigger.on_processing_time(ctx))
265    }
266
267    fn clear(&mut self, window: &Window) {
268        for trigger in &mut self.triggers {
269            trigger.clear(window);
270        }
271    }
272}
273
274/// Trigger that fires on every element (for immediate evaluation)
275#[derive(Debug, Clone)]
276pub struct ImmediateTrigger;
277
278impl ImmediateTrigger {
279    pub fn new() -> Self {
280        Self
281    }
282}
283
284impl Default for ImmediateTrigger {
285    fn default() -> Self {
286        Self::new()
287    }
288}
289
290impl WindowTrigger for ImmediateTrigger {
291    fn on_element(&mut self, _ctx: &TriggerContext) -> TriggerResult {
292        TriggerResult::Fire
293    }
294}
295
296/// Trigger that never fires automatically (manual trigger only)
297#[derive(Debug, Clone)]
298pub struct NeverTrigger;
299
300impl NeverTrigger {
301    pub fn new() -> Self {
302        Self
303    }
304}
305
306impl Default for NeverTrigger {
307    fn default() -> Self {
308        Self::new()
309    }
310}
311
312impl WindowTrigger for NeverTrigger {
313    // All methods return Continue by default
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use crate::window::types::WindowBounds;
320    use chrono::TimeZone;
321
322    fn create_timestamp(millis: i64) -> DateTime<Utc> {
323        Utc.timestamp_millis_opt(millis).unwrap()
324    }
325
326    fn create_window(start_millis: i64, end_millis: i64) -> Window {
327        Window::new(WindowBounds::new(
328            create_timestamp(start_millis),
329            create_timestamp(end_millis),
330        ))
331    }
332
333    fn create_context(
334        window: Window,
335        watermark: Option<DateTime<Utc>>,
336        processing_time: DateTime<Utc>,
337        element_count: usize,
338    ) -> TriggerContext {
339        TriggerContext {
340            window,
341            event_time: None,
342            processing_time,
343            watermark,
344            element_count,
345        }
346    }
347
348    #[test]
349    fn test_watermark_trigger() {
350        let mut trigger = OnWatermarkTrigger::new();
351        let window = create_window(1000, 2000);
352
353        // Watermark before window end -> Continue
354        let ctx = create_context(
355            window.clone(),
356            Some(create_timestamp(1500)),
357            create_timestamp(3000),
358            5,
359        );
360        assert_eq!(trigger.on_watermark(&ctx), TriggerResult::Continue);
361
362        // Watermark at window end -> Fire
363        let ctx = create_context(
364            window.clone(),
365            Some(create_timestamp(2000)),
366            create_timestamp(3000),
367            5,
368        );
369        assert_eq!(trigger.on_watermark(&ctx), TriggerResult::Fire);
370
371        // Watermark past window end -> Fire
372        let ctx = create_context(
373            window.clone(),
374            Some(create_timestamp(2500)),
375            create_timestamp(3000),
376            5,
377        );
378        assert_eq!(trigger.on_watermark(&ctx), TriggerResult::Fire);
379
380        // No watermark -> Continue
381        let ctx = create_context(
382            window,
383            None,
384            create_timestamp(3000),
385            5,
386        );
387        assert_eq!(trigger.on_watermark(&ctx), TriggerResult::Continue);
388    }
389
390    #[test]
391    fn test_processing_time_trigger() {
392        let mut trigger = ProcessingTimeTrigger::new();
393        let window = create_window(1000, 2000);
394
395        // Processing time before window end -> Continue
396        let ctx = create_context(
397            window.clone(),
398            None,
399            create_timestamp(1500),
400            5,
401        );
402        assert_eq!(trigger.on_element(&ctx), TriggerResult::Continue);
403
404        // Processing time at window end -> Fire
405        let ctx = create_context(
406            window.clone(),
407            None,
408            create_timestamp(2000),
409            5,
410        );
411        assert_eq!(trigger.on_element(&ctx), TriggerResult::Fire);
412
413        // Processing time past window end -> Fire
414        let ctx = create_context(
415            window,
416            None,
417            create_timestamp(2500),
418            5,
419        );
420        assert_eq!(trigger.on_element(&ctx), TriggerResult::Fire);
421    }
422
423    #[test]
424    fn test_processing_time_trigger_with_delay() {
425        let mut trigger = ProcessingTimeTrigger::with_delay(Duration::milliseconds(1000));
426        let window = create_window(1000, 2000);
427
428        // Processing time before window end + delay -> Continue
429        let ctx = create_context(
430            window.clone(),
431            None,
432            create_timestamp(2500),
433            5,
434        );
435        assert_eq!(trigger.on_element(&ctx), TriggerResult::Continue);
436
437        // Processing time at window end + delay -> Fire
438        let ctx = create_context(
439            window.clone(),
440            None,
441            create_timestamp(3000),
442            5,
443        );
444        assert_eq!(trigger.on_element(&ctx), TriggerResult::Fire);
445    }
446
447    #[test]
448    fn test_count_trigger() {
449        let mut trigger = CountTrigger::new(3);
450        let window = create_window(1000, 2000);
451
452        // First element -> Continue
453        let ctx = create_context(
454            window.clone(),
455            None,
456            create_timestamp(1500),
457            1,
458        );
459        assert_eq!(trigger.on_element(&ctx), TriggerResult::Continue);
460
461        // Second element -> Continue
462        let ctx = create_context(
463            window.clone(),
464            None,
465            create_timestamp(1600),
466            2,
467        );
468        assert_eq!(trigger.on_element(&ctx), TriggerResult::Continue);
469
470        // Third element -> Fire
471        let ctx = create_context(
472            window.clone(),
473            None,
474            create_timestamp(1700),
475            3,
476        );
477        assert_eq!(trigger.on_element(&ctx), TriggerResult::Fire);
478
479        // Fourth element -> Fire (threshold already met)
480        let ctx = create_context(
481            window.clone(),
482            None,
483            create_timestamp(1800),
484            4,
485        );
486        assert_eq!(trigger.on_element(&ctx), TriggerResult::Fire);
487
488        // Clear and start over
489        trigger.clear(&window);
490        let ctx = create_context(
491            window.clone(),
492            None,
493            create_timestamp(1900),
494            1,
495        );
496        assert_eq!(trigger.on_element(&ctx), TriggerResult::Continue);
497    }
498
499    #[test]
500    fn test_composite_trigger_any() {
501        let count_trigger: Box<dyn WindowTrigger> = Box::new(CountTrigger::new(5));
502        let watermark_trigger: Box<dyn WindowTrigger> = Box::new(OnWatermarkTrigger::new());
503
504        let mut trigger = CompositeTrigger::any(vec![count_trigger, watermark_trigger]);
505        let window = create_window(1000, 2000);
506
507        // Neither trigger fires -> Continue
508        let ctx = create_context(
509            window.clone(),
510            Some(create_timestamp(1500)),
511            create_timestamp(1500),
512            1,
513        );
514        assert_eq!(trigger.on_watermark(&ctx), TriggerResult::Continue);
515
516        // Watermark trigger fires (watermark past window end) -> Fire
517        let ctx = create_context(
518            window.clone(),
519            Some(create_timestamp(2500)),
520            create_timestamp(1500),
521            2,
522        );
523        assert_eq!(trigger.on_watermark(&ctx), TriggerResult::Fire);
524    }
525
526    #[test]
527    fn test_composite_trigger_all() {
528        // Use two processing time triggers with different delays
529        let trigger1: Box<dyn WindowTrigger> = Box::new(ProcessingTimeTrigger::new());
530        let trigger2: Box<dyn WindowTrigger> = Box::new(ProcessingTimeTrigger::with_delay(Duration::milliseconds(500)));
531
532        let mut trigger = CompositeTrigger::all(vec![trigger1, trigger2]);
533        let window = create_window(1000, 2000);
534
535        // Processing time at window end -> Continue (only first trigger fires)
536        let ctx = create_context(
537            window.clone(),
538            None,
539            create_timestamp(2000),
540            1,
541        );
542        assert_eq!(trigger.on_processing_time(&ctx), TriggerResult::Continue);
543
544        // Processing time at window end + 500ms -> Fire (both triggers fire)
545        let ctx = create_context(
546            window.clone(),
547            None,
548            create_timestamp(2500),
549            1,
550        );
551        assert_eq!(trigger.on_processing_time(&ctx), TriggerResult::Fire);
552    }
553
554    #[test]
555    fn test_immediate_trigger() {
556        let mut trigger = ImmediateTrigger::new();
557        let window = create_window(1000, 2000);
558
559        let ctx = create_context(
560            window,
561            None,
562            create_timestamp(1500),
563            1,
564        );
565        assert_eq!(trigger.on_element(&ctx), TriggerResult::Fire);
566    }
567
568    #[test]
569    fn test_never_trigger() {
570        let mut trigger = NeverTrigger::new();
571        let window = create_window(1000, 2000);
572
573        let ctx = create_context(
574            window,
575            Some(create_timestamp(5000)),
576            create_timestamp(5000),
577            100,
578        );
579
580        assert_eq!(trigger.on_element(&ctx), TriggerResult::Continue);
581        assert_eq!(trigger.on_watermark(&ctx), TriggerResult::Continue);
582        assert_eq!(trigger.on_processing_time(&ctx), TriggerResult::Continue);
583    }
584
585    #[test]
586    #[should_panic(expected = "Count threshold must be positive")]
587    fn test_count_trigger_invalid_threshold() {
588        CountTrigger::new(0);
589    }
590
591    #[test]
592    #[should_panic(expected = "Composite trigger must have at least one trigger")]
593    fn test_composite_trigger_empty() {
594        CompositeTrigger::any(vec![]);
595    }
596}