Rust Data Processing Best Practices¶
Objective: Master senior-level Rust data processing patterns for production systems. When you need to build high-performance data pipelines, when you want to process large datasets efficiently, when you need enterprise-grade data processingโthese best practices become your weapon of choice.
Core Principles¶
- Stream Processing: Process data in streams for memory efficiency
- Parallel Processing: Leverage multiple cores for data processing
- Memory Management: Optimize memory usage for large datasets
- Error Handling: Robust error handling for data processing
- Backpressure: Handle data flow control in streaming scenarios
Data Processing Patterns¶
Stream Processing¶
// rust/01-stream-processing.rs
/*
Stream processing patterns and best practices
*/
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
/// Data stream processor.
pub struct StreamProcessor {
input_channel: mpsc::Receiver<DataRecord>,
output_channel: mpsc::Sender<ProcessedRecord>,
buffer_size: usize,
processing_timeout: Duration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataRecord {
pub id: String,
pub timestamp: Instant,
pub data: HashMap<String, serde_json::Value>,
pub source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessedRecord {
pub id: String,
pub timestamp: Instant,
pub processed_data: HashMap<String, serde_json::Value>,
pub processing_time: Duration,
pub source: String,
}
impl StreamProcessor {
pub fn new(
input_channel: mpsc::Receiver<DataRecord>,
output_channel: mpsc::Sender<ProcessedRecord>,
buffer_size: usize,
processing_timeout: Duration,
) -> Self {
Self {
input_channel,
output_channel,
buffer_size,
processing_timeout,
}
}
/// Process data stream.
pub async fn process_stream(&mut self) -> Result<(), String> {
let mut buffer = Vec::with_capacity(self.buffer_size);
loop {
// Try to receive data with timeout
match tokio::time::timeout(self.processing_timeout, self.input_channel.recv()).await {
Ok(Some(record)) => {
buffer.push(record);
// Process buffer when it's full
if buffer.len() >= self.buffer_size {
self.process_batch(&mut buffer).await?;
}
}
Ok(None) => {
// Input channel closed, process remaining buffer
if !buffer.is_empty() {
self.process_batch(&mut buffer).await?;
}
break;
}
Err(_) => {
// Timeout, process current buffer
if !buffer.is_empty() {
self.process_batch(&mut buffer).await?;
}
}
}
}
Ok(())
}
/// Process a batch of records.
async fn process_batch(&mut self, buffer: &mut Vec<DataRecord>) -> Result<(), String> {
let start_time = Instant::now();
// Process each record in the batch
for record in buffer.drain(..) {
let processed = self.process_record(record).await?;
self.output_channel.send(processed).await
.map_err(|e| format!("Failed to send processed record: {}", e))?;
}
let processing_time = start_time.elapsed();
println!("Processed batch in {:?}", processing_time);
Ok(())
}
/// Process a single record.
async fn process_record(&self, record: DataRecord) -> Result<ProcessedRecord, String> {
let start_time = Instant::now();
// Simulate data processing
let mut processed_data = HashMap::new();
for (key, value) in record.data {
// Transform the data
let transformed_value = self.transform_value(&key, &value)?;
processed_data.insert(key, transformed_value);
}
let processing_time = start_time.elapsed();
Ok(ProcessedRecord {
id: record.id,
timestamp: record.timestamp,
processed_data,
processing_time,
source: record.source,
})
}
/// Transform a value based on its key.
fn transform_value(&self, key: &str, value: &serde_json::Value) -> Result<serde_json::Value, String> {
match key {
"temperature" => {
if let Some(temp) = value.as_f64() {
// Convert Celsius to Fahrenheit
let fahrenheit = (temp * 9.0 / 5.0) + 32.0;
Ok(serde_json::Value::Number(serde_json::Number::from_f64(fahrenheit).unwrap()))
} else {
Err("Invalid temperature value".to_string())
}
}
"pressure" => {
if let Some(pressure) = value.as_f64() {
// Convert Pa to PSI
let psi = pressure * 0.000145038;
Ok(serde_json::Value::Number(serde_json::Number::from_f64(psi).unwrap()))
} else {
Err("Invalid pressure value".to_string())
}
}
_ => Ok(value.clone()),
}
}
}
/// Data pipeline orchestrator.
pub struct DataPipeline {
processors: Vec<Arc<StreamProcessor>>,
input_channels: Vec<mpsc::Sender<DataRecord>>,
output_channels: Vec<mpsc::Receiver<ProcessedRecord>>,
}
impl DataPipeline {
pub fn new() -> Self {
Self {
processors: Vec::new(),
input_channels: Vec::new(),
output_channels: Vec::new(),
}
}
/// Add a processor to the pipeline.
pub fn add_processor(&mut self, buffer_size: usize, processing_timeout: Duration) -> (mpsc::Sender<DataRecord>, mpsc::Receiver<ProcessedRecord>) {
let (input_tx, input_rx) = mpsc::channel(buffer_size);
let (output_tx, output_rx) = mpsc::channel(buffer_size);
let processor = Arc::new(StreamProcessor::new(
input_rx,
output_tx,
buffer_size,
processing_timeout,
));
self.processors.push(processor);
self.input_channels.push(input_tx);
self.output_channels.push(output_rx);
(self.input_channels.last().unwrap().clone(), output_rx)
}
/// Start all processors.
pub async fn start(&self) -> Result<(), String> {
let mut handles = Vec::new();
for processor in &self.processors {
let processor = Arc::clone(processor);
let handle = tokio::spawn(async move {
let mut processor = (*processor).clone();
processor.process_stream().await
});
handles.push(handle);
}
// Wait for all processors to complete
for handle in handles {
handle.await.map_err(|e| format!("Processor error: {}", e))??;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_stream_processor() {
let (input_tx, input_rx) = mpsc::channel(10);
let (output_tx, output_rx) = mpsc::channel(10);
let mut processor = StreamProcessor::new(
input_rx,
output_tx,
5,
Duration::from_secs(1),
);
// Send test data
let mut data = HashMap::new();
data.insert("temperature".to_string(), serde_json::Value::Number(serde_json::Number::from(25)));
data.insert("pressure".to_string(), serde_json::Value::Number(serde_json::Number::from(101325)));
let record = DataRecord {
id: "test-1".to_string(),
timestamp: Instant::now(),
data,
source: "sensor-1".to_string(),
};
input_tx.send(record).await.unwrap();
drop(input_tx);
// Start processing
let handle = tokio::spawn(async move {
processor.process_stream().await
});
// Wait for processing to complete
handle.await.unwrap().unwrap();
// Check output
let processed = output_rx.recv().await.unwrap();
assert_eq!(processed.id, "test-1");
}
#[tokio::test]
async fn test_data_pipeline() {
let mut pipeline = DataPipeline::new();
let (input_tx, output_rx) = pipeline.add_processor(10, Duration::from_secs(1));
// Send test data
let mut data = HashMap::new();
data.insert("temperature".to_string(), serde_json::Value::Number(serde_json::Number::from(25)));
let record = DataRecord {
id: "test-1".to_string(),
timestamp: Instant::now(),
data,
source: "sensor-1".to_string(),
};
input_tx.send(record).await.unwrap();
drop(input_tx);
// Start pipeline
let handle = tokio::spawn(async move {
pipeline.start().await
});
// Wait for processing to complete
handle.await.unwrap().unwrap();
// Check output
let processed = output_rx.recv().await.unwrap();
assert_eq!(processed.id, "test-1");
}
}
Batch Processing¶
// rust/02-batch-processing.rs
/*
Batch processing patterns and best practices
*/
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use serde::{Deserialize, Serialize};
/// Batch processor for large datasets.
pub struct BatchProcessor {
batch_size: usize,
max_workers: usize,
data: Arc<RwLock<Vec<DataRecord>>>,
results: Arc<RwLock<Vec<ProcessedRecord>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataRecord {
pub id: String,
pub data: HashMap<String, serde_json::Value>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessedRecord {
pub id: String,
pub processed_data: HashMap<String, serde_json::Value>,
pub processing_time: std::time::Duration,
pub worker_id: usize,
}
impl BatchProcessor {
pub fn new(batch_size: usize, max_workers: usize) -> Self {
Self {
batch_size,
max_workers,
data: Arc::new(RwLock::new(Vec::new())),
results: Arc::new(RwLock::new(Vec::new())),
}
}
/// Add data to the processor.
pub async fn add_data(&self, record: DataRecord) {
let mut data = self.data.write().await;
data.push(record);
}
/// Process all data in batches.
pub async fn process_all(&self) -> Result<Vec<ProcessedRecord>, String> {
let data = self.data.read().await;
let total_records = data.len();
let total_batches = (total_records + self.batch_size - 1) / self.batch_size;
println!("Processing {} records in {} batches", total_records, total_batches);
// Process batches in parallel
let mut handles = Vec::new();
for batch_index in 0..total_batches {
let start = batch_index * self.batch_size;
let end = std::cmp::min(start + self.batch_size, total_records);
let batch_data = data[start..end].to_vec();
let results = Arc::clone(&self.results);
let handle = tokio::spawn(async move {
Self::process_batch(batch_data, batch_index).await
});
handles.push(handle);
}
// Wait for all batches to complete
for handle in handles {
let batch_results = handle.await.map_err(|e| format!("Batch processing error: {}", e))?;
let mut results = self.results.write().await;
results.extend(batch_results);
}
let results = self.results.read().await;
Ok(results.clone())
}
/// Process a single batch.
async fn process_batch(batch_data: Vec<DataRecord>, batch_index: usize) -> Vec<ProcessedRecord> {
let start_time = std::time::Instant::now();
let mut results = Vec::new();
for (index, record) in batch_data.into_iter().enumerate() {
let worker_id = batch_index * 1000 + index;
let processed = Self::process_record(record, worker_id).await;
results.push(processed);
}
let processing_time = start_time.elapsed();
println!("Batch {} processed in {:?}", batch_index, processing_time);
results
}
/// Process a single record.
async fn process_record(record: DataRecord, worker_id: usize) -> ProcessedRecord {
let start_time = std::time::Instant::now();
// Simulate data processing
let mut processed_data = HashMap::new();
for (key, value) in record.data {
// Transform the data
let transformed_value = Self::transform_value(&key, &value);
processed_data.insert(key, transformed_value);
}
let processing_time = start_time.elapsed();
ProcessedRecord {
id: record.id,
processed_data,
processing_time,
worker_id,
}
}
/// Transform a value based on its key.
fn transform_value(key: &str, value: &serde_json::Value) -> serde_json::Value {
match key {
"temperature" => {
if let Some(temp) = value.as_f64() {
// Convert Celsius to Fahrenheit
let fahrenheit = (temp * 9.0 / 5.0) + 32.0;
serde_json::Value::Number(serde_json::Number::from_f64(fahrenheit).unwrap())
} else {
value.clone()
}
}
"pressure" => {
if let Some(pressure) = value.as_f64() {
// Convert Pa to PSI
let psi = pressure * 0.000145038;
serde_json::Value::Number(serde_json::Number::from_f64(psi).unwrap())
} else {
value.clone()
}
}
_ => value.clone(),
}
}
}
/// Data aggregator for batch results.
pub struct DataAggregator {
results: Arc<RwLock<Vec<ProcessedRecord>>>,
}
impl DataAggregator {
pub fn new() -> Self {
Self {
results: Arc::new(RwLock::new(Vec::new())),
}
}
/// Add processed results.
pub async fn add_results(&self, results: Vec<ProcessedRecord>) {
let mut all_results = self.results.write().await;
all_results.extend(results);
}
/// Get aggregated statistics.
pub async fn get_statistics(&self) -> ProcessingStatistics {
let results = self.results.read().await;
let total_records = results.len();
let total_processing_time: std::time::Duration = results
.iter()
.map(|r| r.processing_time)
.sum();
let avg_processing_time = if total_records > 0 {
total_processing_time / total_records as u32
} else {
std::time::Duration::from_secs(0)
};
let worker_counts: HashMap<usize, usize> = results
.iter()
.map(|r| r.worker_id)
.fold(HashMap::new(), |mut acc, id| {
*acc.entry(id).or_insert(0) += 1;
acc
});
ProcessingStatistics {
total_records,
total_processing_time,
avg_processing_time,
worker_counts,
}
}
}
#[derive(Debug, Clone)]
pub struct ProcessingStatistics {
pub total_records: usize,
pub total_processing_time: std::time::Duration,
pub avg_processing_time: std::time::Duration,
pub worker_counts: HashMap<usize, usize>,
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_batch_processor() {
let processor = BatchProcessor::new(10, 4);
// Add test data
for i in 0..25 {
let mut data = HashMap::new();
data.insert("temperature".to_string(), serde_json::Value::Number(serde_json::Number::from(20 + i)));
data.insert("pressure".to_string(), serde_json::Value::Number(serde_json::Number::from(100000 + i * 1000)));
let record = DataRecord {
id: format!("test-{}", i),
data,
metadata: HashMap::new(),
};
processor.add_data(record).await;
}
// Process all data
let results = processor.process_all().await.unwrap();
assert_eq!(results.len(), 25);
}
#[tokio::test]
async fn test_data_aggregator() {
let aggregator = DataAggregator::new();
let mut results = Vec::new();
for i in 0..10 {
let processed = ProcessedRecord {
id: format!("test-{}", i),
processed_data: HashMap::new(),
processing_time: std::time::Duration::from_millis(100),
worker_id: i % 3,
};
results.push(processed);
}
aggregator.add_results(results).await;
let stats = aggregator.get_statistics().await;
assert_eq!(stats.total_records, 10);
}
}
Data Validation¶
// rust/03-data-validation.rs
/*
Data validation patterns and best practices
*/
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// Data validation error types.
#[derive(Error, Debug)]
pub enum ValidationError {
#[error("Field '{field}' is required")]
RequiredField { field: String },
#[error("Field '{field}' has invalid format: {message}")]
InvalidFormat { field: String, message: String },
#[error("Field '{field}' is out of range: {min} <= {value} <= {max}")]
OutOfRange { field: String, value: f64, min: f64, max: f64 },
#[error("Field '{field}' has invalid type: expected {expected}, got {actual}")]
InvalidType { field: String, expected: String, actual: String },
}
/// Data validator trait.
pub trait DataValidator {
fn validate(&self, data: &HashMap<String, serde_json::Value>) -> Result<(), Vec<ValidationError>>;
}
/// Temperature validator.
pub struct TemperatureValidator {
min_temp: f64,
max_temp: f64,
}
impl TemperatureValidator {
pub fn new(min_temp: f64, max_temp: f64) -> Self {
Self { min_temp, max_temp }
}
}
impl DataValidator for TemperatureValidator {
fn validate(&self, data: &HashMap<String, serde_json::Value>) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
if let Some(temp_value) = data.get("temperature") {
if let Some(temp) = temp_value.as_f64() {
if temp < self.min_temp || temp > self.max_temp {
errors.push(ValidationError::OutOfRange {
field: "temperature".to_string(),
value: temp,
min: self.min_temp,
max: self.max_temp,
});
}
} else {
errors.push(ValidationError::InvalidType {
field: "temperature".to_string(),
expected: "number".to_string(),
actual: temp_value.type_str().to_string(),
});
}
} else {
errors.push(ValidationError::RequiredField {
field: "temperature".to_string(),
});
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
/// Pressure validator.
pub struct PressureValidator {
min_pressure: f64,
max_pressure: f64,
}
impl PressureValidator {
pub fn new(min_pressure: f64, max_pressure: f64) -> Self {
Self { min_pressure, max_pressure }
}
}
impl DataValidator for PressureValidator {
fn validate(&self, data: &HashMap<String, serde_json::Value>) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
if let Some(pressure_value) = data.get("pressure") {
if let Some(pressure) = pressure_value.as_f64() {
if pressure < self.min_pressure || pressure > self.max_pressure {
errors.push(ValidationError::OutOfRange {
field: "pressure".to_string(),
value: pressure,
min: self.min_pressure,
max: self.max_pressure,
});
}
} else {
errors.push(ValidationError::InvalidType {
field: "pressure".to_string(),
expected: "number".to_string(),
actual: pressure_value.type_str().to_string(),
});
}
} else {
errors.push(ValidationError::RequiredField {
field: "pressure".to_string(),
});
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
/// Composite validator.
pub struct CompositeValidator {
validators: Vec<Box<dyn DataValidator>>,
}
impl CompositeValidator {
pub fn new() -> Self {
Self {
validators: Vec::new(),
}
}
pub fn add_validator(&mut self, validator: Box<dyn DataValidator>) {
self.validators.push(validator);
}
}
impl DataValidator for CompositeValidator {
fn validate(&self, data: &HashMap<String, serde_json::Value>) -> Result<(), Vec<ValidationError>> {
let mut all_errors = Vec::new();
for validator in &self.validators {
if let Err(errors) = validator.validate(data) {
all_errors.extend(errors);
}
}
if all_errors.is_empty() {
Ok(())
} else {
Err(all_errors)
}
}
}
/// Data validation service.
pub struct ValidationService {
validator: CompositeValidator,
}
impl ValidationService {
pub fn new() -> Self {
Self {
validator: CompositeValidator::new(),
}
}
pub fn add_validator(&mut self, validator: Box<dyn DataValidator>) {
self.validator.add_validator(validator);
}
pub fn validate_data(&self, data: &HashMap<String, serde_json::Value>) -> Result<(), Vec<ValidationError>> {
self.validator.validate(data)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_temperature_validator() {
let validator = TemperatureValidator::new(-50.0, 50.0);
let mut data = HashMap::new();
data.insert("temperature".to_string(), serde_json::Value::Number(serde_json::Number::from(25)));
let result = validator.validate(&data);
assert!(result.is_ok());
data.insert("temperature".to_string(), serde_json::Value::Number(serde_json::Number::from(100)));
let result = validator.validate(&data);
assert!(result.is_err());
}
#[test]
fn test_pressure_validator() {
let validator = PressureValidator::new(0.0, 200000.0);
let mut data = HashMap::new();
data.insert("pressure".to_string(), serde_json::Value::Number(serde_json::Number::from(101325)));
let result = validator.validate(&data);
assert!(result.is_ok());
}
#[test]
fn test_composite_validator() {
let mut validator = CompositeValidator::new();
validator.add_validator(Box::new(TemperatureValidator::new(-50.0, 50.0)));
validator.add_validator(Box::new(PressureValidator::new(0.0, 200000.0)));
let mut data = HashMap::new();
data.insert("temperature".to_string(), serde_json::Value::Number(serde_json::Number::from(25)));
data.insert("pressure".to_string(), serde_json::Value::Number(serde_json::Number::from(101325)));
let result = validator.validate(&data);
assert!(result.is_ok());
}
}
TL;DR Runbook¶
Quick Start¶
// 1. Stream processing
let (input_tx, input_rx) = mpsc::channel(10);
let (output_tx, output_rx) = mpsc::channel(10);
let processor = StreamProcessor::new(input_rx, output_tx, 100, Duration::from_secs(1));
// 2. Batch processing
let processor = BatchProcessor::new(1000, 4);
processor.add_data(record).await;
let results = processor.process_all().await?;
// 3. Data validation
let mut validator = CompositeValidator::new();
validator.add_validator(Box::new(TemperatureValidator::new(-50.0, 50.0)));
validator.validate(&data)?;
Essential Patterns¶
// Complete data processing setup
pub fn setup_rust_data_processing() {
// 1. Stream processing
// 2. Batch processing
// 3. Data validation
// 4. Error handling
// 5. Memory management
// 6. Parallel processing
// 7. Data transformation
// 8. Performance optimization
println!("Rust data processing setup complete!");
}
This guide provides the complete machinery for Rust data processing. Each pattern includes implementation examples, data processing strategies, and real-world usage patterns for enterprise data pipelines.