llm_optimizer_processor/window/
assigner.rs1use super::types::{Window, WindowBounds};
7use chrono::{DateTime, Duration, TimeZone, Utc};
8use std::fmt;
9
10pub trait WindowAssigner: Send + Sync + fmt::Debug {
12 fn assign_windows(&self, timestamp: DateTime<Utc>) -> Vec<Window>;
14
15 fn max_windows_per_event(&self) -> usize {
17 1
18 }
19
20 fn is_session_window(&self) -> bool {
22 false
23 }
24}
25
26#[derive(Debug, Clone)]
38pub struct TumblingWindowAssigner {
39 size: Duration,
41 offset: Duration,
43}
44
45impl TumblingWindowAssigner {
46 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 pub fn with_offset(mut self, offset: Duration) -> Self {
57 self.offset = offset;
58 self
59 }
60
61 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#[derive(Debug, Clone)]
94pub struct SlidingWindowAssigner {
95 size: Duration,
97 slide: Duration,
99 offset: Duration,
101}
102
103impl SlidingWindowAssigner {
104 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 pub fn with_offset(mut self, offset: Duration) -> Self {
119 self.offset = offset;
120 self
121 }
122
123 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 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 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#[derive(Debug, Clone)]
182pub struct SessionWindowAssigner {
183 gap: Duration,
185}
186
187impl SessionWindowAssigner {
188 pub fn new(gap: Duration) -> Self {
190 assert!(gap > Duration::zero(), "Session gap must be positive");
191 Self { gap }
192 }
193
194 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 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 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 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 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 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 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 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 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 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 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}