Durable Functions 101
I’ve done a lot of Durable Functions hackfests with my customers. I noticed that every time I explain the same things to them. I’d like to share the seven tips when you start Durable Functions.
- Tip 1. Class and responsibility
- Tip 2. Use Storage Emulator
- Tip 3. Orchestrator should be deterministic
- Tip 4. When to use Sub Orchestrator
- Tip 5. DevOps CD pipeline
- Tip 6. HttpClient should be static
- Tip 7. Run-From-Zip deployment
You can find Japanese version in here.
Tip 1. Class and responsibility
I’d like to share three components which consist of Durable Functions.
Activity functions
Activity Functions are almost the same as plain “Azure Functions”. You can write code as usual. One big difference is you need ActivityTrigger which is trigged from orchestrator.
[FunctionName("Sample_Hello")]public static string SayHello([ActivityTrigger] string name, TraceWriter log){log.Info($"Saying hello to {name}.");return $"Hello {name}!";}
Orchestrator
Orchestrator orchestrates Activity Functions. It requires OrchestrationTrigger.
[FunctionName("Sample")]public static async Task<List<string>> RunOrchestrator([OrchestrationTrigger] DurableOrchestrationContext context){var outputs = new List<string>();// Replace "hello" with the name of your Durable Activity Function.outputs.Add(await context.CallActivityAsync<string>("Sample_Hello", "Tokyo"));outputs.Add(await context.CallActivityAsync<string>("Sample_Hello", "Seattle"));outputs.Add(await context.CallActivityAsync<string>("Sample_Hello", "London"));// returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]return outputs;}
As you can see, this orchestrator calls Activity functions like this. It looks like it is calling a function, but it actually sending a message to a work-item queue. The activity functions receive messages from this queue via ActivityTrigger and then execute the logic. Once an activity function finishes its execution, it send a response message to the control queue, which the orchestrator function recieves via the OrchestrationTrigger. This is the basic behavior of Durable Functions.
await context.CallActivityAsync<string>("Sample_Hello", "Tokyo")
Once you start a Durable function, it creates four control queue and one worker item queue. It also creates DurableFunctionsHubHistory and DurableFunctionsHubInstances Azure Storage tables. You can find these on your storage account.
For more details,
OrchestrationClient
OrchestrationClient is responsible for starting/stopping orchestrators and monitoring their status. When you send http request to the following sample function, it start the orchestrator. It requires the OrchestrationClient binding. NOTE 9/12/2019 OrchestrationClient is going to move DurableClient from v2. More details here.
[FunctionName("Sample_HttpStart")]public static async Task<HttpResponseMessage> HttpStart([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req,[OrchestrationClient]DurableOrchestrationClient starter,TraceWriter log){// Function input comes from the request content.string instanceId = await starter.StartNewAsync("Sample", null);log.Info($"Started orchestration with ID = '{instanceId}'.");return starter.CreateCheckStatusResponse(req, instanceId);}
To start writing code, I recommend using the Durable Functions template in Visual Studio. It is the easiest way to understand the whole picture.
Unfortunately the template supports only V2 functions.For V1 functions, you can refer to my repo, copy the code, and update the Microsoft.Azure.WebJobs.Extensions.DurableTask nuget package.
However, currently, it has one issue related to Newtonsoft.Json. But it has a workaround. Please watch this issue.
Also, you can find samples on the GitHub repo.
Tip 2. Use Storage Emulator
When you develop Durable Functions via Visual Studio, I strongly recommend using the Azure Storage Emulator.
It is already installed if you already installed Visual Studio with Azure SDK. For more details, you can refer to this article.
Durable Functions store their state in the Azure Storage Table and Azure Storage Queues. When you develop Durable Functions with several people, if they share the same storage account, it will cause weird behavior. To avoid these problem, please use the storage emulator for local development. Using it is effortless. Start the Storage emulator, then clear the Storage queues and tables using this command.
AzureStorageEmulator.exe clear all
Then please double-check your local.settings.json
{"IsEncrypted": false,"Values": {"AzureWebJobsStorage": "UseDevelopmentStorage=true","AzureWebJobsDashboard": "UseDevelopmentStorage=true"}}
Tip 3. Orchestrator should be deterministic
Orchestrator code should be deterministic. These operations are not allowed.
- Useing random data (e.g. Guid.NewGuid(), DateTime.Now)
- I/O (HttpClient and other bindings)
- Non-Durable Asynchronous API (e.g., Task.Delay)
If you want to use these, you can use them in the Activity Functions. The reason is, orchestrator functions replay several times to build local state. Orchestrator functions write to the History table to update their state. After it sends a message to an Activity function, it goes to sleep. When it receives a response message from the Activity function, it reads the history from the history table, and then it replays the code until it reaches the previous code has been executed. That is why it should be deterministic. For more details, I recommend seeing this PowerPoint slide by Chris from P43. It shows the behavior. Also, I shoot three minutes video to understand the behavior.
Tip 4. When to use Sub Orchestrator
Durable Functions also supports Sub Orchestrator. This feature enables us to create sub sorchestrators which orchestrate several activities. But when should they be used?
You can use sub orchestrators when you have orchestration which repeats a lot of times, like 100 or 1000. Also, you can use it when you want to organize a complex orchestration.
I recommend that Activity functions each have a single responsibility. One reason is that Durable Functions has an excellent retry policy feature. If you design your activity functions to have a single responsibility, you can easily make them idempotent. Then you can easily use retry policies. It reduces a bunch of your code.
public static async Task Run(DurableOrchestrationContext context) { var retryOptions = new RetryOptions(
firstRetryInterval: TimeSpan.FromSeconds(5),
maxNumberOfAttempts: 3);
await ctx.CallActivityWithRetryAsync("FlakyFunction", retryOptions, null);
// ...
}
For more detail
Tip 5. DevOps CD pipeline
You need to know what happens when you deploy a new application. The simple fact is when you change these things,
- Orchestrator code
- Activity Functions interface
It affects the data in the queue and history table. If you have a running instance and deploy a new version of Durable Functions, the running instances may encounter an error because of the code no longer matches the history.
The easiest solution is to delay the deployment until the all running instance has been finished by not accepting the new request until the new version is deployed. There is two ways you can monitor for this.
Query all instances
You can query all instances’ status via an HTTP API or the OrchestrationClient bindings. However, this strategy doesn’t work well when there are lots of instances due to current performance issue. I’ll contribute a fix to prevent that issues.
EventGrid publishing
Also, you can use the integration with Azure Event Grid. Once you configure this feature, it emits the orchestration lifecycle events to the EventGrid topic, but you will need to manage the state by yourself so that you can check if there are running instances or not.
If you have long running instances, you can use this strategy.
Tip 6. HttpClient should be static
This tip is not specific to Durable Functions is for Azure Functions in general. If you instantiate many outbound traffic network clients like HttpClient, it will causes a performance issue. The reason is, inside of the Scale Unit where the FunctionApp resides, when it establishes an outbound connection with other services, it changes the configuration of the routing table, and it is very slow. It causes a performance issue. Also, there are a limited number of outbound TCP ports. If too many connections are establish, all the ports can be used up and new connections will fail.
If you reuse the connection, it won’t happen. Please refer to this article.
Tip 7. Run-From-Zip deployment
This tip is also not only for Durable Functions but Azure Functions in general. We have several ways to deploy Durable Functions to the Function App. However, there is a cold-start problem for the Consumption plan. To minimize cold-start, We can use the Run-From-Zip deployment. All you need to do is zip your app, upload somewhere (like Azure Blob storage) and add the URL to AppSettings. The consumption plan normally uses Azure File to share files among the VMs. This can cause slow cold-start performance. Using Run-From-Zip, contents can be extracted locally and loaded into memory. This deployment method will eventually become mainstream.
That’s it
Enjoy coding with Durable Functions!
Next steps
You can enjoy these videos.
If you are interesting, Chirs talked in Japanese!