Orchestrator function code constraints
Durable Functions is an extension of Azure Functions that lets you build stateful apps. You can use an orchestrator function to orchestrate the execution of other durable functions within a function app. Orchestrator functions are stateful, reliable, and potentially long-running.
Orchestrator code constraints
Orchestrator functions use event sourcing to ensure reliable execution and to maintain local variable state. The replay behavior of orchestrator code creates constraints on the type of code that you can write in an orchestrator function. For example, orchestrator functions must be deterministic: an orchestrator function will be replayed multiple times, and it must produce the same result each time.
Using deterministic APIs
This section provides some simple guidelines that help ensure your code is deterministic.
Orchestrator functions can call any API in their target languages. However, it's important that orchestrator functions call only deterministic APIs. A deterministic API is an API that always returns the same value given the same input, no matter when or how often it's called.
The following sections provide guidance on APIs and patterns that you should avoid because they are not deterministic. These restrictions apply only to orchestrator functions. Other function types don't have such restrictions.
Several types of code constraints are described below. This list is unfortunately not comprehensive and some use cases might not be covered. The most important thing to consider when writing orchestrator code is whether an API you're using is deterministic. Once you're comfortable with thinking this way, it's easy to understand which APIs are safe to use and which are not without needing to refer to this documented list.
Dates and times
APIs that return the current date or time are nondeterministic and should never be used in orchestrator functions. This is because each orchestrator function replay will produce a different value. You should instead use the Durable Functions equivalent API for getting the current date or time, which remains consistent across replays.
Do not use
DateTime.UtcNow, or equivalent APIs for getting the current time. Classes such as
Stopwatch should also be avoided. For .NET in-process orchestrator functions, use the
IDurableOrchestrationContext.CurrentUtcDateTime property to get the current time. For .NET isolated orchestrator functions, use the
TaskOrchestrationContext.CurrentDateTimeUtc property to get the current time.
DateTime startTime = context.CurrentUtcDateTime; // do some work TimeSpan totalTime = context.CurrentUtcDateTime.Subtract(startTime);
GUIDs and UUIDs
APIs that return a random GUID or UUID are nondeterministic because the generated value is different for each replay. Depending on which language you use, a built-in API for generating deterministic GUIDs or UUIDs may be available. Otherwise, use an activity function to return a randomly generated GUID or UUID.
Do not use APIs like
Guid.NewGuid() to generate random GUIDs. Instead, use the context object's
NewGuid() API to generate a random GUID that's safe for orchestrator replay.
Guid randomGuid = context.NewGuid();
GUIDs generated with orchestration context APIs are Type 5 UUIDs.
Use an activity function to return random numbers to an orchestrator function. The return values of activity functions are always safe for replay because they are saved into the orchestration history.
Alternatively, a random number generator with a fixed seed value can be used directly in an orchestrator function. This approach is safe as long as the same sequence of numbers is generated for each orchestration replay.
An orchestrator function must not use any bindings, including even the orchestration client and entity client bindings. Always use input and output bindings from within a client or activity function. This is important because orchestrator functions may be replayed multiple times, causing nondeterministic and duplicate I/O with external systems.
Avoid using static variables in orchestrator functions because their values can change over time, resulting in nondeterministic runtime behavior. Instead, use constants, or limit the use of static variables to activity functions.
Even outside of orchestrator functions, using static variables in Azure Functions can be problematic for a variety of reasons since there's no guarantee that static state will persist across multiple function executions. Static variables should be avoided except in very specific usecases, such as best-effort in-memory caching in activity or entity functions.
Do not use environment variables in orchestrator functions. Their values can change over time, resulting in nondeterministic runtime behavior. If an orchestrator function needs configuration that's defined in an environment variable, you must pass the configuration value into the orchestrator function as an input or as the return value of an activity function.
Network and HTTP
Use activity functions to make outbound network calls. If you need to make an HTTP call from your orchestrator function, you also can use the durable HTTP APIs.
Blocking APIs like "sleep" can cause performance and scale problems for orchestrator functions and should be avoided. In the Azure Functions Consumption plan, they can even result in unnecessary execution time charges. Use alternatives to blocking APIs when they're available. For example, use Durable timers to create delays that are safe for replay and don't count towards the execution time of an orchestrator function.
Orchestrator code must never start any async operation except those defined by the orchestration trigger's context object. For example, never use
HttpClient.SendAsync in .NET or
async because the Node.js runtime doesn't guarantee that asynchronous functions are deterministic.
You must not declare Python orchestrator functions as coroutines. In other words, never declare Python orchestrator functions with the
async keyword because coroutine semantics do not align with the Durable Functions replay model. You must always declare Python orchestrator functions as generators, meaning that you should expect the
context API to use
yield instead of
.NET threading APIs
The Durable Task Framework runs orchestrator code on a single thread and can't interact with any other threads. Running async continuations on a worker pool thread an orchestration's execution can result in nondeterministic execution or deadlocks. For this reason, orchestrator functions should almost never use threading APIs. For example, never use
ConfigureAwait(continueOnCapturedContext: false) in an orchestrator function. This ensures that task continuations run on the orchestrator function's original
The Durable Task Framework attempts to detect accidental use of non-orchestrator threads in orchestrator functions. If it finds a violation, the framework throws a NonDeterministicOrchestrationException exception. However, this detection behavior won't catch all violations, and you shouldn't depend on it.
A durable orchestration might run continuously for days, months, years, or even eternally. Any code updates made to Durable Functions apps that affect unfinished orchestrations might break the orchestrations' replay behavior. That's why it's important to plan carefully when making updates to code. For a more detailed description of how to version your code, see the versioning article.
This section describes internal implementation details of the Durable Task Framework. You can use durable functions without knowing this information. It is intended only to help you understand the replay behavior.
Tasks that can safely wait in orchestrator functions are occasionally referred to as durable tasks. The Durable Task Framework creates and manages these tasks. Examples are the tasks returned by
CreateTimer in .NET orchestrator functions.
These durable tasks are internally managed by a list of
TaskCompletionSource objects in .NET. During replay, these tasks are created as part of orchestrator code execution. They're finished as the dispatcher enumerates the corresponding history events.
The tasks are executed synchronously using a single thread until all the history has been replayed. Durable tasks that aren't finished by the end of history replay have appropriate actions carried out. For example, a message might be enqueued to call an activity function.
This section's description of runtime behavior should help you understand why an orchestrator function can't use
yield in a nondurable task. There are two reasons: the dispatcher thread can't wait for the task to finish, and any callback by that task might potentially corrupt the tracking state of the orchestrator function. Some runtime checks are in place to help detect these violations.
To learn more about how the Durable Task Framework executes orchestrator functions, consult the Durable Task source code on GitHub. In particular, see TaskOrchestrationExecutor.cs and TaskOrchestrationContext.cs.