Skip to main content

oxirs_stream/watermark/
late_handler.rs

1//! Late event handler with drop / re-assign / side-output policies.
2//!
3//! This file is the dedicated home for the [`LateDataHandler`] type and its
4//! related enums.  The original implementation lives in the parent module and is
5//! re-exported through this module so callers can import a stable path:
6//!
7//! ```rust
8//! use oxirs_stream::watermark::late_handler::{LateDataHandler, LateDataPolicy};
9//! ```
10//!
11//! No duplication: [`super::LateDataHandler`], [`super::LateDataPolicy`], and
12//! [`super::LateDataDecision`] are simply re-exported.  Allowed-lateness
13//! policies, drop counters, and side-output routing all live in the parent
14//! module.
15//!
16//! Additional helpers in this module:
17//!
18//! * [`AllowedLatenessTracker`] — maintains a per-window allowed-lateness
19//!   budget and answers "should we keep the window open for this late event?".
20//! * [`SideOutputRouter`] — accumulates events tagged for side-output channels
21//!   so callers can drain them per channel.
22
23use std::collections::HashMap;
24
25pub use super::{LateDataDecision, LateDataHandler, LateDataPolicy};
26
27// ─── AllowedLatenessTracker ──────────────────────────────────────────────────
28
29/// Tracks per-window allowed-lateness budgets.
30///
31/// A window is *closed* once `now ≥ window_end + allowed_lateness`.  Until
32/// then late events may still be re-assigned to it.
33#[derive(Debug, Default)]
34pub struct AllowedLatenessTracker {
35    /// Map from window-id → (window_end_ms, allowed_lateness_ms).
36    windows: HashMap<String, (i64, i64)>,
37}
38
39impl AllowedLatenessTracker {
40    /// Create an empty tracker.
41    pub fn new() -> Self {
42        Self {
43            windows: HashMap::new(),
44        }
45    }
46
47    /// Register a window.
48    pub fn register(
49        &mut self,
50        window_id: impl Into<String>,
51        window_end_ms: i64,
52        allowed_lateness_ms: i64,
53    ) {
54        self.windows
55            .insert(window_id.into(), (window_end_ms, allowed_lateness_ms));
56    }
57
58    /// Returns `true` if the window is still accepting late events at `now_ms`.
59    pub fn is_open(&self, window_id: &str, now_ms: i64) -> bool {
60        match self.windows.get(window_id) {
61            None => false,
62            Some(&(end_ms, lateness_ms)) => now_ms < end_ms.saturating_add(lateness_ms),
63        }
64    }
65
66    /// Drop windows whose allowed-lateness budget has expired.
67    /// Returns the IDs of evicted windows.
68    pub fn evict_closed(&mut self, now_ms: i64) -> Vec<String> {
69        let mut evicted = Vec::new();
70        self.windows.retain(|id, &mut (end_ms, lateness_ms)| {
71            let still_open = now_ms < end_ms.saturating_add(lateness_ms);
72            if !still_open {
73                evicted.push(id.clone());
74            }
75            still_open
76        });
77        evicted
78    }
79
80    /// Number of tracked windows.
81    pub fn len(&self) -> usize {
82        self.windows.len()
83    }
84
85    /// True iff no windows are tracked.
86    pub fn is_empty(&self) -> bool {
87        self.windows.is_empty()
88    }
89}
90
91// ─── SideOutputRouter ────────────────────────────────────────────────────────
92
93/// Per-channel buffer for events routed via [`LateDataPolicy::SideOutput`].
94#[derive(Debug, Default)]
95pub struct SideOutputRouter<E> {
96    channels: HashMap<String, Vec<E>>,
97}
98
99impl<E> SideOutputRouter<E> {
100    /// Create an empty router.
101    pub fn new() -> Self {
102        Self {
103            channels: HashMap::new(),
104        }
105    }
106
107    /// Append `event` to the named channel buffer.
108    pub fn push(&mut self, channel: &str, event: E) {
109        self.channels
110            .entry(channel.to_string())
111            .or_default()
112            .push(event);
113    }
114
115    /// Drain (and return) all events from the given channel.
116    pub fn drain(&mut self, channel: &str) -> Vec<E> {
117        self.channels.remove(channel).unwrap_or_default()
118    }
119
120    /// Number of events buffered on `channel`.
121    pub fn len(&self, channel: &str) -> usize {
122        self.channels.get(channel).map(|v| v.len()).unwrap_or(0)
123    }
124
125    /// True iff the named channel has no buffered events.
126    pub fn is_empty(&self, channel: &str) -> bool {
127        self.channels
128            .get(channel)
129            .map(|v| v.is_empty())
130            .unwrap_or(true)
131    }
132
133    /// All channel names known to this router.
134    pub fn channels(&self) -> impl Iterator<Item = &String> {
135        self.channels.keys()
136    }
137}
138
139// ─── Tests ───────────────────────────────────────────────────────────────────
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn allowed_lateness_open_until_budget_exhausted() {
147        let mut t = AllowedLatenessTracker::new();
148        t.register("w1", 1_000, 500);
149        assert!(t.is_open("w1", 1_400));
150        assert!(!t.is_open("w1", 1_500));
151        assert!(!t.is_open("w1", 2_000));
152    }
153
154    #[test]
155    fn allowed_lateness_evict_closed() {
156        let mut t = AllowedLatenessTracker::new();
157        t.register("a", 100, 100);
158        t.register("b", 1_000, 100);
159        let evicted = t.evict_closed(500);
160        assert!(evicted.contains(&"a".to_string()));
161        assert!(!evicted.contains(&"b".to_string()));
162        assert_eq!(t.len(), 1);
163    }
164
165    #[test]
166    fn side_output_router_pushes_and_drains() {
167        let mut router: SideOutputRouter<i32> = SideOutputRouter::new();
168        router.push("late", 1);
169        router.push("late", 2);
170        router.push("dlq", 7);
171        assert_eq!(router.len("late"), 2);
172        assert_eq!(router.len("dlq"), 1);
173        let drained = router.drain("late");
174        assert_eq!(drained, vec![1, 2]);
175        assert!(router.is_empty("late"));
176        assert_eq!(router.len("dlq"), 1);
177    }
178
179    #[test]
180    fn re_export_late_data_handler_is_visible() {
181        let mut h = LateDataHandler::new(LateDataPolicy::Drop);
182        let d = h.handle(10, 50);
183        assert_eq!(d, LateDataDecision::Drop);
184    }
185}