Featured image of post This is how I endure Durable Functions

This is how I endure Durable Functions

I’ll skip the shenanigans. If you’re reading this, then you’ve probably already been through Stackoverflow, official documentation, and all other classical sources. Let’s cut to the chase and dive into these magnificent beasts, the Azure Durable Functions.

Do you really know what Durable Functions do?

If you think they are exactly like common Azure Functions but run longer, then you’re mistaken. A durable function will run like a classic function until it meets an “await” statement (triggering an Activity function). Then it will save its state and go to sleep until the response arrives. This is good because we don’t pay for the function while it sleeps. When the response arrives, the durable function wakes up and starts over. Yes, it starts over, running again from the beginning, but it remembers the state of all variables and won’t change it. Be careful—your code must always be deterministic, as it will be executed repeatedly.

Hint: Logging in Durable Functions

Of course, you want to write log messages. But if you do, the same log messages will appear repeatedly because the Durable Function is re-executed. Use the CreateReplaySafeLogger command to prevent flooding your app insights with duplicate messages:

Replay Safe Logger Example
Courtesy of Joonas Westlin!

The Durable Function doesn’t start with the Orchestration Function

This is why your HTTP Trigger didn’t work. You don’t start there—you start with the client function! There are four types of functions, distinguished by triggers and bindings (e.g., [OrchestrationTrigger], [ActivityTrigger], etc.). Instead of copying from the official documentation, here’s some code, which is available on GitHub:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Function1
{
    private readonly ILogger _logger;

    public Function1(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<Function1>();
    }

    [Function("StartHere")]
    public async Task<HttpResponseData> StartHere(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
        [DurableClient] DurableClientContext durableContext,
        FunctionContext executionContext)
    {
        ILogger logger = executionContext.GetLogger(nameof(StartHere));

        string instanceId = await durableContext.Client.ScheduleNewOrchestrationInstanceAsync(nameof(RunOrchestrator));
        logger.LogInformation("Step 1. Created new orchestration with instance ID = {instanceId}", instanceId);

        return durableContext.CreateCheckStatusResponse(req, instanceId);
    }

    [Function(nameof(RunOrchestrator))]
    public async Task RunOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
    {
        var log = context.CreateReplaySafeLogger(_logger);

        log.LogInformation("Step 2. Running a couple of activities");

        await context.CallActivityAsync(nameof(RunActivity), "John");
        await context.CallActivityAsync(nameof(RunActivity), "Jim");
        await context.CallActivityAsync(nameof(RunActivity), "Joe");
    }

    [Function(nameof(RunActivity))]
    public async Task RunActivity([ActivityTrigger] string input)
    {
        _logger.LogInformation("Hello " + input);

        await Task.Delay(100);
    }
}

Instead of calling the Orchestrator function (which you can’t directly), you start by triggering the DurableClient, which then invokes the orchestration:

Durable Client Example

All rights reserved
Built with Hugo
Theme Stack designed by Jimmy