Skip to main content

llm_optimizer_processor/window/
assigner.rs

1//! Window assigners
2//!
3//! This module provides different strategies for assigning events to windows
4//! based on their timestamps.
5
6use super::types::{Window, WindowBounds};
7use chrono::{DateTime, Duration, TimeZone, Utc};
8use std::fmt;
9
10/// Trait for assigning events to windows
11pub trait WindowAssigner: Send + Sync + fmt::Debug {
12    /// Assign a timestamp to one or more windows
13    fn assign_windows(&self, timestamp: DateTime<Utc>) -> Vec<Window>;
14
15    /// Get the maximum number of windows an event can be assigned to
16    fn max_windows_per_event(&self) -> usize {
17        1
18    }
19
20    /// Check if this is a session window assigner (requires merging)
21    fn is_session_window(&self) -> bool {
22        false
23    }
24}
25
26/// Tumbling window assigner
27///
28/// Creates fixed-size, non-overlapping windows. Each event is assigned to
29/// exactly one window based on its timestamp.
30///
31/// # Example
32/// ```text
33/// Window size: 5 seconds
34/// Event at timestamp 7 -> Window [5, 10)
35/// Event at timestamp 12 -> Window [10, 15)
36/// ```
37#[derive(Debug, Clone)]
38pub struct TumblingWindowAssigner {
39    /// Size of each window
40    size: Duration,
41    /// Optional offset for window alignment
42    offset: Duration,
43}
44
45impl TumblingWindowAssigner {
46    /// Create a new tumbling window assigner
47    pub fn new(size: Duration) -> Self {
48        assert!(size > Duration::zero(), "Window size must be positive");
49        Self {
50            size,
51            offset: Duration::zero(),
52        }
53    }
54
55    /// Create a tumbling window assigner with an offset
56    pub fn with_offset(mut self, offset: Duration) -> Self {
57        self.offset = offset;
58        self
59    }
60
61    /// Calculate the window start for a given timestamp
62    fn window_start(&self, timestamp: DateTime<Utc>) -> DateTime<Utc> {
63        let ts_millis = timestamp.timestamp_millis();
64        let offset_millis = self.offset.num_milliseconds();
65        let size_millis = self.size.num_milliseconds();
66
67        let aligned = ((ts_millis - offset_millis) / size_millis) * size_millis + offset_millis;
68
69        Utc.timestamp_millis_opt(aligned).unwrap()
70    }
71}
72
73impl WindowAssigner for TumblingWindowAssigner {
74    fn assign_windows(&self, timestamp: DateTime<Utc>) -> Vec<Window> {
75        let start = self.window_start(timestamp);
76        let end = start + self.size;
77        let bounds = WindowBounds::new(start, end);
78        vec![Window::new(bounds)]
79    }
80}
81
82/// Sliding window assigner
83///
84/// Creates fixed-size, overlapping windows. Each event may be assigned to
85/// multiple windows based on the slide interval.
86///
87/// # Example
88/// ```text
89/// Window size: 10 seconds, Slide: 5 seconds
90/// Event at timestamp 7 -> Windows [0, 10), [5, 15)
91/// Event at timestamp 12 -> Windows [5, 15), [10, 20)
92/// ```
93#[derive(Debug, Clone)]
94pub struct SlidingWindowAssigner {
95    /// Size of each window
96    size: Duration,
97    /// Slide interval between windows
98    slide: Duration,
99    /// Optional offset for window alignment
100    offset: Duration,
101}
102
103impl SlidingWindowAssigner {
104    /// Create a new sliding window assigner
105    pub fn new(size: Duration, slide: Duration) -> Self {
106        assert!(size > Duration::zero(), "Window size must be positive");
107        assert!(slide > Duration::zero(), "Slide must be positive");
108        assert!(slide <= size, "Slide must not be larger than window size");
109
110        Self {
111            size,
112            slide,
113            offset: Duration::zero(),
114        }
115    }
116
117    /// Create a sliding window assigner with an offset
118    pub fn with_offset(mut self, offset: Duration) -> Self {
119        self.offset = offset;
120        self
121    }
122
123    /// Calculate all window starts that contain the timestamp
124    fn window_starts(&self, timestamp: DateTime<Utc>) -> Vec<DateTime<Utc>> {
125        let ts_millis = timestamp.timestamp_millis();
126        let offset_millis = self.offset.num_milliseconds();
127        let size_millis = self.size.num_milliseconds();
128        let slide_millis = self.slide.num_milliseconds();
129
130        // Find the first window that could contain this timestamp
131        let first_start = ((ts_millis - offset_millis - size_millis + 1) / slide_millis) * slide_millis + offset_millis;
132
133        let mut starts = Vec::new();
134        let mut current_start = first_start;
135
136        // Find all windows that contain this timestamp
137        while current_start <= ts_millis - offset_millis {
138            let start_time = Utc.timestamp_millis_opt(current_start).unwrap();
139            let end_time = start_time + self.size;
140
141            if timestamp >= start_time && timestamp < end_time {
142                starts.push(start_time);
143            }
144
145            current_start += slide_millis;
146        }
147
148        starts
149    }
150}
151
152impl WindowAssigner for SlidingWindowAssigner {
153    fn assign_windows(&self, timestamp: DateTime<Utc>) -> Vec<Window> {
154        self.window_starts(timestamp)
155            .into_iter()
156            .map(|start| {
157                let end = start + self.size;
158                let bounds = WindowBounds::new(start, end);
159                Window::new(bounds)
160            })
161            .collect()
162    }
163
164    fn max_windows_per_event(&self) -> usize {
165        (self.size.num_milliseconds() / self.slide.num_milliseconds()) as usize
166    }
167}
168
169/// Session window assigner
170///
171/// Creates variable-size windows based on inactivity gaps. Events are grouped
172/// into the same window if they occur within the session gap of each other.
173/// Session windows require merging when new events arrive.
174///
175/// # Example
176/// ```text
177/// Session gap: 5 seconds
178/// Events at 0, 2, 10 -> Windows [0, 7), [10, 15)
179/// Event at 4 arrives -> Windows merge to [0, 9), [10, 15)
180/// ```
181#[derive(Debug, Clone)]
182pub struct SessionWindowAssigner {
183    /// Maximum gap between events in a session
184    gap: Duration,
185}
186
187impl SessionWindowAssigner {
188    /// Create a new session window assigner
189    pub fn new(gap: Duration) -> Self {
190        assert!(gap > Duration::zero(), "Session gap must be positive");
191        Self { gap }
192    }
193
194    /// Get the session gap
195    pub fn gap(&self) -> Duration {
196        self.gap
197    }
198}
199
200impl WindowAssigner for SessionWindowAssigner {
201    fn assign_windows(&self, timestamp: DateTime<Utc>) -> Vec<Window> {
202        // Session windows start at the event timestamp and end at timestamp + gap
203        let start = timestamp;
204        let end = timestamp + self.gap;
205        let bounds = WindowBounds::new(start, end);
206        vec![Window::new(bounds)]
207    }
208
209    fn is_session_window(&self) -> bool {
210        true
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use chrono::TimeZone;
218
219    fn create_timestamp(millis: i64) -> DateTime<Utc> {
220        Utc.timestamp_millis_opt(millis).unwrap()
221    }
222
223    #[test]
224    fn test_tumbling_window_assignment() {
225        let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000));
226
227        // Event at 500ms -> Window [0, 1000)
228        let windows = assigner.assign_windows(create_timestamp(500));
229        assert_eq!(windows.len(), 1);
230        assert_eq!(windows[0].bounds.start, create_timestamp(0));
231        assert_eq!(windows[0].bounds.end, create_timestamp(1000));
232
233        // Event at 1500ms -> Window [1000, 2000)
234        let windows = assigner.assign_windows(create_timestamp(1500));
235        assert_eq!(windows.len(), 1);
236        assert_eq!(windows[0].bounds.start, create_timestamp(1000));
237        assert_eq!(windows[0].bounds.end, create_timestamp(2000));
238
239        // Event exactly at window boundary -> Next window
240        let windows = assigner.assign_windows(create_timestamp(2000));
241        assert_eq!(windows.len(), 1);
242        assert_eq!(windows[0].bounds.start, create_timestamp(2000));
243        assert_eq!(windows[0].bounds.end, create_timestamp(3000));
244    }
245
246    #[test]
247    fn test_tumbling_window_with_offset() {
248        let assigner = TumblingWindowAssigner::new(Duration::milliseconds(1000))
249            .with_offset(Duration::milliseconds(200));
250
251        // Event at 500ms -> Window [200, 1200)
252        let windows = assigner.assign_windows(create_timestamp(500));
253        assert_eq!(windows.len(), 1);
254        assert_eq!(windows[0].bounds.start, create_timestamp(200));
255        assert_eq!(windows[0].bounds.end, create_timestamp(1200));
256    }
257
258    #[test]
259    fn test_sliding_window_assignment() {
260        let assigner = SlidingWindowAssigner::new(
261            Duration::milliseconds(1000),
262            Duration::milliseconds(500),
263        );
264
265        // Event at 700ms should be in windows [0, 1000) and [500, 1500)
266        let windows = assigner.assign_windows(create_timestamp(700));
267        assert_eq!(windows.len(), 2);
268        assert_eq!(windows[0].bounds.start, create_timestamp(0));
269        assert_eq!(windows[0].bounds.end, create_timestamp(1000));
270        assert_eq!(windows[1].bounds.start, create_timestamp(500));
271        assert_eq!(windows[1].bounds.end, create_timestamp(1500));
272
273        // Event at 1200ms should be in windows [500, 1500), [1000, 2000)
274        let windows = assigner.assign_windows(create_timestamp(1200));
275        assert_eq!(windows.len(), 2);
276        assert_eq!(windows[0].bounds.start, create_timestamp(500));
277        assert_eq!(windows[0].bounds.end, create_timestamp(1500));
278        assert_eq!(windows[1].bounds.start, create_timestamp(1000));
279        assert_eq!(windows[1].bounds.end, create_timestamp(2000));
280    }
281
282    #[test]
283    fn test_sliding_window_max_windows() {
284        let assigner = SlidingWindowAssigner::new(
285            Duration::milliseconds(1000),
286            Duration::milliseconds(500),
287        );
288        assert_eq!(assigner.max_windows_per_event(), 2);
289
290        let assigner = SlidingWindowAssigner::new(
291            Duration::milliseconds(1000),
292            Duration::milliseconds(250),
293        );
294        assert_eq!(assigner.max_windows_per_event(), 4);
295    }
296
297    #[test]
298    fn test_sliding_window_with_offset() {
299        let assigner = SlidingWindowAssigner::new(
300            Duration::milliseconds(1000),
301            Duration::milliseconds(500),
302        ).with_offset(Duration::milliseconds(100));
303
304        let windows = assigner.assign_windows(create_timestamp(700));
305        // Windows should be offset by 100ms
306        assert!(windows.iter().all(|w| w.bounds.start.timestamp_millis() % 500 == 100));
307    }
308
309    #[test]
310    fn test_session_window_assignment() {
311        let assigner = SessionWindowAssigner::new(Duration::milliseconds(5000));
312
313        // Event at 1000ms -> Window [1000, 6000)
314        let windows = assigner.assign_windows(create_timestamp(1000));
315        assert_eq!(windows.len(), 1);
316        assert_eq!(windows[0].bounds.start, create_timestamp(1000));
317        assert_eq!(windows[0].bounds.end, create_timestamp(6000));
318
319        // Event at 2000ms -> Window [2000, 7000)
320        let windows = assigner.assign_windows(create_timestamp(2000));
321        assert_eq!(windows.len(), 1);
322        assert_eq!(windows[0].bounds.start, create_timestamp(2000));
323        assert_eq!(windows[0].bounds.end, create_timestamp(7000));
324    }
325
326    #[test]
327    fn test_session_window_is_session() {
328        let assigner = SessionWindowAssigner::new(Duration::milliseconds(5000));
329        assert!(assigner.is_session_window());
330
331        let tumbling = TumblingWindowAssigner::new(Duration::milliseconds(1000));
332        assert!(!tumbling.is_session_window());
333    }
334
335    #[test]
336    #[should_panic(expected = "Window size must be positive")]
337    fn test_tumbling_invalid_size() {
338        TumblingWindowAssigner::new(Duration::zero());
339    }
340
341    #[test]
342    #[should_panic(expected = "Slide must not be larger than window size")]
343    fn test_sliding_invalid_slide() {
344        SlidingWindowAssigner::new(
345            Duration::milliseconds(1000),
346            Duration::milliseconds(2000),
347        );
348    }
349
350    #[test]
351    #[should_panic(expected = "Session gap must be positive")]
352    fn test_session_invalid_gap() {
353        SessionWindowAssigner::new(Duration::zero());
354    }
355}