Durable Task Framework (DTFx)

repository·main·Indexed 23 days ago

https://github.com/azure/durabletask

A C# library for building long-running, persistent workflows using async/await. DTFx allows developers to manage complex orchestration state across restarts and failures using a model based on Task Hubs, Workers, and Clients. It supports multiple backend storage providers including Azure Storage, MSSQL, Netherite, and Service Fabric, and provides features such as automatic retries, timers, sub-orchestrations, and OpenTelemetry-compatible distributed tracing.

Tokens
101K
Snippets
178
Records
295
Agent score
82%

What's inside azure-durabletask

  1. Overview of Durable Task Framework features

    main

    The Durable Task Framework provides several built-in features and patterns for managing long-running, reliable workflows. Key capabilities include:

    • Retries: Automatic retry policies for activities and sub-orchestrations.
    • Timers: Durable delays and scheduling using CreateTimer.
    • External Events: Receiving data from outside sources like webhooks or human interaction.
    • Sub-Orchestrations: Breaking workflows into smaller, reusable pieces.
    • Error Handling: Exception handling, compensation, and recovery patterns.
    • Eternal Orchestrations: Managing long-running workflows using ContinueAsNew.
    • Versioning: Strategies for updating orchestrations safely without breaking existing workflows.
  2. Overview of Durable Task Framework (DTFx)

    main

    The Durable Task Framework (DTFx) is a C# library for writing long-running, persistent workflows (orchestrations) using standard async/await constructs.

    Important Considerations:

    • Support: DTFx is a community-maintained open-source project. It does not come with official Microsoft support. For official support, use Durable Functions (serverless) or Durable Task SDKs with the Durable Task Scheduler backend (self-hosted).
    • Infrastructure: You are responsible for managing the hosting and operational infrastructure.
    • Core Package: The core programming model is provided by the DurableTask.Core NuGet package.
  3. Understand the Durable Task Framework core concepts

    main

    To effectively use the Durable Task Framework, you should understand its fundamental architecture and operational model. The framework is built around three primary pillars: Task Hubs, Workers, and Clients.

    Key concepts include:

    • Orchestrations: Durable workflows that manage the execution logic.
    • Activities: The basic, atomic units of work performed by orchestrations.
    • Replay and Durability: The mechanism using event sourcing to ensure fault tolerance.
    • Deterministic Constraints: Specific rules required when writing orchestration code to ensure reliable replay.

    It is highly recommended to first understand the high-level architecture (Task Hubs, Workers, Clients) and then study the Replay and Durability model to understand why certain deterministic constraints must be followed when writing orchestrations.

  4. Advanced features in Durable Task Framework

    main

    The Durable Task Framework provides several advanced mechanisms for extending and testing orchestration and activity execution:

    • Middleware: Use middleware to intercept and extend the execution of orchestrations and activities, allowing you to implement cross-cutting concerns (e.g., logging, telemetry, error handling).
    • Serialization: Implement custom data converters and define specific serialization patterns for data passing between tasks.
    • Testing: Utilize specialized techniques for unit testing activities and performing integration testing using the emulator.
    • Entities: While the framework supports the concept of Durable Entities, they are not supported for direct use within the core Durable Task Framework (DTFx).
  5. Use the MSSQL Provider for orchestration state

    main

    The MSSQL provider allows you to use Microsoft SQL Server or Azure SQL Database as the backend storage for orchestration state. This provider is compatible with both the Durable Task Framework (DTFx) and Azure Durable Functions.

    Key Features:

    • Supports on-premises SQL Server and Azure SQL.
    • Includes database migrations for schema management.
    • Enables querying orchestration state using standard SQL tools.
    • Provides transactional guarantees of a relational database.
  6. Current capabilities and limitations of the Service Fabric provider

    main

    The Service Fabric provider currently has the following functional status:

    Supported Features

    • Scheduled Tasks
    • Timers
    • Sub-orchestrations

    Limitations of the Fabric-based Instance Store

    • No history event support: History events are not currently supported.
    • State retention: Only the latest state for a given Orchestration instance is stored; previous state changes are overwritten. This state is kept for a 24-hour window.
    • Query constraints: You can only query state for Orchestrations that are currently running/pending, or those that completed within the last hour.
    • Cleanup: State for orchestrations completed more than a day ago is automatically cleaned up.
    • No Dead Letter support: Bad activities or sessions will remain in the system indefinitely.
    • No automatic lock expiry: Activities or orchestrations fetched from the store that are neither completed nor abandoned do not have automatic lock expiry.
  7. Implement FIFO Job Queues using Eternal Orchestrations

    main

    You can implement FIFO (First-In-First-Out) job queues by using one orchestration instance per logical queue. Because an orchestration executes its logic sequentially, it naturally serializes jobs.

    Key Implementation Details:

    • Serialization: Only one job per queue is active at a time. Different queues run in parallel.
    • Buffering: Incoming jobs are buffered using the External Events pattern.
    • Persistence: Use ContinueAsNew to carry unprocessed jobs (a backlog) forward to the next generation. This ensures that when history is reset, no jobs are lost.
    • Concurrency Control: To prevent history from growing indefinitely, limit the number of items processed in a single generation (e.g., using a MaxUpdatesPerGeneration constant) and pass the remaining items in the Backlog via ContinueAsNew.
    // One instance per resource ID applies each update to a primary store then a replica,
    // strictly in FIFO order. Expected event: "Enqueue" (ResourceUpdate).
    // The 4-type-parameter base declares ResourceUpdate as the event payload type, so the
    // framework deserializes incoming events and passes OnEvent a typed value.
    public class ResourceUpdateQueueOrchestration : TaskOrchestration<object, QueueState, ResourceUpdate, string>
    {
        const int MaxUpdatesPerGeneration = 100;  // Max updates processed per generation
    
        // Rebuilt deterministically on replay, since OnEvent replays in original enqueue order.
        readonly Queue<ResourceUpdate> inbox = new Queue<ResourceUpdate>();
        TaskCompletionSource<bool> newWork;  // Wakes RunTask when an update arrives while parked.
    
        public override async Task<object> RunTask(OrchestrationContext context, QueueState state)
        {
            // Re-seed with updates carried over from the previous generation (oldest first, so
            // they stay ahead of any newly arriving events and FIFO order is preserved).
            foreach (ResourceUpdate update in state?.Backlog ?? Enumerable.Empty<ResourceUpdate>())
            {
                this.inbox.Enqueue(update);
            }
    
            // Block until the first update arrives.
            if (this.inbox.Count == 0)
            {
                this.newWork = new TaskCompletionSource<bool>();
                await this.newWork.Task;
                this.newWork = null;
            }
    
            // Drain in FIFO order. Limit the number processed per generation to bound history size; if there are more, we'll pick up the rest in the next generation.
            for (int processed = 0; this.inbox.Count > 0 && processed < MaxUpdatesPerGeneration; processed++)
            {
                ResourceUpdate next = this.inbox.Dequeue();
                await context.ScheduleTask<bool>(typeof(UpdatePrimaryStoreActivity), next);
                await context.ScheduleTask<bool>(typeof(UpdateReplicaStoreActivity), next);
            }
    
            // Reset history, carrying any updates that arrived while we were busy.
            context.ContinueAsNew(new QueueState { Backlog = this.inbox.ToArray() });
            return null;
        }
    
        public override void OnEvent(OrchestrationContext context, string name, ResourceUpdate input)
        {
            if (name == "Enqueue")
            {
                this.inbox.Enqueue(input);
                this.newWork?.TrySetResult(true);
            }
        }
    }
    
    public class QueueState
    {
        // Updates carried over from the previous generation, oldest first.
        public ResourceUpdate[] Backlog { get; set; } = Array.Empty<ResourceUpdate>();
    }
  8. Best practices for testing orchestrations

    main

    When testing Durable Task orchestrations, follow these best practices:

    1. Use the Emulator for Speed: Use LocalOrchestrationService for most unit and integration tests. Reserve AzureStorageOrchestrationService for slow, end-to-end tests.
    2. Test Determinism: Run the same orchestration multiple times with the same input to ensure the output remains consistent.
    3. Test Edge Cases: Explicitly test how your orchestrations handle null inputs or empty collections (e.g., List<T>).
    4. Isolate Tests: Ensure each test uses a fresh LocalOrchestrationService instance in the [TestInitialize] phase to prevent state leakage between tests.
  9. Migrate between Durable Task Framework providers

    main

    Because each provider uses a different storage format for orchestration state, there is no built-in tool for automatic state migration. To move from one provider to another, you must follow these steps:

    1. Complete or terminate all currently running orchestrations in the old provider.
    2. Reconfigure your application to use the new provider.
    3. Restart orchestrations from the beginning (they cannot resume from their previous state in the new provider).
  10. How partitioning works in Azure Storage Provider

    main

    The Azure Storage provider uses partitions to distribute orchestration workloads across workers.

    • Assignment: Orchestrations and entities are assigned to partitions by hashing the InstanceID. A single orchestration instance is always processed by exactly one partition (and one worker) at a time.
    • Control Queues: The PartitionCount (1–16, default 4) determines how many control queues are created (e.g., {taskhub}-control-0, {taskhub}-control-1). Each queue is owned by exactly one worker.
    • Work Item Queue: A single, shared queue ({taskhub}-workitems) is used for activity function messages. All workers compete for messages in this queue.
    • Scaling: The maximum number of workers that can process orchestrations concurrently is limited by the PartitionCount.
    IMPORTANT

    The PartitionCount cannot be changed after the task hub is created. Set it high enough for future scale-out needs, but note that higher counts increase Azure Storage costs.

  11. How to use Durable Entities

    main

    Durable Entities are not supported for direct use within the core Durable Task Framework (DTFx). To use Durable Entities, you should use one of the following supported implementations:

    1. Azure Durable Functions: For a managed service experience.
    2. Durable Task SDK: Combined with the Durable Task Scheduler.
  12. Implement the Saga Pattern for Compensation

    main

    The Saga pattern is used to maintain consistency by compensating (undoing) previous successful steps when a subsequent step fails.

    Pattern Workflow:

    1. Track completed steps in a list or state variable.
    2. Wrap activity calls in a try-catch block.
    3. In the catch block, iterate through the completed steps in reverse order and execute corresponding compensation activities (e.g., if 'Charge Payment' succeeded but 'Ship Order' failed, run 'Refund Payment').
    public override async Task<OrderResult> RunTask(OrchestrationContext context, OrderInput input)
    {
        var completedSteps = new List<string>();
        
        try
        {
            await context.ScheduleTask<bool>(typeof(ReserveInventoryActivity), input);
            completedSteps.Add("inventory");
            
            await context.ScheduleTask<bool>(typeof(ChargePaymentActivity), input);
            completedSteps.Add("payment");
            
            await context.ScheduleTask<bool>(typeof(ShipOrderActivity), input);
            return new OrderResult { Success = true };
        }
        catch (TaskFailedException ex)
        {
            // Compensate in reverse order
            if (completedSteps.Contains("payment"))
            {
                await context.ScheduleTask<bool>(typeof(RefundPaymentActivity), input);
            }
            
            if (completedSteps.Contains("inventory"))
            {
                await context.ScheduleTask<bool>(typeof(ReleaseInventoryActivity), input);
            }
            
            return new OrderResult { Success = false, Error = ex.InnerException?.Message };
        }
    }