use crate::config::{ProcessorConfig, StateBackend, WindowConfig, WatermarkConfig};
use crate::error::{ProcessorError, Result};
use crate::pipeline::executor::StreamExecutor;
use crate::pipeline::operator::StreamOperator;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineConfig {
pub name: String,
pub processor: ProcessorConfig,
pub description: Option<String>,
pub tags: Vec<String>,
}
impl Default for PipelineConfig {
fn default() -> Self {
Self {
name: "unnamed-pipeline".to_string(),
processor: ProcessorConfig::default(),
description: None,
tags: Vec::new(),
}
}
}
impl PipelineConfig {
pub fn validate(&self) -> Result<()> {
if self.name.is_empty() {
return Err(ProcessorError::Configuration {
source: "pipeline name cannot be empty".into(),
});
}
self.processor.validate()?;
Ok(())
}
}
pub struct StreamPipelineBuilder {
config: PipelineConfig,
}
impl Default for StreamPipelineBuilder {
fn default() -> Self {
Self::new()
}
}
impl StreamPipelineBuilder {
pub fn new() -> Self {
Self {
config: PipelineConfig::default(),
}
}
pub fn with_name<S: Into<String>>(mut self, name: S) -> Self {
self.config.name = name.into();
self
}
pub fn with_description<S: Into<String>>(mut self, description: S) -> Self {
self.config.description = Some(description.into());
self
}
pub fn with_tag<S: Into<String>>(mut self, tag: S) -> Self {
self.config.tags.push(tag.into());
self
}
pub fn with_tags<I, S>(mut self, tags: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.config.tags.extend(tags.into_iter().map(|s| s.into()));
self
}
pub fn with_parallelism(mut self, parallelism: usize) -> Self {
self.config.processor.parallelism = parallelism;
self
}
pub fn with_buffer_size(mut self, buffer_size: usize) -> Self {
self.config.processor.buffer_size = buffer_size;
self
}
pub fn with_metrics(mut self, enabled: bool) -> Self {
self.config.processor.metrics_enabled = enabled;
self
}
pub fn with_window(mut self, window: WindowConfig) -> Self {
self.config.processor.window = window;
self
}
pub fn with_tumbling_window(mut self, size_ms: u64) -> Self {
self.config.processor.window = WindowConfig::tumbling(size_ms);
self
}
pub fn with_sliding_window(mut self, size_ms: u64, slide_ms: u64) -> Self {
self.config.processor.window = WindowConfig::sliding(size_ms, slide_ms);
self
}
pub fn with_session_window(mut self, gap_ms: u64) -> Self {
self.config.processor.window = WindowConfig::session(gap_ms);
self
}
pub fn with_allowed_lateness(mut self, lateness_ms: u64) -> Self {
self.config.processor.window.allowed_lateness_ms = lateness_ms;
self
}
pub fn with_drop_late_events(mut self, drop: bool) -> Self {
self.config.processor.window.drop_late_events = drop;
self
}
pub fn with_watermark_config(mut self, watermark: WatermarkConfig) -> Self {
self.config.processor.watermark = watermark;
self
}
pub fn with_watermark_delay(mut self, delay: Duration) -> Self {
self.config.processor.watermark.max_delay_ms = delay.as_millis() as u64;
self
}
pub fn with_idle_timeout(mut self, timeout: Duration) -> Self {
self.config.processor.watermark.idle_timeout_ms = timeout.as_millis() as u64;
self
}
pub fn with_watermark_interval(mut self, interval: Duration) -> Self {
self.config.processor.watermark.update_interval_ms = interval.as_millis() as u64;
self
}
pub fn with_state_backend(mut self, backend: StateBackend) -> Self {
self.config.processor.state.backend = backend;
self
}
pub fn with_state_path<S: Into<std::path::PathBuf>>(mut self, path: S) -> Self {
self.config.processor.state.storage_path = Some(path.into());
self
}
pub fn with_checkpoint_interval(mut self, interval: Duration) -> Self {
self.config.processor.state.checkpoint_interval_ms = interval.as_millis() as u64;
self
}
pub fn with_incremental_checkpoints(mut self, enabled: bool) -> Self {
self.config.processor.state.incremental_checkpoints = enabled;
self
}
pub fn with_checkpoint_retention(mut self, count: usize) -> Self {
self.config.processor.state.checkpoint_retention = count;
self
}
pub fn with_state_ttl(mut self, ttl: Duration) -> Self {
self.config.processor.state.ttl_ms = Some(ttl.as_millis() as u64);
self
}
pub fn with_max_state_size(mut self, size_bytes: u64) -> Self {
self.config.processor.state.max_size_bytes = Some(size_bytes);
self
}
pub fn with_state_compression(mut self, enabled: bool) -> Self {
self.config.processor.state.compression_enabled = enabled;
self
}
pub fn with_percentiles(mut self, percentiles: Vec<u8>) -> Self {
self.config.processor.aggregation.percentiles = percentiles;
self
}
pub fn with_histogram_buckets(mut self, buckets: Vec<f64>) -> Self {
self.config.processor.aggregation.histogram_buckets = Some(buckets);
self
}
pub fn with_min_samples(mut self, min: usize) -> Self {
self.config.processor.aggregation.min_samples = min;
self
}
pub fn with_max_samples(mut self, max: usize) -> Self {
self.config.processor.aggregation.max_samples = max;
self
}
pub fn with_outlier_removal(mut self, threshold: f64) -> Self {
self.config.processor.aggregation.remove_outliers = true;
self.config.processor.aggregation.outlier_threshold = threshold;
self
}
pub fn with_deduplication(mut self, enabled: bool, ttl: Duration) -> Self {
self.config.processor.deduplication.enabled = enabled;
self.config.processor.deduplication.ttl = ttl;
self
}
pub fn with_deduplication_config(mut self, config: crate::config::DeduplicationConfig) -> Self {
self.config.processor.deduplication = config;
self
}
pub fn with_deduplication_ttl(mut self, ttl: Duration) -> Self {
self.config.processor.deduplication.ttl = ttl;
self
}
pub fn with_deduplication_strategy(mut self, strategy: crate::config::DeduplicationStrategy) -> Self {
self.config.processor.deduplication.strategy = strategy;
self
}
pub fn with_deduplication_redis_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
self.config.processor.deduplication.redis_key_prefix = prefix.into();
self
}
pub fn with_normalization(mut self, enabled: bool, interval: Duration) -> Self {
self.config.processor.normalization.enabled = enabled;
self.config.processor.normalization.interval = interval;
self
}
pub fn with_normalization_config(mut self, config: crate::config::NormalizationConfig) -> Self {
self.config.processor.normalization = config;
self
}
pub fn with_normalization_interval(mut self, interval: Duration) -> Self {
self.config.processor.normalization.interval = interval;
self
}
pub fn with_fill_strategy(mut self, strategy: crate::config::FillStrategy) -> Self {
self.config.processor.normalization.fill_strategy = strategy;
self
}
pub fn with_alignment_strategy(mut self, strategy: crate::config::AlignmentStrategy) -> Self {
self.config.processor.normalization.alignment = strategy;
self
}
pub fn with_normalization_buffer_size(mut self, size: usize) -> Self {
self.config.processor.normalization.buffer_size = size;
self
}
pub fn with_interpolation_max_gap(mut self, gap: Duration) -> Self {
self.config.processor.normalization.interpolation_max_gap = gap;
self
}
pub fn with_normalization_outlier_detection(mut self, threshold: f64) -> Self {
self.config.processor.normalization.drop_outliers = true;
self.config.processor.normalization.outlier_threshold = threshold;
self
}
pub fn build(self) -> Result<StreamPipeline> {
self.config.validate()?;
Ok(StreamPipeline {
config: Arc::new(self.config),
operators: Vec::new(),
})
}
}
#[derive(Clone)]
pub struct StreamPipeline {
config: Arc<PipelineConfig>,
operators: Vec<Arc<dyn StreamOperator>>,
}
impl StreamPipeline {
pub fn config(&self) -> &PipelineConfig {
&self.config
}
pub fn name(&self) -> &str {
&self.config.name
}
pub fn description(&self) -> Option<&str> {
self.config.description.as_deref()
}
pub fn tags(&self) -> &[String] {
&self.config.tags
}
pub fn add_operator<O: StreamOperator + 'static>(&mut self, operator: O) {
self.operators.push(Arc::new(operator));
}
pub fn operators(&self) -> &[Arc<dyn StreamOperator>] {
&self.operators
}
pub fn create_executor<T>(
&self,
) -> StreamExecutor<T>
where
T: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de>,
{
StreamExecutor::new(self.clone())
}
}
impl std::fmt::Debug for StreamPipeline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StreamPipeline")
.field("config", &self.config)
.field("operator_count", &self.operators.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::WindowType;
#[test]
fn test_builder_default() {
let pipeline = StreamPipelineBuilder::new().build().unwrap();
assert_eq!(pipeline.name(), "unnamed-pipeline");
}
#[test]
fn test_builder_with_name() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test-pipeline")
.build()
.unwrap();
assert_eq!(pipeline.name(), "test-pipeline");
}
#[test]
fn test_builder_with_description() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_description("A test pipeline")
.build()
.unwrap();
assert_eq!(pipeline.description(), Some("A test pipeline"));
}
#[test]
fn test_builder_with_tags() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_tag("production")
.with_tag("metrics")
.build()
.unwrap();
assert_eq!(pipeline.tags(), &["production", "metrics"]);
}
#[test]
fn test_builder_with_multiple_tags() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_tags(vec!["tag1", "tag2", "tag3"])
.build()
.unwrap();
assert_eq!(pipeline.tags().len(), 3);
}
#[test]
fn test_builder_with_parallelism() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_parallelism(16)
.build()
.unwrap();
assert_eq!(pipeline.config().processor.parallelism, 16);
}
#[test]
fn test_builder_with_buffer_size() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_buffer_size(50_000)
.build()
.unwrap();
assert_eq!(pipeline.config().processor.buffer_size, 50_000);
}
#[test]
fn test_builder_with_tumbling_window() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_tumbling_window(60_000)
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.window.window_type,
WindowType::Tumbling
);
assert_eq!(pipeline.config().processor.window.size_ms, Some(60_000));
}
#[test]
fn test_builder_with_sliding_window() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_sliding_window(300_000, 60_000)
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.window.window_type,
WindowType::Sliding
);
assert_eq!(pipeline.config().processor.window.size_ms, Some(300_000));
assert_eq!(pipeline.config().processor.window.slide_ms, Some(60_000));
}
#[test]
fn test_builder_with_session_window() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_session_window(300_000)
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.window.window_type,
WindowType::Session
);
assert_eq!(pipeline.config().processor.window.gap_ms, Some(300_000));
}
#[test]
fn test_builder_with_watermark_delay() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_watermark_delay(Duration::from_secs(10))
.build()
.unwrap();
assert_eq!(pipeline.config().processor.watermark.max_delay_ms, 10_000);
}
#[test]
fn test_builder_with_state_backend() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_state_backend(StateBackend::FileSystem)
.with_state_path("/tmp/state")
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.state.backend,
StateBackend::FileSystem
);
assert!(pipeline.config().processor.state.storage_path.is_some());
}
#[test]
fn test_builder_with_checkpoint_config() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_checkpoint_interval(Duration::from_secs(30))
.with_checkpoint_retention(5)
.with_incremental_checkpoints(false)
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.state.checkpoint_interval_ms,
30_000
);
assert_eq!(pipeline.config().processor.state.checkpoint_retention, 5);
assert!(!pipeline.config().processor.state.incremental_checkpoints);
}
#[test]
fn test_builder_with_state_ttl() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_state_ttl(Duration::from_secs(3600))
.build()
.unwrap();
assert_eq!(pipeline.config().processor.state.ttl_ms, Some(3_600_000));
}
#[test]
fn test_builder_with_aggregation_config() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_percentiles(vec![50, 90, 95, 99])
.with_min_samples(10)
.with_max_samples(200_000)
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.aggregation.percentiles,
vec![50, 90, 95, 99]
);
assert_eq!(pipeline.config().processor.aggregation.min_samples, 10);
assert_eq!(pipeline.config().processor.aggregation.max_samples, 200_000);
}
#[test]
fn test_builder_with_outlier_removal() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_outlier_removal(3.0)
.build()
.unwrap();
assert!(pipeline.config().processor.aggregation.remove_outliers);
assert_eq!(
pipeline.config().processor.aggregation.outlier_threshold,
3.0
);
}
#[test]
fn test_builder_fluent_api() {
let pipeline = StreamPipelineBuilder::new()
.with_name("complex-pipeline")
.with_description("A complex stream processing pipeline")
.with_tags(vec!["production", "metrics", "aggregation"])
.with_parallelism(8)
.with_buffer_size(100_000)
.with_tumbling_window(60_000)
.with_watermark_delay(Duration::from_secs(5))
.with_state_backend(StateBackend::Memory)
.with_percentiles(vec![50, 95, 99])
.build()
.unwrap();
assert_eq!(pipeline.name(), "complex-pipeline");
assert_eq!(pipeline.config().processor.parallelism, 8);
assert_eq!(pipeline.config().processor.buffer_size, 100_000);
}
#[test]
fn test_pipeline_validation_fails_on_empty_name() {
let result = StreamPipelineBuilder::new()
.with_name("")
.build();
assert!(result.is_err());
}
#[test]
fn test_pipeline_create_executor() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.build()
.unwrap();
let _executor: StreamExecutor<i32> = pipeline.create_executor();
}
#[test]
fn test_builder_with_deduplication() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_deduplication(true, Duration::from_secs(3600))
.build()
.unwrap();
assert!(pipeline.config().processor.deduplication.enabled);
assert_eq!(
pipeline.config().processor.deduplication.ttl,
Duration::from_secs(3600)
);
}
#[test]
fn test_builder_with_deduplication_config() {
use crate::config::{DeduplicationConfig, DeduplicationStrategy};
let dedup_config = DeduplicationConfig::new()
.enabled()
.with_ttl(Duration::from_secs(7200))
.with_strategy(DeduplicationStrategy::EventId)
.with_redis_key_prefix("custom_dedup");
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_deduplication_config(dedup_config)
.build()
.unwrap();
let config = &pipeline.config().processor.deduplication;
assert!(config.enabled);
assert_eq!(config.ttl, Duration::from_secs(7200));
assert_eq!(config.strategy, DeduplicationStrategy::EventId);
assert_eq!(config.redis_key_prefix, "custom_dedup");
}
#[test]
fn test_builder_with_deduplication_ttl() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_deduplication_ttl(Duration::from_secs(1800))
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.deduplication.ttl,
Duration::from_secs(1800)
);
}
#[test]
fn test_builder_with_deduplication_strategy() {
use crate::config::DeduplicationStrategy;
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_deduplication_strategy(DeduplicationStrategy::CompositeKey)
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.deduplication.strategy,
DeduplicationStrategy::CompositeKey
);
}
#[test]
fn test_builder_with_deduplication_redis_prefix() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_deduplication_redis_prefix("myapp:dedup")
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.deduplication.redis_key_prefix,
"myapp:dedup"
);
}
#[test]
fn test_builder_deduplication_fluent_api() {
use crate::config::DeduplicationStrategy;
let pipeline = StreamPipelineBuilder::new()
.with_name("dedup-pipeline")
.with_deduplication(true, Duration::from_secs(3600))
.with_deduplication_strategy(DeduplicationStrategy::ContentHash)
.with_deduplication_redis_prefix("prod:dedup")
.build()
.unwrap();
let config = &pipeline.config().processor.deduplication;
assert!(config.enabled);
assert_eq!(config.ttl, Duration::from_secs(3600));
assert_eq!(config.strategy, DeduplicationStrategy::ContentHash);
assert_eq!(config.redis_key_prefix, "prod:dedup");
}
#[test]
fn test_builder_with_normalization() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_normalization(true, Duration::from_secs(5))
.build()
.unwrap();
assert!(pipeline.config().processor.normalization.enabled);
assert_eq!(
pipeline.config().processor.normalization.interval,
Duration::from_secs(5)
);
}
#[test]
fn test_builder_with_normalization_config() {
use crate::config::{NormalizationConfig, FillStrategy, AlignmentStrategy};
let norm_config = NormalizationConfig::new()
.enabled()
.with_interval(Duration::from_secs(10))
.with_fill_strategy(FillStrategy::LinearInterpolation)
.with_alignment(AlignmentStrategy::End);
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_normalization_config(norm_config)
.build()
.unwrap();
let config = &pipeline.config().processor.normalization;
assert!(config.enabled);
assert_eq!(config.interval, Duration::from_secs(10));
assert_eq!(config.fill_strategy, FillStrategy::LinearInterpolation);
assert_eq!(config.alignment, AlignmentStrategy::End);
}
#[test]
fn test_builder_with_normalization_interval() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_normalization_interval(Duration::from_secs(15))
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.normalization.interval,
Duration::from_secs(15)
);
}
#[test]
fn test_builder_with_fill_strategy() {
use crate::config::FillStrategy;
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_fill_strategy(FillStrategy::BackwardFill)
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.normalization.fill_strategy,
FillStrategy::BackwardFill
);
}
#[test]
fn test_builder_with_alignment_strategy() {
use crate::config::AlignmentStrategy;
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_alignment_strategy(AlignmentStrategy::Center)
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.normalization.alignment,
AlignmentStrategy::Center
);
}
#[test]
fn test_builder_with_normalization_buffer_size() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_normalization_buffer_size(5000)
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.normalization.buffer_size,
5000
);
}
#[test]
fn test_builder_with_interpolation_max_gap() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_interpolation_max_gap(Duration::from_secs(20))
.build()
.unwrap();
assert_eq!(
pipeline.config().processor.normalization.interpolation_max_gap,
Duration::from_secs(20)
);
}
#[test]
fn test_builder_with_normalization_outlier_detection() {
let pipeline = StreamPipelineBuilder::new()
.with_name("test")
.with_normalization_outlier_detection(2.5)
.build()
.unwrap();
assert!(pipeline.config().processor.normalization.drop_outliers);
assert_eq!(
pipeline.config().processor.normalization.outlier_threshold,
2.5
);
}
#[test]
fn test_builder_normalization_fluent_api() {
use crate::config::{FillStrategy, AlignmentStrategy};
let pipeline = StreamPipelineBuilder::new()
.with_name("norm-pipeline")
.with_normalization(true, Duration::from_secs(5))
.with_fill_strategy(FillStrategy::LinearInterpolation)
.with_alignment_strategy(AlignmentStrategy::Start)
.with_normalization_buffer_size(2000)
.with_interpolation_max_gap(Duration::from_secs(15))
.with_normalization_outlier_detection(3.0)
.build()
.unwrap();
let config = &pipeline.config().processor.normalization;
assert!(config.enabled);
assert_eq!(config.interval, Duration::from_secs(5));
assert_eq!(config.fill_strategy, FillStrategy::LinearInterpolation);
assert_eq!(config.alignment, AlignmentStrategy::Start);
assert_eq!(config.buffer_size, 2000);
assert_eq!(config.interpolation_max_gap, Duration::from_secs(15));
assert!(config.drop_outliers);
assert_eq!(config.outlier_threshold, 3.0);
}
}