llm_optimizer_processor/pipeline/
normalization_operator.rs1use crate::config::{AlignmentStrategy, FillStrategy, NormalizationConfig};
7use crate::error::Result;
8use crate::pipeline::operator::{OperatorContext, StreamOperator};
9use async_trait::async_trait;
10use chrono::{DateTime, Duration, Utc};
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, VecDeque};
13use std::sync::{Arc, RwLock};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct TimestampedEvent {
18 pub timestamp: DateTime<Utc>,
19 pub value: f64,
20}
21
22#[derive(Debug, Clone, Default)]
24pub struct NormalizationStats {
25 pub events_processed: u64,
27 pub events_aligned: u64,
29 pub intervals_filled: u64,
31 pub outliers_dropped: u64,
33 pub out_of_order_events: u64,
35 pub events_dropped: u64,
37}
38
39#[derive(Debug, Clone)]
64pub struct NormalizationOperator {
65 name: String,
66 config: NormalizationConfig,
67 buffer: Arc<RwLock<BTreeMap<i64, Vec<f64>>>>,
69 last_emitted: Arc<RwLock<Option<DateTime<Utc>>>>,
71 stats: Arc<RwLock<NormalizationStats>>,
73 recent_values: Arc<RwLock<VecDeque<f64>>>,
75}
76
77impl NormalizationOperator {
78 pub fn new<S: Into<String>>(name: S, config: NormalizationConfig) -> Self {
80 Self {
81 name: name.into(),
82 config,
83 buffer: Arc::new(RwLock::new(BTreeMap::new())),
84 last_emitted: Arc::new(RwLock::new(None)),
85 stats: Arc::new(RwLock::new(NormalizationStats::default())),
86 recent_values: Arc::new(RwLock::new(VecDeque::with_capacity(100))),
87 }
88 }
89
90 fn align_timestamp(&self, timestamp: DateTime<Utc>) -> DateTime<Utc> {
92 let interval_ms = self.config.interval.as_millis() as i64;
93 let timestamp_ms = timestamp.timestamp_millis();
94
95 let aligned_ms = match self.config.alignment {
96 AlignmentStrategy::Start => {
97 (timestamp_ms / interval_ms) * interval_ms
99 }
100 AlignmentStrategy::End => {
101 ((timestamp_ms + interval_ms - 1) / interval_ms) * interval_ms
103 }
104 AlignmentStrategy::Center => {
105 let base = (timestamp_ms / interval_ms) * interval_ms;
107 base + interval_ms / 2
108 }
109 AlignmentStrategy::Nearest => {
110 ((timestamp_ms + interval_ms / 2) / interval_ms) * interval_ms
112 }
113 };
114
115 DateTime::from_timestamp_millis(aligned_ms)
116 .unwrap_or(timestamp)
117 }
118
119 fn is_outlier(&self, value: f64) -> bool {
121 if !self.config.drop_outliers {
122 return false;
123 }
124
125 let recent = self.recent_values.read().unwrap();
126 if recent.len() < 3 {
127 return false;
129 }
130
131 let sum: f64 = recent.iter().sum();
133 let mean = sum / recent.len() as f64;
134
135 let variance: f64 = recent.iter()
136 .map(|&v| (v - mean).powi(2))
137 .sum::<f64>() / recent.len() as f64;
138 let std_dev = variance.sqrt();
139
140 if std_dev < 1e-10 {
141 return false;
143 }
144
145 let z_score = ((value - mean) / std_dev).abs();
147 z_score > self.config.outlier_threshold
148 }
149
150 fn update_recent_values(&self, value: f64) {
152 let mut recent = self.recent_values.write().unwrap();
153 recent.push_back(value);
154 if recent.len() > 100 {
155 recent.pop_front();
156 }
157 }
158
159 fn fill_missing_intervals(
161 &self,
162 start: DateTime<Utc>,
163 end: DateTime<Utc>,
164 prev_value: Option<f64>,
165 next_value: Option<f64>,
166 ) -> Vec<TimestampedEvent> {
167 let mut filled = Vec::new();
168 let interval = self.config.interval;
169
170 let mut current = start + interval;
171 while current < end {
172 let value = match self.config.fill_strategy {
173 FillStrategy::Zero => 0.0,
174 FillStrategy::ForwardFill => prev_value.unwrap_or(0.0),
175 FillStrategy::BackwardFill => next_value.unwrap_or(0.0),
176 FillStrategy::LinearInterpolation => {
177 if let (Some(prev), Some(next)) = (prev_value, next_value) {
178 let total_gap = (end.timestamp_millis() - start.timestamp_millis()) as f64;
180 let current_offset = (current.timestamp_millis() - start.timestamp_millis()) as f64;
181 let ratio = current_offset / total_gap;
182 prev + (next - prev) * ratio
183 } else {
184 prev_value.or(next_value).unwrap_or(0.0)
185 }
186 }
187 FillStrategy::Drop => {
188 current = current + interval;
190 continue;
191 }
192 FillStrategy::Constant => {
193 0.0
195 }
196 FillStrategy::Mean => {
197 if let (Some(prev), Some(next)) = (prev_value, next_value) {
198 (prev + next) / 2.0
199 } else {
200 prev_value.or(next_value).unwrap_or(0.0)
201 }
202 }
203 };
204
205 filled.push(TimestampedEvent {
206 timestamp: current,
207 value,
208 });
209
210 current = current + interval;
211 }
212
213 filled
214 }
215
216 pub fn stats(&self) -> NormalizationStats {
218 self.stats.read().unwrap().clone()
219 }
220
221 pub fn reset(&self) {
223 self.buffer.write().unwrap().clear();
224 *self.last_emitted.write().unwrap() = None;
225 *self.stats.write().unwrap() = NormalizationStats::default();
226 self.recent_values.write().unwrap().clear();
227 }
228}
229
230#[async_trait]
231impl StreamOperator for NormalizationOperator {
232 async fn process(
233 &self,
234 input: Vec<u8>,
235 _ctx: &OperatorContext,
236 ) -> Result<Vec<Vec<u8>>> {
237 if !self.config.enabled {
239 return Ok(vec![input]);
240 }
241
242 let event: TimestampedEvent = bincode::deserialize(&input)?;
244
245 {
247 let mut stats = self.stats.write().unwrap();
248 stats.events_processed += 1;
249 }
250
251 if self.is_outlier(event.value) {
253 let mut stats = self.stats.write().unwrap();
254 stats.outliers_dropped += 1;
255 return Ok(vec![]); }
257
258 self.update_recent_values(event.value);
260
261 let aligned_timestamp = self.align_timestamp(event.timestamp);
263 let aligned_ms = aligned_timestamp.timestamp_millis();
264
265 {
267 let mut buffer = self.buffer.write().unwrap();
268 buffer.entry(aligned_ms)
269 .or_insert_with(Vec::new)
270 .push(event.value);
271
272 if buffer.len() > self.config.buffer_size {
274 if let Some(&oldest_key) = buffer.keys().next() {
276 buffer.remove(&oldest_key);
277 let mut stats = self.stats.write().unwrap();
278 stats.events_dropped += 1;
279 }
280 }
281 }
282
283 let mut output = Vec::new();
285 let last_emitted = self.last_emitted.read().unwrap().clone();
286
287 let emit_threshold = aligned_timestamp - self.config.interval;
290
291 let events_to_emit: Vec<(DateTime<Utc>, f64)> = {
292 let buffer = self.buffer.read().unwrap();
293 buffer.iter()
294 .filter_map(|(&ts_ms, values)| {
295 let ts = DateTime::from_timestamp_millis(ts_ms)?;
296 if ts <= emit_threshold {
297 let avg = values.iter().sum::<f64>() / values.len() as f64;
299 Some((ts, avg))
300 } else {
301 None
302 }
303 })
304 .collect()
305 };
306
307 {
309 let mut buffer = self.buffer.write().unwrap();
310 for (ts, _) in &events_to_emit {
311 buffer.remove(&ts.timestamp_millis());
312 }
313 }
314
315 for (ts, value) in events_to_emit {
317 if let Some(last) = last_emitted {
319 let gap = ts.signed_duration_since(last);
320 let interval_chrono = chrono::Duration::from_std(self.config.interval)
321 .unwrap_or(chrono::Duration::seconds(60));
322 if gap > interval_chrono {
323 let filled = self.fill_missing_intervals(
325 last,
326 ts,
327 None, Some(value),
329 );
330
331 let mut stats = self.stats.write().unwrap();
332 stats.intervals_filled += filled.len() as u64;
333
334 for filled_event in filled {
335 let serialized = bincode::serialize(&filled_event)?;
336 output.push(serialized);
337 }
338 }
339 }
340
341 let aligned_event = TimestampedEvent {
343 timestamp: ts,
344 value,
345 };
346
347 let serialized = bincode::serialize(&aligned_event)?;
348 output.push(serialized);
349
350 *self.last_emitted.write().unwrap() = Some(ts);
352
353 let mut stats = self.stats.write().unwrap();
354 stats.events_aligned += 1;
355 }
356
357 Ok(output)
358 }
359
360 fn name(&self) -> &str {
361 &self.name
362 }
363
364 async fn open(&mut self) -> Result<()> {
365 self.reset();
366 Ok(())
367 }
368
369 async fn close(&mut self) -> Result<()> {
370 self.buffer.write().unwrap().clear();
372 Ok(())
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use crate::watermark::Watermark;
380
381 #[tokio::test]
382 async fn test_normalization_operator_disabled() {
383 let config = NormalizationConfig::default(); let operator = NormalizationOperator::new("test", config);
385 let ctx = OperatorContext::new(Watermark::min(), 0);
386
387 let event = TimestampedEvent {
388 timestamp: Utc::now(),
389 value: 42.0,
390 };
391
392 let input = bincode::serialize(&event).unwrap();
393 let output = operator.process(input.clone(), &ctx).await.unwrap();
394
395 assert_eq!(output.len(), 1);
397 assert_eq!(output[0], input);
398 }
399
400 #[tokio::test]
401 async fn test_normalization_operator_alignment() {
402 let config = NormalizationConfig::new()
403 .enabled()
404 .with_interval(Duration::from_secs(5).to_std().unwrap())
405 .with_alignment(AlignmentStrategy::Start);
406
407 let operator = NormalizationOperator::new("test", config);
408
409 let ts = DateTime::from_timestamp(1234567892, 0).unwrap(); let aligned = operator.align_timestamp(ts);
412
413 assert_eq!(aligned.timestamp() % 5, 0);
415 }
416
417 #[tokio::test]
418 async fn test_normalization_operator_stats() {
419 let config = NormalizationConfig::new()
420 .enabled()
421 .with_interval(Duration::from_secs(1).to_std().unwrap());
422
423 let operator = NormalizationOperator::new("test", config);
424 let ctx = OperatorContext::new(Watermark::min(), 0);
425
426 let event = TimestampedEvent {
427 timestamp: Utc::now(),
428 value: 42.0,
429 };
430
431 let input = bincode::serialize(&event).unwrap();
432 operator.process(input, &ctx).await.unwrap();
433
434 let stats = operator.stats();
435 assert_eq!(stats.events_processed, 1);
436 }
437
438 #[tokio::test]
439 async fn test_normalization_operator_outlier_detection() {
440 let config = NormalizationConfig::new()
441 .enabled()
442 .with_interval(Duration::from_secs(1).to_std().unwrap())
443 .with_outlier_detection(2.0);
444
445 let operator = NormalizationOperator::new("test", config);
446 let ctx = OperatorContext::new(Watermark::min(), 0);
447
448 for i in 1..=10 {
450 let event = TimestampedEvent {
451 timestamp: Utc::now(),
452 value: 10.0 + i as f64,
453 };
454 let input = bincode::serialize(&event).unwrap();
455 operator.process(input, &ctx).await.unwrap();
456 }
457
458 let outlier = TimestampedEvent {
460 timestamp: Utc::now(),
461 value: 1000.0,
462 };
463 let input = bincode::serialize(&outlier).unwrap();
464 operator.process(input, &ctx).await.unwrap();
465
466 let stats = operator.stats();
467 assert!(stats.outliers_dropped > 0);
468 }
469
470 #[test]
471 fn test_alignment_strategies() {
472 let timestamp = DateTime::from_timestamp(1234567892, 0).unwrap();
473
474 let config = NormalizationConfig::new()
476 .with_interval(Duration::from_secs(5).to_std().unwrap())
477 .with_alignment(AlignmentStrategy::Start);
478 let operator = NormalizationOperator::new("test", config);
479 let aligned = operator.align_timestamp(timestamp);
480 assert_eq!(aligned.timestamp() % 5, 0);
481 assert!(aligned <= timestamp);
482
483 let config = NormalizationConfig::new()
485 .with_interval(Duration::from_secs(5).to_std().unwrap())
486 .with_alignment(AlignmentStrategy::End);
487 let operator = NormalizationOperator::new("test", config);
488 let aligned = operator.align_timestamp(timestamp);
489 assert_eq!(aligned.timestamp() % 5, 0);
490 assert!(aligned >= timestamp);
491 }
492
493 #[test]
494 fn test_normalization_operator_reset() {
495 let config = NormalizationConfig::new().enabled();
496 let operator = NormalizationOperator::new("test", config);
497
498 operator.update_recent_values(42.0);
500
501 operator.reset();
503
504 let stats = operator.stats();
505 assert_eq!(stats.events_processed, 0);
506 }
507}