durable-workflow 0.1.9

First-party Rust SDK for Durable Workflow workers and clients
Documentation
use std::{
    sync::{
        atomic::{AtomicUsize, Ordering},
        Arc,
    },
    time::{Duration, SystemTime, UNIX_EPOCH},
};

use durable_workflow::{
    json, ActivityOptions, ActivityRetryPolicy, Client, Error, Result, Worker,
    WorkflowResultOptions,
};

#[tokio::main]
async fn main() -> Result<()> {
    let server_url = std::env::var("DURABLE_WORKFLOW_SERVER_URL")
        .unwrap_or_else(|_| "http://127.0.0.1:8080".to_string());
    let token = std::env::var("DURABLE_WORKFLOW_TOKEN").ok();
    let task_queue = std::env::var("TASK_QUEUE").unwrap_or_else(|_| "rust-workers".to_string());
    let client = Client::builder(server_url)
        .token(token)
        .namespace("default")
        .build()?;
    let attempts = Arc::new(AtomicUsize::new(0));
    let mut worker = Worker::new(client.clone(), task_queue.clone())
        .worker_id(format!("rust-activity-options-{}", unique_suffix()))
        .poll_timeout(Duration::from_secs(5));

    worker.register_activity("rust.flaky", {
        let attempts = attempts.clone();
        move |ctx, _args| {
            let attempts = attempts.clone();
            async move {
                let attempt = attempts.fetch_add(1, Ordering::SeqCst) + 1;
                ctx.heartbeat(json!({"attempt": attempt})).await?;
                if attempt < 3 {
                    return Err(Error::WorkerLoop(format!("planned failure {attempt}")));
                }
                Ok(json!({"attempt": attempt, "status": "completed"}))
            }
        }
    });
    worker.register_activity("rust.terminal", |_ctx, _args| async move {
        Err(Error::WorkerLoop("planned terminal failure".to_string()))
    });

    let activity_queue = task_queue.clone();
    worker.register_workflow("rust.activity-options", move |ctx, _input| {
        let activity_queue = activity_queue.clone();
        async move {
            let retry = ActivityRetryPolicy::new(3).exponential_backoff(
                Duration::from_secs(1),
                2,
                Some(Duration::from_secs(10)),
            );
            let flaky = ctx
                .activity_with_options(
                    "rust.flaky",
                    ActivityOptions::new()
                        .task_queue(activity_queue.clone())
                        .retry_policy(retry)
                        .start_to_close_timeout(Duration::from_secs(30))
                        .schedule_to_close_timeout(Duration::from_secs(60))
                        .heartbeat_timeout(Duration::from_secs(5)),
                    json!([]),
                )
                .await?;

            let terminal = ctx
                .activity_with_options(
                    "rust.terminal",
                    ActivityOptions::new()
                        .task_queue(activity_queue)
                        .retry_policy(ActivityRetryPolicy::new(1))
                        .start_to_close_timeout(Duration::from_secs(10)),
                    json!([]),
                )
                .await;
            let failure = match terminal {
                Err(Error::ActivityFailed(failure)) => json!({
                    "kind": format!("{:?}", failure.kind),
                    "reason": failure.reason,
                    "category": failure.failure_category,
                    "activity_execution_id": failure.activity_execution_id,
                    "activity_attempt_id": failure.activity_attempt_id,
                    "timeout_kind": failure.timeout_kind,
                }),
                Err(error) => return Err(error),
                Ok(value) => json!({"unexpected_success": value}),
            };
            Ok(json!({"retry_result": flaky, "terminal_failure": failure}))
        }
    });

    worker.register().await?;
    let workflow_id = format!("rust-activity-options-{}", unique_suffix());
    let handle = client
        .start_workflow(
            "rust.activity-options",
            &task_queue,
            &workflow_id,
            json!([]),
        )
        .await?;
    let watcher = handle.clone();
    worker
        .run_until(async move {
            loop {
                if watcher
                    .describe()
                    .await
                    .is_ok_and(|description| description.is_terminal())
                {
                    break;
                }
                tokio::time::sleep(Duration::from_millis(500)).await;
            }
        })
        .await?;

    let result = handle
        .result(WorkflowResultOptions {
            poll_interval: Duration::from_millis(500),
            timeout: Duration::from_secs(90),
        })
        .await?;
    println!("{result}");
    Ok(())
}

fn unique_suffix() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis()
}