Editja

Ixxerja permezz ta’


Durable orchestrations

Durable Functions is an extension of Azure Functions that provides a way to run stateful functions in a serverless compute environment. Within a durable function app, you can use an orchestrator function to orchestrate the execution of other durable functions. Orchestrator functions have the following characteristics:

  • They define function workflows by using procedural code. No declarative schemas or designers are needed.
  • They can call other durable functions synchronously and asynchronously. Output from called functions can be saved to local variables.
  • They're designed to be durable and reliable. Execution progress is automatically saved as a checkpoint when the function calls an await or yield operator. Local state isn't lost when the process recycles or the virtual machine reboots.
  • They can be long running. The total lifespan of an orchestration instance can be seconds, days, or months, or the instance can be configured to never end.

This article gives you an overview of orchestrator functions and how they can help you solve various app development challenges. For information about the types of functions available in a Durable Functions app, see Durable Functions types and features.

Orchestration identity

Each instance of an orchestration has an instance identifier, also known as an instance ID. By default, each instance ID is an autogenerated globally unique identifier (GUID). However, instance IDs can also be any user-generated string value. Each orchestration instance ID must be unique within a task hub.

The following rules apply to instance IDs:

  • They must be between 1 and 100 characters.
  • They must not start with @.
  • They must not contain /, \, #, or ? characters.
  • They must not contain control characters.

Note

We generally recommend that you use autogenerated instance IDs whenever possible. User-generated instance IDs are intended for scenarios where there's a one-to-one mapping between an orchestration instance and an external application-specific entity, like a purchase order or a document.

Also, the actual enforcement of character restriction rules can vary depending on the storage provider that the app uses. To help ensure correct behavior and compatibility, we strongly recommend that you follow the preceding instance ID rules.

An orchestration's instance ID is a required parameter for most instance management operations. Instance IDs are also important for diagnostics. For instance, you use them when you search through orchestration tracking data in Application Insights for troubleshooting or analytics purposes. For this reason, we recommend that you save generated instance IDs to an external location that makes it easy to reference them later. Examples of locations include databases or application logs.

Reliability

Orchestrator functions use the event sourcing design pattern to help maintain their execution state reliably. Instead of directly storing the current state of an orchestration, the Durable Task Framework uses an append-only store to record the full series of actions the function orchestration takes. An append-only store has many benefits compared to dumping the full runtime state. Benefits include increased performance, scalability, and responsiveness. You also get eventual consistency for transactional data and full audit trails and history. The audit trails support reliable compensating actions.

Durable Functions uses event sourcing transparently. Behind the scenes, an orchestrator function uses an await operator in C# and a yield operator in JavaScript and Python. These operators yield control of the orchestrator thread back to the Durable Task Framework dispatcher. In Java functions, there's no special language keyword. Instead, calling .await() on a task yields control back to the dispatcher via a custom instance of Throwable. The dispatcher then commits any new actions scheduled by the orchestrator function to storage. Examples of actions include calling one or more child functions or scheduling a durable timer. The transparent commit action updates the execution history of the orchestration instance by appending all new events into storage, much like an append-only log. Similarly, the commit action creates messages in storage to schedule the actual work. At this point, the orchestrator function can be unloaded from memory. By default, Durable Functions uses Azure Storage as its runtime state store, but other storage providers are also supported.

When an orchestration function is given more work to do (for example, a response message is received or a durable timer expires), the orchestrator wakes up and re-executes the entire function from the start to rebuild the local state. During the replay, if the code tries to call a function (or do any other asynchronous work), the Durable Task Framework consults the execution history of the current orchestration. If it finds that the activity function has already executed and yielded a result, it replays that function's result, and the orchestrator code continues to run. Replay continues until the function code is finished or until it schedules new asynchronous work.

Note

To help the replay pattern work correctly and reliably, orchestrator function code must be deterministic. Nondeterministic orchestrator code can result in runtime errors or other unexpected behavior. For more information about code restrictions for orchestrator functions, see Orchestrator function code constraints.

Note

If an orchestrator function emits log messages, the replay behavior can cause duplicate log messages to be emitted. For more information about why this behavior occurs and how to work around it, see App logging.

Orchestration history

The event-sourcing behavior of the Durable Task Framework is closely coupled with the orchestrator function code you write. Suppose you have an activity-chaining orchestrator function, like the following orchestrator function.

Note

Version 4 of the Node.js programming model for Azure Functions is generally available. The v4 model is designed to provide a more flexible and intuitive experience for JavaScript and TypeScript developers. For more information about the differences between v3 and v4, see the migration guide.

In the following code snippets, JavaScript (PM4) denotes programming model v4, the new experience.

[FunctionName("HelloCities")]
public static async Task<List<string>> Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var outputs = new List<string>();

    outputs.Add(await context.CallActivityAsync<string>("SayHello", "Tokyo"));
    outputs.Add(await context.CallActivityAsync<string>("SayHello", "Seattle"));
    outputs.Add(await context.CallActivityAsync<string>("SayHello", "London"));

    // Return ["Hello Tokyo!", "Hello Seattle!", "Hello London!"].
    return outputs;
}

Whenever an activity function is scheduled, the Durable Task Framework saves the execution state of the function at various checkpoints. At each checkpoint, the framework saves the state into a durable storage back end, which is Azure Table Storage by default. This state is referred to as the orchestration history.

History table

Generally speaking, the Durable Task Framework does the following at each checkpoint:

  • Saves the execution history into durable storage.
  • Enqueues messages for functions the orchestrator wants to invoke.
  • Enqueues messages for the orchestrator itself, such as durable timer messages.

When the checkpoint is complete, the orchestrator function is free to be removed from memory until there's more work for it to do.

Note

Azure Storage doesn't provide any transactional guarantees about data consistency between table storage and queues when the data is saved. To handle failures, the Durable Functions Azure Storage provider uses eventual consistency patterns. These patterns help ensure that no data is lost if there's a crash or loss of connectivity in the middle of a checkpoint. Alternate storage providers, such as the Durable Functions Microsoft SQL Server (MSSQL) storage provider, might provide stronger consistency guarantees.

When the function shown earlier finishes, its history looks something like the data in the following table in Table Storage. The entries are abbreviated for illustration purposes.

PartitionKey (InstanceId) EventType Timestamp Input Name Result Status
eaee885b ExecutionStarted 2021-05-05T18:45:28.852Z null HelloCities
eaee885b OrchestratorStarted 2021-05-05T18:45:32.362Z
eaee885b TaskScheduled 2021-05-05T18:45:32.670Z SayHello
eaee885b OrchestratorCompleted 2021-05-05T18:45:32.670Z
eaee885b TaskCompleted 2021-05-05T18:45:34.201Z """Hello Tokyo!"""
eaee885b OrchestratorStarted 2021-05-05T18:45:34.232Z
eaee885b TaskScheduled 2021-05-05T18:45:34.435Z SayHello
eaee885b OrchestratorCompleted 2021-05-05T18:45:34.435Z
eaee885b TaskCompleted 2021-05-05T18:45:34.763Z """Hello Seattle!"""
eaee885b OrchestratorStarted 2021-05-05T18:45:34.857Z
eaee885b TaskScheduled 2021-05-05T18:45:34.857Z SayHello
eaee885b OrchestratorCompleted 2021-05-05T18:45:34.857Z
eaee885b TaskCompleted 2021-05-05T18:45:34.919Z """Hello London!"""
eaee885b OrchestratorStarted 2021-05-05T18:45:35.032Z
eaee885b OrchestratorCompleted 2021-05-05T18:45:35.044Z
eaee885b ExecutionCompleted 2021-05-05T18:45:35.044Z "[""Hello Tokyo!"",""Hello Seattle!"",""Hello London!""]" Completed

The table columns contain the following values:

  • PartitionKey: The instance ID of the orchestration.
  • EventType: The type of the event. For detailed descriptions of all the history event types, see Durable Task Framework History Events.
  • Timestamp: The Coordinated Universal Time timestamp of the history event.
  • Input: The JSON-formatted input of the function.
  • Name: The name of the invoked function.
  • Result: The output of the function, specifically, its return value.

Warning

This table can be useful as a debugging tool. But keep in mind that its format and content might change as the Durable Functions extension evolves.

Every time the function is resumed after waiting for a task to complete, the Durable Task Framework reruns the orchestrator function from scratch. On each rerun, it consults the execution history to determine whether the current asynchronous task is complete. If the execution history shows that the task is already complete, the framework replays the output of that task and moves on to the next task. This process continues until the entire execution history has been replayed. As soon as the current execution history has been replayed, the local variables will have been restored to their previous values.

Features and patterns

The following sections describe the features and patterns of orchestrator functions.

Sub-orchestrations

Orchestrator functions can call activity functions, but also other orchestrator functions. For example, you can build a larger orchestration out of a library of orchestrator functions. Or, you can run multiple instances of an orchestrator function in parallel.

For more information and for examples, see Sub-orchestrations in Durable Functions (Azure Functions).

Durable timers

Orchestrations can schedule durable timers to implement delays or to set up timeout handling on asynchronous actions. Use durable timers in orchestrator functions instead of language-native sleep APIs.

For more information and for examples, see Timers in Durable Functions (Azure Functions).

External events

Orchestrator functions can wait for external events to update an orchestration instance. This Durable Functions feature is often useful for handling human interactions or other external callbacks.

For more information and for examples, see Handling external events in Durable Functions (Azure Functions).

Error handling

Orchestrator functions can use the error-handling features of the programming language. Existing patterns like try/catch are supported in orchestration code.

Orchestrator functions can also add retry policies to the activity or sub-orchestrator functions that they call. If an activity or sub-orchestrator function fails with an exception, the specified retry policy can automatically delay and retry the execution up to a specified number of times.

Note

If there's an unhandled exception in an orchestrator function, the orchestration instance finishes in a Failed state. An orchestration instance can't be retried after it fails.

For more information and for examples, see Handling errors in Durable Functions (Azure Functions).

Critical sections (Durable Functions 2.x, currently .NET only)

Orchestration instances are single-threaded, so race conditions aren't a concern within an orchestration. However, race conditions are possible when orchestrations interact with external systems. To mitigate race conditions when interacting with external systems, orchestrator functions can define critical sections by using a LockAsync method in .NET.

The following sample code shows an orchestrator function that defines a critical section. It uses the LockAsync method to enter the critical section. This method requires passing one or more references to a durable entity, which durably manages the lock state. Only a single instance of this orchestration can execute the code in the critical section at a time.

[FunctionName("Synchronize")]
public static async Task Synchronize(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var lockId = new EntityId("LockEntity", "MyLockIdentifier");
    using (await context.LockAsync(lockId))
    {
        // Critical section. Only one orchestration can enter at a time.
    }
}

The LockAsync method acquires the durable locks and returns an IDisposable that ends the critical section when disposed. This IDisposable result can be used together with a using block to get a syntactic representation of the critical section. When an orchestrator function enters a critical section, only one instance can execute that block of code. Any other instances that try to enter the critical section are blocked until the previous instance exits the critical section.

The critical section feature is also useful for coordinating changes to durable entities. For more information about critical sections, see Entity coordination.

Note

Critical sections are available in Durable Functions 2.0. Currently, only .NET in-process orchestrations implement this feature. Entities and critical sections aren't yet available in Durable Functions for .NET isolated worker orchestrations.

Calls to HTTP endpoints (Durable Functions 2.x)

Orchestrator functions aren't permitted to do I/O operations, as described in Orchestrator function code constraints. The typical workaround for this limitation is to wrap any code that needs to do I/O operations in an activity function. Orchestrations that interact with external systems frequently use activity functions to make HTTP calls and return the results to the orchestration.

To streamline this common pattern, orchestrator functions can use the CallHttpAsync method to invoke HTTP APIs directly.

[FunctionName("CheckSiteAvailable")]
public static async Task CheckSiteAvailable(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    Uri url = context.GetInput<Uri>();

    // Make an HTTP GET request to the specified endpoint.
    DurableHttpResponse response = 
        await context.CallHttpAsync(HttpMethod.Get, url);

    if ((int)response.StatusCode == 400)
    {
        // Handle error codes.
    }
}

Besides supporting basic request/response patterns, the method supports automatic handling of common asynchronous HTTP 202 polling patterns. It also supports authentication with external services by using managed identities.

For more information and for detailed examples, see HTTP features.

Note

Calling HTTP endpoints directly from orchestrator functions is available in Durable Functions 2.0 and later.

Multiple parameters

It isn't possible to pass multiple parameters to an activity function directly. The recommendation is to pass in an array of objects or composite objects.

In .NET, you can also use ValueTuple objects to pass multiple parameters. The following sample uses ValueTuple features added with C# 7:

[FunctionName("GetCourseRecommendations")]
public static async Task<object> RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    string major = "ComputerScience";
    int universityYear = context.GetInput<int>();

    object courseRecommendations = await context.CallActivityAsync<object>(
        "CourseRecommendations",
        (major, universityYear));
    return courseRecommendations;
}

[FunctionName("CourseRecommendations")]
public static async Task<object> Mapper([ActivityTrigger] IDurableActivityContext inputs)
{
    // Parse the input for the student's major and year in university.
    (string Major, int UniversityYear) studentInfo = inputs.GetInput<(string, int)>();

    // Retrieve and return course recommendations by major and university year.
    return new
    {
        major = studentInfo.Major,
        universityYear = studentInfo.UniversityYear,
        recommendedCourses = new []
        {
            "Introduction to .NET Programming",
            "Introduction to Linux",
            "Becoming an Entrepreneur"
        }
    };
}

Next step