Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
An orchestrator function orchestrates the execution of other functions as a code-based workflow. 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 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
awaitoryieldoperator. Local state isn't lost when the process recycles or the VM reboots. - They can be long running. The total lifespan of an orchestration instance can be seconds, days, or months, or you can configure the instance to never end.
This article gives you an overview of orchestrator functions and how they can help you solve different app development challenges.
For information about the types of functions available in a Durable Functions app, see Durable Task programming model.
Durable Task SDKs provide the same orchestrator capabilities as Durable Functions, but run as standalone applications backed by the Durable Task Scheduler.
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
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.
Note
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, 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 example, you use them when you search through orchestration tracking data in Application Insights for troubleshooting or analytics purposes. For this reason, save generated instance IDs to an external location that makes it easy to reference them later, like a database or application logs.
An orchestration's instance ID is a required parameter for most instance management operations. Instance IDs are also important for diagnostics, so save generated instance IDs to an external location that makes it easy to reference them later, like a database or application logs.
Reliability
Orchestrator functions use the event sourcing design pattern to 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, full audit trails, and history. The audit trails support reliable compensating actions.
The Durable Task Framework 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, calling .await() on a task yields control back to the dispatcher through a custom instance of Throwable. The dispatcher then commits any new actions that the orchestrator function schedules 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 to 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 gets 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 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
For the replay pattern to 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. To learn 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 example.
Isolated worker model
[Function("HelloCities")]
public static async Task<List<string>> Run(
[OrchestrationTrigger] TaskOrchestrationContext 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;
}
In-process model
[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;
}
using Microsoft.DurableTask;
[DurableTask]
public class HelloCities : TaskOrchestrator<object?, List<string>>
{
public override async Task<List<string>> RunAsync(TaskOrchestrationContext context, object? input)
{
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 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 backend. This state is referred to as the orchestration history.
History Table
Generally, 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 can 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. Alternative storage providers, like 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.
| 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 is useful as a debugging tool, but its format and content might change as the Durable Functions extension evolves.
Every time the function resumes 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 is replayed. After the current execution history is replayed, the local variables are 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.
Isolated worker model
[Function("CheckSiteAvailable")]
public static async Task CheckSiteAvailable(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
Uri url = context.GetInput<Uri>();
// Make an HTTP GET request to the specified endpoint.
DurableHttpResponse response = await context.CallHttpAsync(
method: HttpMethod.Get,
uri: url,
content: null,
retryOptions: null);
if ((int)response.StatusCode == 400)
{
// Handle error codes.
}
}
In-process model
[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.
Isolated worker model
In .NET, you can use record types or ValueTuple objects to pass multiple parameters.
public record CourseInfo(string Major, int UniversityYear);
[Function("GetCourseRecommendations")]
public static async Task<object> RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context, int universityYear)
{
CourseInfo courseInfo = new("ComputerScience", universityYear);
object courseRecommendations = await context.CallActivityAsync<object>(
"CourseRecommendations", courseInfo);
return courseRecommendations;
}
In-process model
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;
}
In .NET, you can use record types or tuples to pass multiple parameters as a single composite object.
using Microsoft.DurableTask;
public record LocationInfo(string City, string State);
[DurableTask]
public class GetWeatherOrchestration : TaskOrchestrator<object?, string>
{
public override async Task<string> RunAsync(TaskOrchestrationContext context, object? input)
{
var location = new LocationInfo("Seattle", "WA");
string weather = await context.CallActivityAsync<string>("GetWeather", location);
return weather;
}
}