Configuring Tasks
Retries, per-attempt timeouts, and circuit breakers via TaskConfig.
Every Task can carry a TaskConfig that controls how it retries, how long
each attempt may run, and whether a circuit breaker guards it.
Configuration & Retries
Tasks can be configured with retry strategies to handle transient failures.
The TaskConfig struct allows you to specify the retry behavior.
Retry Strategy Examples
Retry with backoff between attempts
Fixed Retry
Retry a fixed number of times with a constant delay between attempts.
TaskConfig::default()
.with_fixed_retry(3, Duration::from_secs(1))
Exponential Backoff
Retry with exponentially increasing delays, useful for rate-limited APIs.
TaskConfig::default()
.with_exponential_retry(5)
Minimal Config
Fast execution with minimal retry overhead for reliable operations.
TaskConfig::minimal()
Per-Attempt Timeout
Bound each attempt with a fresh deadline. Composes with any retry mode.
TaskConfig::default()
.with_exponential_retry(3)
.with_attempt_timeout(Duration::from_secs(2))
How attempt timeouts compose with retries
When attempt_timeout is set, each attempt inside run_with_retries is wrapped in
tokio::time::timeout. An expired attempt produces a CanoError::Timeout, which is
fed through the same retry path as any other failure — so the configured RetryMode decides
whether to retry. The deadline resets on every attempt, and retry exhaustion still surfaces as
CanoError::RetryExhausted wrapping the underlying timeout context.
Wiring a Circuit Breaker
A CircuitBreaker can be attached to a task's config via
TaskConfig::with_circuit_breaker(Arc::clone(&breaker)). The retry loop consults it
before each attempt; an open breaker short-circuits the whole loop with
CanoError::CircuitOpen (returned raw, not wrapped in RetryExhausted), so a
dependency that is already down is not hammered. Share one Arc<CircuitBreaker>
across every task that hits the same dependency so they trip together.
fn build_config(breaker: Arc<CircuitBreaker>) -> TaskConfig {
TaskConfig::default()
.with_exponential_retry(3)
.with_circuit_breaker(breaker)
}
The breaker itself — its Closed → Open { until } → HalfOpen state machine,
CircuitPolicy, the lazy Open → HalfOpen transition, and the manual
try_acquire / record_success / record_failure RAII API — is
documented in the Resilience guide.
Real-World Example: API Client with Retry
use cano::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum State { Call, Complete }
#[derive(Clone)]
struct ApiClientTask {
endpoint: String,
}
#[task(state = State)]
impl ApiClientTask {
fn config(&self) -> TaskConfig {
// Exponential backoff for API rate limiting
TaskConfig::default()
.with_exponential_retry(5)
}
async fn run(&self, res: &Resources) -> Result<TaskResult<State>, CanoError> {
println!("📡 Calling API: {}", self.endpoint);
let store = res.get::<MemoryStore, _>("store")?;
// Replace this with your HTTP client of choice (reqwest, hyper, etc.)
let data = String::new();
store.put("api_response", data)?;
println!("✅ API call successful");
Ok(TaskResult::Single(State::Complete))
}
}