Duroxide: Durable Code Execution Framework for Rust

repository·main·Indexed 41 days ago

https://github.com/microsoft/duroxide

A lightweight, embeddable durable execution runtime for Rust (v0.1.30) that allows async code to survive process crashes, restarts, and deployments by recording orchestration steps. It features a strict separation between deterministic orchestrations and non-deterministic activities, supporting patterns like function chaining, the Saga pattern for compensation, and sub-orchestrations.

Tokens
284K
Snippets
611
Records
960
Agent score
53%

What's inside Duroxide

  1. What is Duroxide?

    main
    Duroxide is a framework for building reliable, long-running workflows that can survive failures and restarts. It allows you to write asynchronous Rust code that can run for hours, days, or indefinitely by using deterministic replay and durable futures to maintain state across restarts.
  2. Profiling configuration and resources

    main

    The profiling setup consists of the following components:

    • Dockerfile: Contains the environment with Rust, perf, and flamegraph installed.
    • docker-compose.yml: Provides an optional, simplified orchestration for the profiling environment.
    • results/: The directory where generated SVG flamegraphs are stored (this directory is gitignored).
    • RESULTS.md: A document containing the latest performance optimization results.

    For LLM Users: If you are using an AI agent to analyze performance, refer to prompts/duroxide-profile.md for specific instructions on profiling and analysis guidance.

  3. What is Durable Execution in Duroxide

    main

    Durable execution allows your code to continue running through process crashes, restarts, and machine failures. It provides the illusion that code never stopped, even if it takes days or weeks to complete.

    To achieve this, Duroxide uses a replay engine. For this to work, your orchestration code must be deterministic: given the same history of events, it must always make the same decisions. This allows Duroxide to recreate the state of your code by replaying the execution history after a restart.

  4. What is a Provider in Duroxide?

    main

    A Provider is a storage backend used by Duroxide to persist orchestration state. It acts as a database adapter with specific semantics for durable execution.

    Responsibilities of the Provider:

    • Storing event history: Maintaining an append-only log of all orchestration events.
    • Managing work queues: Handling pending work items.
    • Providing atomic operations: Ensuring consistency during orchestration turns (e.g., appending history and enqueuing new work in a single transaction).

    What the Provider is NOT:

    • It does not understand event meanings or workflow logic.
    • It does not make orchestration decisions.
    • It does not generate IDs or timestamps.

    Key Principle: The provider is "dumb storage." All orchestration logic is contained within the runtime.

  5. What is Duroxide and how is it structured?

    main

    Duroxide is a durable execution framework for Rust designed to build async workflows that survive crashes, restarts, and failures. It achieves this through automatic replay, state persistence, and recovery.

    The ecosystem is organized into three distinct layers:

    1. Core Framework (duroxide): The foundation providing the runtime, replay engine, a bundled SQLite provider, and the Client API.
    2. Providers: Storage backends that implement the Provider trait. This allows you to swap storage (e.g., from SQLite to PostgreSQL) without changing your application logic.
    3. Applications: End-user applications built using the framework (e.g., toygres).
  6. Overview of the Lightweight Filesystem Provider

    main

    The Lightweight Filesystem Provider is a specialized Duroxide provider designed for environments where minimal code size, low memory footprint, and transactional safety are critical. It is optimized for single-runtime process ownership of the data directory.

    Use Cases

    • Embedded/edge deployments
    • Development and testing
    • Single-node deployments with low throughput
    • Environments where SQLite is considered too heavy

    Key Constraints and Design Assumptions

    • Single-Runtime Guarantee: The design assumes a single runtime process owns the data directory exclusively. This allows for in-memory locking, which resets automatically on restart, eliminating the need for complex file-based locks.
    • Concurrency: This provider is not intended for multi-runtime, multi-process, or multi-node distributed deployments. For high-throughput or multi-process scenarios, use SQLite or PostgreSQL instead.
  7. Understand the difference between Deletion and Pruning

    main

    The Management API distinguishes between removing an entire orchestration and trimming its history using two distinct concepts:

    • Deletion (delete_instance, delete_instance_bulk): Permanently removes an orchestration instance and ALL its associated data (executions, history events, and queue messages). Use this to clean up terminated or failed orchestrations.
    • Pruning (prune_executions, prune_executions_bulk): Removes old executions from a long-running orchestration (e.g., ContinueAsNew chains) while keeping the instance alive. Use this to manage storage growth for active, long-running orchestrations without stopping them.
  8. Understand error classification and the ProviderError structure

    main

    Duroxide uses structured error types including ProviderError, ErrorDetails, and ClientError.

    Note on Error Sources: In current versions, ProviderError may store the operation and message but might not preserve the full underlying error source chain (i.e., std::error::Error::source() may return None). When debugging, you may need to rely on the flattened message string.

    Note on Error Classification: Some parts of the system (like the SQLite provider or client delete paths) may rely on substring matching in error messages (e.g., checking if a message contains "not found" or "database is locked") to classify errors. This behavior is implementation-dependent and may change between library versions.

  9. Understand activity cancellation semantics via queue-row flags

    main

    Duroxide implements activity cancellation by using a cancel_requested flag directly on the activity's row in the worker queue. This replaces the previous ExecutionState model. The behavior depends on when the cancellation is requested:

    • Before activity starts: The worker dequeues the ActivityExecute item, sees cancel_requested=true, and immediately acks the work item without executing it.
    • While activity is running: The worker's renew loop observes cancel_requested=true. The worker signals the cancellation token to the activity handler. After a grace period, the worker aborts and acks.
    • After completion but before worker ack: The worker acks normally. The orchestration replay engine filters the completion if it is determined to be stale (using is_completion_already_in_history, cancelled_source_ids, or execution ID mismatch).
    • After completion is enqueued to orchestrator: The cancellation flag update is idempotent and benign; the orchestration will process the completion normally.

    Note on Retries: The system assumes one worker_queue row per scheduled activity ID. Retries occur via re-delivery of the same row. Cancellation of a scheduled activity ID cancels all its attempts.

  10. How `durable_async!` handles cancellation and scoping

    main

    The durable_async! macro implements an 'inline task scope' model.

    1. Scope Creation: When the macro is invoked, it calls ctx.begin_scope() to generate a unique scope_id and pushes it onto the OrchestrationContext scope stack.
    2. Activity Tracking: Every activity scheduled within the durable_async! block is automatically registered to the current scope_id via ctx.register_activity_in_scope(event_id).
    3. Automatic Cancellation: If the DurableFuture created by the macro is a 'loser' in a select2() operation, the orchestration engine retrieves all activity IDs associated with that scope_id and marks them as cancelled. This ensures that a group of sequential steps is cleaned up atomically if the group is no longer needed.
  11. Understand session migration behavior in heterogeneous clusters

    main

    In a cluster with multiple runtimes, settings are applied locally to each runtime. There is no global coordination for session capacity.

    When a session migrates from Worker A to Worker B (e.g., because Worker A's lock expired), the session adopts the lock and idle characteristics of Worker B going forward. This means the session's lifecycle is governed by the settings of the worker currently holding it.

  12. Understand Force Delete behavior

    main

    Setting force=true in delete_instance performs a database-only deletion. It does not send signals to running code or kill active Tokio tasks.

    Key behaviors:

    • Running Orchestrations: The database records are removed, but the orchestration turn will continue executing in memory. When it tries to persist its result, it will fail because the instance no longer exists.
    • Running Activities: If a worker has a lock, the worker's next lock renewal will fail, which acts as a cancellation signal via ctx.is_cancelled(). Well-behaved activities should check this signal to terminate gracefully.
    • Queued Items: Timers and pending queue messages for the instance are removed.
    • Zombie Prevention: A force delete must delete the entry in instance_locks to prevent a race condition where a completing turn recreates the deleted instance.