1use crate::error::{ProcessorError, Result};
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9use std::time::Duration;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ProcessorConfig {
14 pub window: WindowConfig,
16
17 pub watermark: WatermarkConfig,
19
20 pub state: StateConfig,
22
23 pub aggregation: AggregationConfig,
25
26 #[serde(default)]
28 pub deduplication: DeduplicationConfig,
29
30 #[serde(default)]
32 pub normalization: NormalizationConfig,
33
34 #[serde(default = "default_parallelism")]
36 pub parallelism: usize,
37
38 #[serde(default = "default_buffer_size")]
40 pub buffer_size: usize,
41
42 #[serde(default = "default_true")]
44 pub metrics_enabled: bool,
45}
46
47impl Default for ProcessorConfig {
48 fn default() -> Self {
49 Self {
50 window: WindowConfig::default(),
51 watermark: WatermarkConfig::default(),
52 state: StateConfig::default(),
53 aggregation: AggregationConfig::default(),
54 deduplication: DeduplicationConfig::default(),
55 normalization: NormalizationConfig::default(),
56 parallelism: default_parallelism(),
57 buffer_size: default_buffer_size(),
58 metrics_enabled: true,
59 }
60 }
61}
62
63impl ProcessorConfig {
64 pub fn validate(&self) -> Result<()> {
66 self.window.validate()?;
67 self.watermark.validate()?;
68 self.state.validate()?;
69 self.aggregation.validate()?;
70 self.deduplication.validate()?;
71 self.normalization.validate()?;
72
73 if self.parallelism == 0 {
74 return Err(ProcessorError::Configuration {
75 source: "parallelism must be greater than 0".into(),
76 });
77 }
78
79 if self.buffer_size == 0 {
80 return Err(ProcessorError::Configuration {
81 source: "buffer_size must be greater than 0".into(),
82 });
83 }
84
85 Ok(())
86 }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct WindowConfig {
92 pub window_type: WindowType,
94
95 pub size_ms: Option<u64>,
97
98 pub slide_ms: Option<u64>,
100
101 pub gap_ms: Option<u64>,
103
104 #[serde(default = "default_allowed_lateness")]
106 pub allowed_lateness_ms: u64,
107
108 #[serde(default)]
110 pub drop_late_events: bool,
111}
112
113#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
115#[serde(rename_all = "snake_case")]
116pub enum WindowType {
117 Tumbling,
119 Sliding,
121 Session,
123 Count,
125 Global,
127}
128
129impl Default for WindowConfig {
130 fn default() -> Self {
131 Self {
132 window_type: WindowType::Tumbling,
133 size_ms: Some(60_000), slide_ms: None,
135 gap_ms: None,
136 allowed_lateness_ms: default_allowed_lateness(),
137 drop_late_events: false,
138 }
139 }
140}
141
142impl WindowConfig {
143 pub fn tumbling(size_ms: u64) -> Self {
145 Self {
146 window_type: WindowType::Tumbling,
147 size_ms: Some(size_ms),
148 slide_ms: None,
149 gap_ms: None,
150 ..Default::default()
151 }
152 }
153
154 pub fn sliding(size_ms: u64, slide_ms: u64) -> Self {
156 Self {
157 window_type: WindowType::Sliding,
158 size_ms: Some(size_ms),
159 slide_ms: Some(slide_ms),
160 gap_ms: None,
161 ..Default::default()
162 }
163 }
164
165 pub fn session(gap_ms: u64) -> Self {
167 Self {
168 window_type: WindowType::Session,
169 size_ms: None,
170 slide_ms: None,
171 gap_ms: Some(gap_ms),
172 ..Default::default()
173 }
174 }
175
176 pub fn validate(&self) -> Result<()> {
178 match self.window_type {
179 WindowType::Tumbling => {
180 if let Some(size) = self.size_ms {
181 if size == 0 {
182 return Err(ProcessorError::Configuration {
183 source: "tumbling window size must be greater than 0".into(),
184 });
185 }
186 } else {
187 return Err(ProcessorError::Configuration {
188 source: "tumbling window requires size_ms".into(),
189 });
190 }
191 }
192 WindowType::Sliding => {
193 let size = self.size_ms.ok_or_else(|| ProcessorError::Configuration {
194 source: "sliding window requires size_ms".into(),
195 })?;
196 let slide = self.slide_ms.ok_or_else(|| ProcessorError::Configuration {
197 source: "sliding window requires slide_ms".into(),
198 })?;
199
200 if size == 0 {
201 return Err(ProcessorError::Configuration {
202 source: "sliding window size must be greater than 0".into(),
203 });
204 }
205 if slide == 0 {
206 return Err(ProcessorError::Configuration {
207 source: "sliding window slide must be greater than 0".into(),
208 });
209 }
210 if slide > size {
211 return Err(ProcessorError::Configuration {
212 source: "sliding window slide must be less than or equal to size".into(),
213 });
214 }
215 }
216 WindowType::Session => {
217 if let Some(gap) = self.gap_ms {
218 if gap == 0 {
219 return Err(ProcessorError::Configuration {
220 source: "session window gap must be greater than 0".into(),
221 });
222 }
223 } else {
224 return Err(ProcessorError::Configuration {
225 source: "session window requires gap_ms".into(),
226 });
227 }
228 }
229 WindowType::Count | WindowType::Global => {
230 }
232 }
233
234 Ok(())
235 }
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct WatermarkConfig {
241 pub strategy: WatermarkStrategy,
243
244 #[serde(default = "default_max_delay")]
246 pub max_delay_ms: u64,
247
248 #[serde(default = "default_idle_timeout")]
250 pub idle_timeout_ms: u64,
251
252 #[serde(default = "default_update_interval")]
254 pub update_interval_ms: u64,
255
256 #[serde(default = "default_true")]
258 pub allow_clock_skew: bool,
259
260 #[serde(default = "default_max_clock_skew")]
262 pub max_clock_skew_ms: u64,
263}
264
265#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
267#[serde(rename_all = "snake_case")]
268pub enum WatermarkStrategy {
269 Periodic,
271 BoundedOutOfOrderness,
273 Ascending,
275 Manual,
277}
278
279impl Default for WatermarkConfig {
280 fn default() -> Self {
281 Self {
282 strategy: WatermarkStrategy::BoundedOutOfOrderness,
283 max_delay_ms: default_max_delay(),
284 idle_timeout_ms: default_idle_timeout(),
285 update_interval_ms: default_update_interval(),
286 allow_clock_skew: true,
287 max_clock_skew_ms: default_max_clock_skew(),
288 }
289 }
290}
291
292impl WatermarkConfig {
293 pub fn validate(&self) -> Result<()> {
295 if self.update_interval_ms == 0 {
296 return Err(ProcessorError::Configuration {
297 source: "watermark update_interval_ms must be greater than 0".into(),
298 });
299 }
300
301 if self.idle_timeout_ms == 0 {
302 return Err(ProcessorError::Configuration {
303 source: "watermark idle_timeout_ms must be greater than 0".into(),
304 });
305 }
306
307 Ok(())
308 }
309
310 pub fn max_delay(&self) -> Duration {
312 Duration::from_millis(self.max_delay_ms)
313 }
314
315 pub fn idle_timeout(&self) -> Duration {
317 Duration::from_millis(self.idle_timeout_ms)
318 }
319
320 pub fn update_interval(&self) -> Duration {
322 Duration::from_millis(self.update_interval_ms)
323 }
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct StateConfig {
329 pub backend: StateBackend,
331
332 pub storage_path: Option<PathBuf>,
334
335 #[serde(default = "default_checkpoint_interval")]
337 pub checkpoint_interval_ms: u64,
338
339 #[serde(default = "default_true")]
341 pub incremental_checkpoints: bool,
342
343 #[serde(default = "default_checkpoint_retention")]
345 pub checkpoint_retention: usize,
346
347 pub ttl_ms: Option<u64>,
349
350 pub max_size_bytes: Option<u64>,
352
353 #[serde(default)]
355 pub compression_enabled: bool,
356}
357
358#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
360#[serde(rename_all = "snake_case")]
361pub enum StateBackend {
362 Memory,
364 FileSystem,
366 Redis,
368 PostgreSQL,
370}
371
372impl Default for StateConfig {
373 fn default() -> Self {
374 Self {
375 backend: StateBackend::Memory,
376 storage_path: None,
377 checkpoint_interval_ms: default_checkpoint_interval(),
378 incremental_checkpoints: true,
379 checkpoint_retention: default_checkpoint_retention(),
380 ttl_ms: None,
381 max_size_bytes: None,
382 compression_enabled: false,
383 }
384 }
385}
386
387impl StateConfig {
388 pub fn validate(&self) -> Result<()> {
390 if self.checkpoint_interval_ms == 0 {
391 return Err(ProcessorError::Configuration {
392 source: "checkpoint_interval_ms must be greater than 0".into(),
393 });
394 }
395
396 if self.checkpoint_retention == 0 {
397 return Err(ProcessorError::Configuration {
398 source: "checkpoint_retention must be greater than 0".into(),
399 });
400 }
401
402 if matches!(self.backend, StateBackend::FileSystem) && self.storage_path.is_none() {
403 return Err(ProcessorError::Configuration {
404 source: "file-based state backend requires storage_path".into(),
405 });
406 }
407
408 Ok(())
409 }
410
411 pub fn checkpoint_interval(&self) -> Duration {
413 Duration::from_millis(self.checkpoint_interval_ms)
414 }
415
416 pub fn ttl(&self) -> Option<Duration> {
418 self.ttl_ms.map(Duration::from_millis)
419 }
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct AggregationConfig {
425 pub enabled_aggregations: Vec<AggregationType>,
427
428 #[serde(default = "default_percentiles")]
430 pub percentiles: Vec<u8>,
431
432 pub histogram_buckets: Option<Vec<f64>>,
434
435 #[serde(default = "default_true")]
437 pub incremental: bool,
438
439 #[serde(default = "default_min_samples")]
441 pub min_samples: usize,
442
443 #[serde(default = "default_max_samples")]
445 pub max_samples: usize,
446
447 #[serde(default)]
449 pub remove_outliers: bool,
450
451 #[serde(default = "default_outlier_threshold")]
453 pub outlier_threshold: f64,
454}
455
456#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
458#[serde(rename_all = "snake_case")]
459pub enum AggregationType {
460 Sum,
462 Count,
464 Average,
466 Min,
468 Max,
470 Percentile,
472 Histogram,
474 StdDev,
476 Variance,
478}
479
480impl Default for AggregationConfig {
481 fn default() -> Self {
482 Self {
483 enabled_aggregations: vec![
484 AggregationType::Sum,
485 AggregationType::Count,
486 AggregationType::Average,
487 AggregationType::Min,
488 AggregationType::Max,
489 AggregationType::Percentile,
490 ],
491 percentiles: default_percentiles(),
492 histogram_buckets: None,
493 incremental: true,
494 min_samples: default_min_samples(),
495 max_samples: default_max_samples(),
496 remove_outliers: false,
497 outlier_threshold: default_outlier_threshold(),
498 }
499 }
500}
501
502impl AggregationConfig {
503 pub fn validate(&self) -> Result<()> {
505 if self.enabled_aggregations.is_empty() {
506 return Err(ProcessorError::Configuration {
507 source: "at least one aggregation type must be enabled".into(),
508 });
509 }
510
511 for &p in &self.percentiles {
512 if p > 100 {
513 return Err(ProcessorError::Configuration {
514 source: format!("invalid percentile: {}, must be 0-100", p).into(),
515 });
516 }
517 }
518
519 if let Some(buckets) = &self.histogram_buckets {
520 if buckets.is_empty() {
521 return Err(ProcessorError::Configuration {
522 source: "histogram_buckets cannot be empty".into(),
523 });
524 }
525
526 for i in 1..buckets.len() {
528 if buckets[i] <= buckets[i - 1] {
529 return Err(ProcessorError::Configuration {
530 source: "histogram_buckets must be in ascending order".into(),
531 });
532 }
533 }
534 }
535
536 if self.min_samples > self.max_samples {
537 return Err(ProcessorError::Configuration {
538 source: "min_samples cannot be greater than max_samples".into(),
539 });
540 }
541
542 if self.outlier_threshold <= 0.0 {
543 return Err(ProcessorError::Configuration {
544 source: "outlier_threshold must be positive".into(),
545 });
546 }
547
548 Ok(())
549 }
550
551 pub fn is_enabled(&self, agg_type: AggregationType) -> bool {
553 self.enabled_aggregations.contains(&agg_type)
554 }
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct NormalizationConfig {
580 #[serde(default)]
582 pub enabled: bool,
583
584 #[serde(
586 default = "default_normalization_interval",
587 serialize_with = "serialize_duration",
588 deserialize_with = "deserialize_duration"
589 )]
590 pub interval: Duration,
591
592 #[serde(default)]
594 pub fill_strategy: FillStrategy,
595
596 #[serde(default)]
598 pub alignment: AlignmentStrategy,
599
600 #[serde(default = "default_normalization_buffer_size")]
602 pub buffer_size: usize,
603
604 #[serde(
606 default = "default_interpolation_max_gap",
607 serialize_with = "serialize_duration",
608 deserialize_with = "deserialize_duration"
609 )]
610 pub interpolation_max_gap: Duration,
611
612 #[serde(default)]
614 pub drop_outliers: bool,
615
616 #[serde(default = "default_normalization_outlier_threshold")]
618 pub outlier_threshold: f64,
619
620 #[serde(default = "default_true")]
622 pub emit_metrics: bool,
623}
624
625impl Default for NormalizationConfig {
626 fn default() -> Self {
627 Self {
628 enabled: false,
629 interval: default_normalization_interval(),
630 fill_strategy: FillStrategy::default(),
631 alignment: AlignmentStrategy::default(),
632 buffer_size: default_normalization_buffer_size(),
633 interpolation_max_gap: default_interpolation_max_gap(),
634 drop_outliers: false,
635 outlier_threshold: default_normalization_outlier_threshold(),
636 emit_metrics: true,
637 }
638 }
639}
640
641impl NormalizationConfig {
642 pub fn new() -> Self {
644 Self::default()
645 }
646
647 pub fn enabled(mut self) -> Self {
649 self.enabled = true;
650 self
651 }
652
653 pub fn with_interval(mut self, interval: Duration) -> Self {
665 self.interval = interval;
666 self
667 }
668
669 pub fn with_fill_strategy(mut self, strategy: FillStrategy) -> Self {
671 self.fill_strategy = strategy;
672 self
673 }
674
675 pub fn with_alignment(mut self, alignment: AlignmentStrategy) -> Self {
677 self.alignment = alignment;
678 self
679 }
680
681 pub fn with_buffer_size(mut self, size: usize) -> Self {
683 self.buffer_size = size;
684 self
685 }
686
687 pub fn with_interpolation_max_gap(mut self, gap: Duration) -> Self {
689 self.interpolation_max_gap = gap;
690 self
691 }
692
693 pub fn with_outlier_detection(mut self, threshold: f64) -> Self {
695 self.drop_outliers = true;
696 self.outlier_threshold = threshold;
697 self
698 }
699
700 pub fn with_metrics(mut self, enabled: bool) -> Self {
702 self.emit_metrics = enabled;
703 self
704 }
705
706 pub fn validate(&self) -> Result<()> {
708 if !self.enabled {
709 return Ok(());
710 }
711
712 if self.interval.as_millis() == 0 {
713 return Err(ProcessorError::Configuration {
714 source: "normalization interval must be greater than 0".into(),
715 });
716 }
717
718 if self.buffer_size == 0 {
719 return Err(ProcessorError::Configuration {
720 source: "normalization buffer_size must be greater than 0".into(),
721 });
722 }
723
724 if self.interpolation_max_gap.as_millis() == 0 {
725 return Err(ProcessorError::Configuration {
726 source: "interpolation_max_gap must be greater than 0".into(),
727 });
728 }
729
730 if self.outlier_threshold <= 0.0 {
731 return Err(ProcessorError::Configuration {
732 source: "outlier_threshold must be positive".into(),
733 });
734 }
735
736 if self.interpolation_max_gap < self.interval {
738 return Err(ProcessorError::Configuration {
739 source: "interpolation_max_gap should be >= interval".into(),
740 });
741 }
742
743 Ok(())
744 }
745
746 pub fn interval_ms(&self) -> u64 {
748 self.interval.as_millis() as u64
749 }
750
751 pub fn interpolation_max_gap_ms(&self) -> u64 {
753 self.interpolation_max_gap.as_millis() as u64
754 }
755}
756
757#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
759#[serde(rename_all = "snake_case")]
760pub enum FillStrategy {
761 Zero,
763
764 ForwardFill,
766
767 BackwardFill,
769
770 LinearInterpolation,
772
773 Drop,
775
776 Constant,
778
779 Mean,
781}
782
783impl Default for FillStrategy {
784 fn default() -> Self {
785 Self::ForwardFill
786 }
787}
788
789#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
791#[serde(rename_all = "snake_case")]
792pub enum AlignmentStrategy {
793 Start,
796
797 End,
800
801 Center,
804
805 Nearest,
808}
809
810impl Default for AlignmentStrategy {
811 fn default() -> Self {
812 Self::Start
813 }
814}
815
816#[derive(Debug, Clone, Serialize, Deserialize)]
838pub struct DeduplicationConfig {
839 #[serde(default)]
841 pub enabled: bool,
842
843 #[serde(
846 default = "default_deduplication_ttl",
847 serialize_with = "serialize_duration",
848 deserialize_with = "deserialize_duration"
849 )]
850 pub ttl: Duration,
851
852 #[serde(default)]
854 pub strategy: DeduplicationStrategy,
855
856 #[serde(default = "default_redis_key_prefix")]
858 pub redis_key_prefix: String,
859
860 #[serde(default = "default_max_retries")]
862 pub max_retries: u32,
863
864 #[serde(
866 default = "default_cleanup_interval",
867 serialize_with = "serialize_duration",
868 deserialize_with = "deserialize_duration"
869 )]
870 pub cleanup_interval: Duration,
871}
872
873impl Default for DeduplicationConfig {
874 fn default() -> Self {
875 Self {
876 enabled: false,
877 ttl: default_deduplication_ttl(),
878 strategy: DeduplicationStrategy::default(),
879 redis_key_prefix: default_redis_key_prefix(),
880 max_retries: default_max_retries(),
881 cleanup_interval: default_cleanup_interval(),
882 }
883 }
884}
885
886impl DeduplicationConfig {
887 pub fn new() -> Self {
889 Self::default()
890 }
891
892 pub fn enabled(mut self) -> Self {
894 self.enabled = true;
895 self
896 }
897
898 pub fn with_ttl(mut self, ttl: Duration) -> Self {
900 self.ttl = ttl;
901 self
902 }
903
904 pub fn with_strategy(mut self, strategy: DeduplicationStrategy) -> Self {
906 self.strategy = strategy;
907 self
908 }
909
910 pub fn with_redis_key_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
912 self.redis_key_prefix = prefix.into();
913 self
914 }
915
916 pub fn with_max_retries(mut self, retries: u32) -> Self {
918 self.max_retries = retries;
919 self
920 }
921
922 pub fn with_cleanup_interval(mut self, interval: Duration) -> Self {
924 self.cleanup_interval = interval;
925 self
926 }
927
928 pub fn validate(&self) -> Result<()> {
930 if !self.enabled {
931 return Ok(());
932 }
933
934 if self.ttl.as_secs() == 0 {
935 return Err(ProcessorError::Configuration {
936 source: "deduplication ttl must be greater than 0".into(),
937 });
938 }
939
940 if self.redis_key_prefix.is_empty() {
941 return Err(ProcessorError::Configuration {
942 source: "deduplication redis_key_prefix cannot be empty".into(),
943 });
944 }
945
946 if self.cleanup_interval.as_secs() == 0 {
947 return Err(ProcessorError::Configuration {
948 source: "deduplication cleanup_interval must be greater than 0".into(),
949 });
950 }
951
952 Ok(())
953 }
954
955 pub fn ttl_ms(&self) -> u64 {
957 self.ttl.as_millis() as u64
958 }
959
960 pub fn cleanup_interval_ms(&self) -> u64 {
962 self.cleanup_interval.as_millis() as u64
963 }
964}
965
966#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
968#[serde(rename_all = "snake_case")]
969pub enum DeduplicationStrategy {
970 ContentHash,
973
974 EventId,
977
978 CompositeKey,
981}
982
983impl Default for DeduplicationStrategy {
984 fn default() -> Self {
985 Self::ContentHash
986 }
987}
988
989fn serialize_duration<S>(duration: &Duration, serializer: S) -> std::result::Result<S::Ok, S::Error>
991where
992 S: serde::Serializer,
993{
994 serializer.serialize_u64(duration.as_millis() as u64)
995}
996
997fn deserialize_duration<'de, D>(deserializer: D) -> std::result::Result<Duration, D::Error>
998where
999 D: serde::Deserializer<'de>,
1000{
1001 let millis = u64::deserialize(deserializer)?;
1002 Ok(Duration::from_millis(millis))
1003}
1004
1005fn default_parallelism() -> usize {
1007 num_cpus::get()
1008}
1009
1010fn default_buffer_size() -> usize {
1011 10_000
1012}
1013
1014fn default_allowed_lateness() -> u64 {
1015 5_000 }
1017
1018fn default_max_delay() -> u64 {
1019 10_000 }
1021
1022fn default_idle_timeout() -> u64 {
1023 60_000 }
1025
1026fn default_update_interval() -> u64 {
1027 1_000 }
1029
1030fn default_max_clock_skew() -> u64 {
1031 30_000 }
1033
1034fn default_checkpoint_interval() -> u64 {
1035 60_000 }
1037
1038fn default_checkpoint_retention() -> usize {
1039 3
1040}
1041
1042fn default_percentiles() -> Vec<u8> {
1043 vec![50, 95, 99]
1044}
1045
1046fn default_min_samples() -> usize {
1047 1
1048}
1049
1050fn default_max_samples() -> usize {
1051 100_000
1052}
1053
1054fn default_outlier_threshold() -> f64 {
1055 3.0
1056}
1057
1058fn default_true() -> bool {
1059 true
1060}
1061
1062fn default_deduplication_ttl() -> Duration {
1063 Duration::from_secs(3600) }
1065
1066fn default_redis_key_prefix() -> String {
1067 "dedup".to_string()
1068}
1069
1070fn default_max_retries() -> u32 {
1071 3
1072}
1073
1074fn default_cleanup_interval() -> Duration {
1075 Duration::from_secs(300) }
1077
1078fn default_normalization_interval() -> Duration {
1079 Duration::from_secs(1) }
1081
1082fn default_normalization_buffer_size() -> usize {
1083 1000
1084}
1085
1086fn default_interpolation_max_gap() -> Duration {
1087 Duration::from_secs(5) }
1089
1090fn default_normalization_outlier_threshold() -> f64 {
1091 3.0 }
1093
1094#[cfg(test)]
1095mod tests {
1096 use super::*;
1097
1098 #[test]
1099 fn test_default_processor_config() {
1100 let config = ProcessorConfig::default();
1101 assert!(config.validate().is_ok());
1102 }
1103
1104 #[test]
1105 fn test_tumbling_window_config() {
1106 let config = WindowConfig::tumbling(60_000);
1107 assert_eq!(config.window_type, WindowType::Tumbling);
1108 assert_eq!(config.size_ms, Some(60_000));
1109 assert!(config.validate().is_ok());
1110 }
1111
1112 #[test]
1113 fn test_sliding_window_config() {
1114 let config = WindowConfig::sliding(60_000, 30_000);
1115 assert_eq!(config.window_type, WindowType::Sliding);
1116 assert_eq!(config.size_ms, Some(60_000));
1117 assert_eq!(config.slide_ms, Some(30_000));
1118 assert!(config.validate().is_ok());
1119 }
1120
1121 #[test]
1122 fn test_invalid_sliding_window() {
1123 let mut config = WindowConfig::sliding(30_000, 60_000);
1124 assert!(config.validate().is_err());
1125 }
1126
1127 #[test]
1128 fn test_session_window_config() {
1129 let config = WindowConfig::session(5_000);
1130 assert_eq!(config.window_type, WindowType::Session);
1131 assert_eq!(config.gap_ms, Some(5_000));
1132 assert!(config.validate().is_ok());
1133 }
1134
1135 #[test]
1136 fn test_watermark_config_validation() {
1137 let config = WatermarkConfig::default();
1138 assert!(config.validate().is_ok());
1139
1140 let mut invalid_config = config.clone();
1141 invalid_config.update_interval_ms = 0;
1142 assert!(invalid_config.validate().is_err());
1143 }
1144
1145 #[test]
1146 fn test_state_config_validation() {
1147 let config = StateConfig::default();
1148 assert!(config.validate().is_ok());
1149
1150 let mut fs_config = config.clone();
1151 fs_config.backend = StateBackend::FileSystem;
1152 assert!(fs_config.validate().is_err()); fs_config.storage_path = Some(PathBuf::from("/tmp/state"));
1155 assert!(fs_config.validate().is_ok());
1156 }
1157
1158 #[test]
1159 fn test_aggregation_config_validation() {
1160 let config = AggregationConfig::default();
1161 assert!(config.validate().is_ok());
1162
1163 let mut invalid_config = config.clone();
1164 invalid_config.percentiles.push(101);
1165 assert!(invalid_config.validate().is_err());
1166 }
1167
1168 #[test]
1169 fn test_aggregation_is_enabled() {
1170 let config = AggregationConfig::default();
1171 assert!(config.is_enabled(AggregationType::Sum));
1172 assert!(config.is_enabled(AggregationType::Average));
1173 assert!(!config.is_enabled(AggregationType::StdDev));
1174 }
1175
1176 #[test]
1177 fn test_histogram_bucket_validation() {
1178 let mut config = AggregationConfig::default();
1179 config.histogram_buckets = Some(vec![1.0, 5.0, 10.0, 50.0, 100.0]);
1180 assert!(config.validate().is_ok());
1181
1182 config.histogram_buckets = Some(vec![1.0, 10.0, 5.0]);
1184 assert!(config.validate().is_err());
1185
1186 config.histogram_buckets = Some(vec![]);
1188 assert!(config.validate().is_err());
1189 }
1190
1191 #[test]
1192 fn test_duration_conversions() {
1193 let watermark_config = WatermarkConfig::default();
1194 assert_eq!(
1195 watermark_config.max_delay(),
1196 Duration::from_millis(default_max_delay())
1197 );
1198
1199 let state_config = StateConfig::default();
1200 assert_eq!(
1201 state_config.checkpoint_interval(),
1202 Duration::from_millis(default_checkpoint_interval())
1203 );
1204 }
1205
1206 #[test]
1207 fn test_default_deduplication_config() {
1208 let config = DeduplicationConfig::default();
1209 assert!(!config.enabled);
1210 assert_eq!(config.ttl, Duration::from_secs(3600));
1211 assert_eq!(config.strategy, DeduplicationStrategy::ContentHash);
1212 assert_eq!(config.redis_key_prefix, "dedup");
1213 assert_eq!(config.max_retries, 3);
1214 assert!(config.validate().is_ok());
1215 }
1216
1217 #[test]
1218 fn test_deduplication_config_builder() {
1219 let config = DeduplicationConfig::new()
1220 .enabled()
1221 .with_ttl(Duration::from_secs(7200))
1222 .with_strategy(DeduplicationStrategy::EventId)
1223 .with_redis_key_prefix("custom_dedup")
1224 .with_max_retries(5)
1225 .with_cleanup_interval(Duration::from_secs(600));
1226
1227 assert!(config.enabled);
1228 assert_eq!(config.ttl, Duration::from_secs(7200));
1229 assert_eq!(config.strategy, DeduplicationStrategy::EventId);
1230 assert_eq!(config.redis_key_prefix, "custom_dedup");
1231 assert_eq!(config.max_retries, 5);
1232 assert_eq!(config.cleanup_interval, Duration::from_secs(600));
1233 assert!(config.validate().is_ok());
1234 }
1235
1236 #[test]
1237 fn test_deduplication_config_validation() {
1238 let config = DeduplicationConfig::default();
1240 assert!(config.validate().is_ok());
1241
1242 let mut invalid_config = DeduplicationConfig::new().enabled();
1244 invalid_config.ttl = Duration::from_secs(0);
1245 assert!(invalid_config.validate().is_err());
1246
1247 let mut invalid_config = DeduplicationConfig::new().enabled();
1249 invalid_config.redis_key_prefix = String::new();
1250 assert!(invalid_config.validate().is_err());
1251
1252 let mut invalid_config = DeduplicationConfig::new().enabled();
1254 invalid_config.cleanup_interval = Duration::from_secs(0);
1255 assert!(invalid_config.validate().is_err());
1256 }
1257
1258 #[test]
1259 fn test_deduplication_strategy_default() {
1260 let strategy = DeduplicationStrategy::default();
1261 assert_eq!(strategy, DeduplicationStrategy::ContentHash);
1262 }
1263
1264 #[test]
1265 fn test_deduplication_config_ttl_ms() {
1266 let config = DeduplicationConfig::new()
1267 .with_ttl(Duration::from_secs(60));
1268 assert_eq!(config.ttl_ms(), 60_000);
1269 }
1270
1271 #[test]
1272 fn test_deduplication_config_cleanup_interval_ms() {
1273 let config = DeduplicationConfig::new()
1274 .with_cleanup_interval(Duration::from_secs(300));
1275 assert_eq!(config.cleanup_interval_ms(), 300_000);
1276 }
1277
1278 #[test]
1279 fn test_processor_config_with_deduplication() {
1280 let config = ProcessorConfig::default();
1281 assert!(!config.deduplication.enabled);
1282 assert!(config.validate().is_ok());
1283 }
1284
1285 #[test]
1286 fn test_default_normalization_config() {
1287 let config = NormalizationConfig::default();
1288 assert!(!config.enabled);
1289 assert_eq!(config.interval, Duration::from_secs(1));
1290 assert_eq!(config.fill_strategy, FillStrategy::ForwardFill);
1291 assert_eq!(config.alignment, AlignmentStrategy::Start);
1292 assert_eq!(config.buffer_size, 1000);
1293 assert!(!config.drop_outliers);
1294 assert_eq!(config.outlier_threshold, 3.0);
1295 assert!(config.validate().is_ok());
1296 }
1297
1298 #[test]
1299 fn test_normalization_config_builder() {
1300 let config = NormalizationConfig::new()
1301 .enabled()
1302 .with_interval(Duration::from_secs(5))
1303 .with_fill_strategy(FillStrategy::LinearInterpolation)
1304 .with_alignment(AlignmentStrategy::End)
1305 .with_buffer_size(2000)
1306 .with_interpolation_max_gap(Duration::from_secs(15))
1307 .with_outlier_detection(2.5)
1308 .with_metrics(false);
1309
1310 assert!(config.enabled);
1311 assert_eq!(config.interval, Duration::from_secs(5));
1312 assert_eq!(config.fill_strategy, FillStrategy::LinearInterpolation);
1313 assert_eq!(config.alignment, AlignmentStrategy::End);
1314 assert_eq!(config.buffer_size, 2000);
1315 assert_eq!(config.interpolation_max_gap, Duration::from_secs(15));
1316 assert!(config.drop_outliers);
1317 assert_eq!(config.outlier_threshold, 2.5);
1318 assert!(!config.emit_metrics);
1319 assert!(config.validate().is_ok());
1320 }
1321
1322 #[test]
1323 fn test_normalization_config_validation() {
1324 let config = NormalizationConfig::default();
1326 assert!(config.validate().is_ok());
1327
1328 let mut invalid_config = NormalizationConfig::new().enabled();
1330 invalid_config.interval = Duration::from_secs(0);
1331 assert!(invalid_config.validate().is_err());
1332
1333 let mut invalid_config = NormalizationConfig::new().enabled();
1335 invalid_config.buffer_size = 0;
1336 assert!(invalid_config.validate().is_err());
1337
1338 let mut invalid_config = NormalizationConfig::new().enabled();
1340 invalid_config.interpolation_max_gap = Duration::from_secs(0);
1341 assert!(invalid_config.validate().is_err());
1342
1343 let mut invalid_config = NormalizationConfig::new().enabled();
1345 invalid_config.outlier_threshold = -1.0;
1346 assert!(invalid_config.validate().is_err());
1347
1348 let mut invalid_config = NormalizationConfig::new()
1350 .enabled()
1351 .with_interval(Duration::from_secs(10))
1352 .with_interpolation_max_gap(Duration::from_secs(5));
1353 assert!(invalid_config.validate().is_err());
1354 }
1355
1356 #[test]
1357 fn test_fill_strategy_default() {
1358 let strategy = FillStrategy::default();
1359 assert_eq!(strategy, FillStrategy::ForwardFill);
1360 }
1361
1362 #[test]
1363 fn test_alignment_strategy_default() {
1364 let strategy = AlignmentStrategy::default();
1365 assert_eq!(strategy, AlignmentStrategy::Start);
1366 }
1367
1368 #[test]
1369 fn test_normalization_config_interval_ms() {
1370 let config = NormalizationConfig::new()
1371 .with_interval(Duration::from_secs(5));
1372 assert_eq!(config.interval_ms(), 5_000);
1373 }
1374
1375 #[test]
1376 fn test_normalization_config_interpolation_max_gap_ms() {
1377 let config = NormalizationConfig::new()
1378 .with_interpolation_max_gap(Duration::from_secs(10));
1379 assert_eq!(config.interpolation_max_gap_ms(), 10_000);
1380 }
1381
1382 #[test]
1383 fn test_processor_config_with_normalization() {
1384 let config = ProcessorConfig::default();
1385 assert!(!config.normalization.enabled);
1386 assert!(config.validate().is_ok());
1387 }
1388
1389 #[test]
1390 fn test_fill_strategies() {
1391 assert_eq!(FillStrategy::Zero, FillStrategy::Zero);
1392 assert_ne!(FillStrategy::Zero, FillStrategy::ForwardFill);
1393
1394 let strategies = vec![
1395 FillStrategy::Zero,
1396 FillStrategy::ForwardFill,
1397 FillStrategy::BackwardFill,
1398 FillStrategy::LinearInterpolation,
1399 FillStrategy::Drop,
1400 FillStrategy::Constant,
1401 FillStrategy::Mean,
1402 ];
1403
1404 for strategy in strategies {
1405 let serialized = serde_json::to_string(&strategy).unwrap();
1406 let deserialized: FillStrategy = serde_json::from_str(&serialized).unwrap();
1407 assert_eq!(strategy, deserialized);
1408 }
1409 }
1410
1411 #[test]
1412 fn test_alignment_strategies() {
1413 let strategies = vec![
1414 AlignmentStrategy::Start,
1415 AlignmentStrategy::End,
1416 AlignmentStrategy::Center,
1417 AlignmentStrategy::Nearest,
1418 ];
1419
1420 for strategy in strategies {
1421 let serialized = serde_json::to_string(&strategy).unwrap();
1422 let deserialized: AlignmentStrategy = serde_json::from_str(&serialized).unwrap();
1423 assert_eq!(strategy, deserialized);
1424 }
1425 }
1426}