use super::builder::PipelineConfig;
use super::stage::{PipelineStage, StageResult};
use crate::error::{Result, StreamingError};
use crate::pipeline::zerocopy::ZeroCopyBuffer;
use bytes::Bytes;
use std::sync::Arc;
use std::time::Instant;
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct ExecutionStats {
pub items_processed: usize,
pub bytes_processed: usize,
pub total_time_ms: u64,
pub avg_time_per_item_ms: f64,
}
pub struct PipelineExecutor {
config: PipelineConfig,
stages: Vec<Arc<dyn PipelineStage>>,
}
impl PipelineExecutor {
pub async fn new(config: PipelineConfig, stages: Vec<Arc<dyn PipelineStage>>) -> Result<Self> {
if stages.is_empty() {
return Err(StreamingError::ConfigError(
"Pipeline must have at least one stage".to_string(),
));
}
for stage in &stages {
stage.initialize().await?;
}
info!("Created pipeline executor with {} stages", stages.len());
Ok(Self { config, stages })
}
pub async fn process(&self, data: Bytes) -> Result<StageResult> {
let start = Instant::now();
let mut current = ZeroCopyBuffer::new(data);
for stage in &self.stages {
debug!("Executing stage: {}", stage.name());
let result = stage.process(current).await?;
current = result.data;
}
let elapsed_ms = start.elapsed().as_millis() as u64;
Ok(StageResult::new(current, elapsed_ms))
}
pub async fn process_batch(&self, items: Vec<Bytes>) -> Result<ExecutionStats> {
let start = Instant::now();
let mut total_bytes = 0usize;
let item_count = items.len();
for item in items {
let byte_len = item.len();
self.process(item).await?;
total_bytes += byte_len;
}
let total_ms = start.elapsed().as_millis() as u64;
let avg_ms = if item_count > 0 {
total_ms as f64 / item_count as f64
} else {
0.0
};
let stats = ExecutionStats {
items_processed: item_count,
bytes_processed: total_bytes,
total_time_ms: total_ms,
avg_time_per_item_ms: avg_ms,
};
info!(
"Processed {} items ({} bytes) in {}ms",
stats.items_processed, stats.bytes_processed, stats.total_time_ms
);
Ok(stats)
}
pub async fn finalize(self) -> Result<()> {
for stage in &self.stages {
stage.finalize().await?;
}
info!("Pipeline finalized");
Ok(())
}
pub fn config(&self) -> &PipelineConfig {
&self.config
}
pub fn stage_count(&self) -> usize {
self.stages.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::stage::TransformStage;
use bytes::Bytes;
#[tokio::test]
async fn test_pipeline_executor() {
let config = PipelineConfig::default();
let stage = Arc::new(TransformStage::new(
"identity".to_string(),
|data: &[u8]| Ok(data.to_vec()),
));
let executor = PipelineExecutor::new(config, vec![stage])
.await
.expect("executor should be created");
let data = Bytes::from(vec![1u8, 2, 3, 4, 5]);
let result = executor
.process(data)
.await
.expect("process should succeed");
assert_eq!(result.bytes_processed, 5);
}
}