Error Handling & Retry
Ichika provides robust error handling with built-in retry semantics for transient failures.
Error Propagation
Errors naturally flow through the pipeline using Result types:
#![allow(unused)]
fn main() {
let pool = pipe![
|req: String| -> anyhow::Result<i32> {
req.parse().map_err(Into::into)
},
|req: anyhow::Result<i32>| -> anyhow::Result<i32> {
let n = req?;
Ok(n * 2)
},
|req: anyhow::Result<i32>| -> String {
match req {
Ok(n) => format!("Result: {}", n),
Err(e) => format!("Error: {}", e),
}
}
]?;
}
Type Transformation
When a stage returns a Result, the next stage receives that Result:
#![allow(unused)]
fn main() {
|req: String| -> anyhow::Result<usize> { ... } // Returns Result
|req: anyhow::Result<usize>| -> usize { // Receives Result
req.unwrap()
}
}
Retry Semantics
Ichika provides automatic retry for operations that may fail transiently.
Basic Retry
Use the retry function to retry an operation:
#![allow(unused)]
fn main() {
use ichika::retry;
let result = retry(|| {
// Operation that might fail
Ok::<_, anyhow::Error>(42)
})?;
}
Retry with Policy
Control retry behavior with a RetryPolicy:
#![allow(unused)]
fn main() {
use ichika::{retry_with, RetryPolicy};
use std::time::Duration;
let policy = RetryPolicy {
max_attempts: 3,
backoff: Duration::from_millis(100),
..Default::default()
};
let result = retry_with(policy, || {
// Operation with custom retry policy
Ok::<_, anyhow::Error>(42)
})?;
}
RetryPolicy Options
#![allow(unused)]
fn main() {
pub struct RetryPolicy {
/// Maximum number of retry attempts
pub max_attempts: usize,
/// Initial backoff duration (exponential backoff is applied)
pub backoff: Duration,
/// Maximum backoff duration
pub max_backoff: Duration,
/// Whether to use jitter in backoff calculation
pub jitter: bool,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 3,
backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(30),
jitter: true,
}
}
}
}
Using Retry in Pipelines
Retry Within a Stage
#![allow(unused)]
fn main() {
let pool = pipe![
#[name("fetch")]
|req: String| -> anyhow::Result<String> {
// Retry the fetch operation
retry_with(
RetryPolicy {
max_attempts: 3,
backoff: Duration::from_millis(100),
..Default::default()
},
|| {
// Simulated fetch that might fail
if rand::random::<f32>() < 0.3 {
Err(anyhow::anyhow!("Network error"))
} else {
Ok(format!("Fetched: {}", req))
}
}
)
}
]?;
}
Retry at Pipeline Level
For more control, handle retry at the caller level:
#![allow(unused)]
fn main() {
fn process_with_retry(pool: &impl ThreadPool<Request = String, Response = String>, input: String) -> anyhow::Result<String> {
retry_with(
RetryPolicy {
max_attempts: 5,
backoff: Duration::from_millis(50),
..Default::default()
},
|| {
pool.send(input.clone())?;
match pool.recv()? {
Some(result) => Ok(result),
None => Err(anyhow::anyhow!("Pipeline terminated")),
}
}
)
}
}
Error Recovery Strategies
Fallback Values
#![allow(unused)]
fn main() {
let pool = pipe![
|req: String| -> anyhow::Result<i32> {
req.parse().map_err(Into::into)
},
|req: anyhow::Result<i32>| -> i32 {
req.unwrap_or(0) // Default to 0 on error
}
]?;
}
Error Aggregation
#![allow(unused)]
fn main() {
let pool = pipe![
|req: Vec<String>| -> Vec<anyhow::Result<i32>> {
req.into_iter()
.map(|s| s.parse::<i32>().map_err(Into::into))
.collect()
},
|req: Vec<anyhow::Result<i32>>| -> (i32, usize) {
let (sum, errors) = req.into_iter().fold(
(0, 0),
|(sum, errs), r| match r {
Ok(n) => (sum + n, errs),
Err(_) => (sum, errs + 1),
},
);
(sum, errors)
}
]?;
}
Circuit Breaker Pattern
#![allow(unused)]
fn main() {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
let circuit_breaker = Arc::new(AtomicBool::new(false));
let pool = pipe![
|req: String| -> anyhow::Result<String> {
if circuit_breaker.load(Ordering::Relaxed) {
return Err(anyhow::anyhow!("Circuit breaker is open"));
}
// Process request
Ok(format!("Processed: {}", req))
}
]?;
}
Complete Example
Here’s a comprehensive example showing error handling with retry:
use ichika::prelude::*;
use ichika::{retry_with, RetryPolicy};
use std::time::Duration;
fn main() -> anyhow::Result<()> {
let pool = pipe![
#[name("validate")]
|req: String| -> anyhow::Result<i32> {
req.parse()
.map_err(|e| anyhow::anyhow!("Invalid input: {}", e))
},
#[name("process")]
|req: anyhow::Result<i32>| -> anyhow::Result<i32> {
let n = req?;
// Simulate transient failure
if n % 3 == 0 {
Err(anyhow::anyhow!("Transient error"))
} else {
Ok(n * 2)
}
},
#[name("format")]
|req: anyhow::Result<i32>| -> String {
match req {
Ok(n) => format!("Success: {}", n),
Err(e) => format!("Failed: {}", e),
}
}
]?;
// Send various inputs
let inputs = vec!["10", "20", "30", "invalid", "40"];
for input in inputs {
pool.send(input.to_string())?;
}
// Collect results
loop {
match pool.recv()? {
Some(result) => println!("{}", result),
None => break,
}
}
Ok(())
}
Best Practices
- Use
anyhow::Resultfor flexible error handling - Set appropriate retry limits to avoid infinite loops
- Use exponential backoff for network operations
- Log errors appropriately for debugging
- Consider circuit breakers for external service calls
- Make errors informative - include context about what failed