1use super::types::Window;
7use chrono::{DateTime, Duration, Utc};
8use std::collections::HashMap;
9use std::fmt;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum TriggerResult {
14 Continue,
16 Fire,
18 FireAndPurge,
20 Purge,
22}
23
24#[derive(Debug, Clone)]
26pub struct TriggerContext {
27 pub window: Window,
29 pub event_time: Option<DateTime<Utc>>,
31 pub processing_time: DateTime<Utc>,
33 pub watermark: Option<DateTime<Utc>>,
35 pub element_count: usize,
37}
38
39pub trait WindowTrigger: Send + Sync + fmt::Debug {
41 fn on_element(&mut self, ctx: &TriggerContext) -> TriggerResult {
43 let _ = ctx;
44 TriggerResult::Continue
45 }
46
47 fn on_watermark(&mut self, ctx: &TriggerContext) -> TriggerResult {
49 let _ = ctx;
50 TriggerResult::Continue
51 }
52
53 fn on_processing_time(&mut self, ctx: &TriggerContext) -> TriggerResult {
55 let _ = ctx;
56 TriggerResult::Continue
57 }
58
59 fn clear(&mut self, window: &Window) {
61 let _ = window;
62 }
63}
64
65#[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#[derive(Debug, Clone)]
103pub struct ProcessingTimeTrigger {
104 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#[derive(Debug)]
152pub struct CountTrigger {
153 threshold: usize,
155 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#[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 Any,
201 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 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 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#[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#[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 }
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}