Skip to content

Zart is in active development — breaking API changes may occur despite our best efforts to keep contracts stable.

Retry Simulation

This example simulates a transient failure to show how Zart automatically retries steps. The first attempt always fails; the framework retries and the second attempt succeeds. All retry metadata is accessible inside the step via zart::context().

Features demonstrated: #[zart_step] with retry = "fixed(n, delay)", zart::context().current_attempt, zart::context().is_retry(), zart::context().max_retries, DurableExecution trait.

#[derive(Debug, Clone, Serialize, Deserialize)]
struct RetrySimulationInput {
name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RetrySimulationOutput {
name: String,
total_attempts: usize,
message: String,
attempts_log: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RetryStepResult {
message: String,
attempt_number: usize,
}
use zart::prelude::*;
use zart::zart_step;
/// Fails on attempt 0; succeeds on any retry.
/// `retry = "fixed(3, 1s)"` means up to 3 retries, 1 second apart.
#[zart_step("intentional-failure", retry = "fixed(3, 1s)")]
async fn intentional_failure_step(name: String) -> Result<RetryStepResult, StepError> {
let ctx = zart::context();
println!(
"[intentional-failure] Attempt #{} | is_retry={} | max_retries={:?}",
ctx.current_attempt,
ctx.is_retry(),
ctx.max_retries,
);
if ctx.current_attempt == 0 {
return Err(StepError::Failed {
step: "intentional-failure".into(),
reason: format!("Simulated transient error on attempt {}", ctx.current_attempt + 1),
});
}
Ok(RetryStepResult {
message: format!("Succeeded for '{}' on retry attempt #{}", name, ctx.current_attempt),
attempt_number: ctx.current_attempt,
})
}
/// A step that always succeeds — no retry configuration needed.
#[zart_step("normal-step")]
async fn normal_step(name: String) -> Result<String, StepError> {
Ok(format!("Normal step completed for '{}'", name))
}
struct RetrySimulationTask;
#[async_trait]
impl DurableExecution for RetrySimulationTask {
type Data = RetrySimulationInput;
type Output = RetrySimulationOutput;
async fn run(&self, data: Self::Data) -> Result<Self::Output, TaskError> {
// Step 1: fails first, then retries automatically.
let result = intentional_failure_step(data.name.clone()).await?;
let mut attempts_log = vec![format!(
"intentional-failure: succeeded on attempt #{} ({} retries)",
result.attempt_number, result.attempt_number
)];
// Step 2: always succeeds on the first try.
let normal = normal_step(data.name.clone()).await?;
attempts_log.push(normal);
Ok(RetrySimulationOutput {
name: data.name,
total_attempts: result.attempt_number + 1,
message: format!(
"Completed after {} attempt(s), succeeded on retry #{}",
result.attempt_number + 1,
result.attempt_number
),
attempts_log,
})
}
}
let mut registry = TaskRegistry::new();
registry.register("retry-simulation", RetrySimulationTask);
let registry = Arc::new(registry);
let durable = DurableScheduler::new(sched.clone());
durable
.start_for::<RetrySimulationTask>(&execution_id, "retry-simulation", &RetrySimulationInput {
name: "retry-demo".into(),
})
.await?;
let output = durable.wait_for::<RetrySimulationTask>(&execution_id, Duration::from_secs(60)).await?;
println!("{}", output.message);
=== Zart Retry Simulation Example ===
Starting execution 'retry-sim-...'...
[intentional-failure] Attempt #0 | is_retry=false | max_retries=Some(3)
Simulated transient failure for 'retry-demo' on attempt #0
[intentional-failure] Attempt #1 | is_retry=true | max_retries=Some(3)
Succeeded for 'retry-demo' on retry attempt #1
[normal-step] Running (no retries needed)
=== Execution Completed ===
Name: retry-demo
Total attempts: 2
Message: Completed after 2 attempt(s), succeeded on retry #1

retry = "fixed(n, delay)" — configures up to n retries with a constant delay between attempts. Use "exponential(n, initial)" for doubling delays.

zart::context().current_attempt — zero-indexed attempt counter available inside every step. 0 on the first run, 1 on the first retry, and so on.

zart::context().is_retry() — returns true if current_attempt > 0. Useful for conditional logic such as skipping expensive setup on retries.

zart::context().max_retries — returns the configured retry limit so the step can adapt its behavior as retries are exhausted.

See ZartStep — Retry Configuration for the full retry API reference.