Duroxide: Durable Code Execution Framework for Rust
repository·main·Indexed 41 days ago
https://github.com/microsoft/duroxideA 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.
What's inside Duroxide
- 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.
Profiling configuration and resources
mainThe profiling setup consists of the following components:
Dockerfile: Contains the environment with Rust,perf, andflamegraphinstalled.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.mdfor specific instructions on profiling and analysis guidance.What is Durable Execution in Duroxide
mainDurable 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.
What is a Provider in Duroxide?
mainA 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.
What is Duroxide and how is it structured?
mainDuroxide 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:
- Core Framework (
duroxide): The foundation providing the runtime, replay engine, a bundled SQLite provider, and the Client API. - Providers: Storage backends that implement the
Providertrait. This allows you to swap storage (e.g., from SQLite to PostgreSQL) without changing your application logic. - Applications: End-user applications built using the framework (e.g.,
toygres).
- Core Framework (
Overview of the Lightweight Filesystem Provider
mainThe 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.
Understand the difference between Deletion and Pruning
mainThe 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.,ContinueAsNewchains) while keeping the instance alive. Use this to manage storage growth for active, long-running orchestrations without stopping them.
- Deletion (
Understand error classification and the ProviderError structure
mainDuroxide uses structured error types including
ProviderError,ErrorDetails, andClientError.Note on Error Sources: In current versions,
ProviderErrormay store theoperationandmessagebut might not preserve the full underlying error source chain (i.e.,std::error::Error::source()may returnNone). When debugging, you may need to rely on the flattenedmessagestring.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.Understand activity cancellation semantics via queue-row flags
mainDuroxide implements activity cancellation by using a
cancel_requestedflag directly on the activity's row in the worker queue. This replaces the previousExecutionStatemodel. The behavior depends on when the cancellation is requested:- Before activity starts: The worker dequeues the
ActivityExecuteitem, seescancel_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_queuerow per scheduled activity ID. Retries occur via re-delivery of the same row. Cancellation of a scheduled activity ID cancels all its attempts.- Before activity starts: The worker dequeues the
How `durable_async!` handles cancellation and scoping
mainThe
durable_async!macro implements an 'inline task scope' model.- Scope Creation: When the macro is invoked, it calls
ctx.begin_scope()to generate a uniquescope_idand pushes it onto theOrchestrationContextscope stack. - Activity Tracking: Every activity scheduled within the
durable_async!block is automatically registered to the currentscope_idviactx.register_activity_in_scope(event_id). - Automatic Cancellation: If the
DurableFuturecreated by the macro is a 'loser' in aselect2()operation, the orchestration engine retrieves all activity IDs associated with thatscope_idand marks them as cancelled. This ensures that a group of sequential steps is cleaned up atomically if the group is no longer needed.
- Scope Creation: When the macro is invoked, it calls
Understand session migration behavior in heterogeneous clusters
mainIn 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.
Understand Force Delete behavior
mainSetting
force=trueindelete_instanceperforms 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_locksto prevent a race condition where a completing turn recreates the deleted instance.