Skip to content

Durable Loops

Regular Rust loops are not durable — if your process restarts mid-loop, all progress is lost and the loop starts over. Zart solves this by giving each iteration a unique step name that is persisted to the database.

The key insight is simple: give each iteration a unique step name. Zart checks whether a step has already been completed before calling the closure.

#[zart_durable("batch-processor")]
async fn process_batch(data: BatchData) -> Result<BatchResult> {
// First, fetch items — this step is itself persisted
let items = z_step!("fetch-items", || async {
db.fetch_pending_items().await
}).await?;
// Each iteration gets a unique step name
for (i, item) in items.iter().enumerate() {
z_step!(&format!("process-item-{i}"), || async {
processor.handle(item).await
}).await?;
}
Ok(BatchResult { processed: items.len() })
}

If the process restarts after processing items 0–3, Zart will skip steps "process-item-0" through "process-item-3" (returning their stored results immediately) and resume from "process-item-4".

The z_durable_loop! macro wraps this pattern for you:

use zart_macros::{zart_durable, z_durable_loop};
#[zart_durable("batch-processor")]
async fn process_batch(data: BatchData) -> Result<()> {
let items = z_step!("fetch-items", || async {
db.fetch_pending_items().await
}).await?;
z_durable_loop!("process-item", items, |i, item| async move {
// i is the zero-based iteration index
// step name becomes "process-item-0", "process-item-1", etc.
processor.handle(&item).await
}).await?;
Ok(())
}

Sometimes you need to poll an external service until a condition is met. Use ctx.sleep() between polls to introduce a durable wait:

use zart::{TaskHandler, TaskContext, TaskError, Scheduler};
use zart_macros::z_step;
use std::time::Duration;
#[zart_durable("wait-for-job", timeout = "6h")]
async fn wait_for_job(data: JobData) -> Result<JobResult> {
let mut attempt = 0u32;
loop {
let status = z_step!(&format!("check-status-{attempt}"), || async {
api.get_job_status(&data.job_id).await
}).await?;
if status.is_complete() {
return Ok(JobResult { output: status.output });
}
if status.is_failed() {
return Err(TaskError::custom("Job failed externally"));
}
// Durable wait — process can restart, timer continues
ctx.sleep(Duration::from_secs(300)).await?; // 5 minutes
attempt += 1;
}
}

Collect results across iterations into a final value:

#[zart_durable("transform-records")]
async fn transform_records(data: TransformJob) -> Result<TransformSummary> {
let records = z_step!("fetch-records", || async {
db.get_records(&data.query).await
}).await?;
let mut results = Vec::with_capacity(records.len());
for (i, record) in records.iter().enumerate() {
let output = z_step!(&format!("transform-{i}"), || async {
transformer.apply(record).await
}).await?;
results.push(output);
}
z_step!("write-results", || async {
db.bulk_insert(&results).await
}).await?;
Ok(TransformSummary {
total: records.len(),
written: results.len(),
})
}

For very large datasets, process in chunks to limit memory usage while keeping each chunk’s work durable:

#[zart_durable("process-large-dataset")]
async fn process_large_dataset(data: DatasetJob) -> Result<()> {
let chunk_count = z_step!("get-chunk-count", || async {
db.count_chunks(&data.dataset_id).await
}).await?;
for chunk_idx in 0..chunk_count {
// Fetch and process each chunk durably
z_step!(&format!("process-chunk-{chunk_idx}"), || async {
let chunk = db.get_chunk(&data.dataset_id, chunk_idx).await?;
processor.process_chunk(chunk).await
}).await?;
}
Ok(())
}

Sometimes you want to keep retrying a step until it succeeds, with configurable backoff:

use zart_macros::z_step_with_retry;
#[zart_durable("provision-with-retry")]
async fn provision(data: ProvisionRequest) -> Result<ProvisionResult> {
// Try up to 10 times with exponential backoff capped at 5 minutes
let result = z_step_with_retry!(
"provision-resource",
retries = 10,
backoff = "exponential",
delay = "5s",
max_delay = "5m",
|| async { cloud.provision(&data.config).await }
).await?;
Ok(result)
}

Step names within a single execution must be unique. Here are the rules:

  • Static steps: use a descriptive string like "send-email".
  • Loop iterations: append the index like "process-item-{i}".
  • Nested loops: combine indices like "row-{r}-col-{c}".
  • Conditional branches: use distinct names like "path-a-step" vs "path-b-step".