Azure Durable Functions: Basic Concepts

Azure Durable Functions: Basic Concepts

If you have idea about what are Azure Functions and how to create them but if you do not know much about Durable Functions, then this article is for you.

We have covered Azure Functions in some of the previous blog articles of AZ-203 series.

For following some of the steps in this article, you will need an Azure account. If you do not have an Azure account,  you can create free account on Azure.

What are Durable Functions ?

Durable Functions are extension to Azure Functions. Mostly Azure Functions are stateless and they have limited timeout.

With Durable Functions, you can create stateful and serverless orchestration of function execution. A durable function app is a solution which contains one or more Azure Functions.

The pricing options are same as for Azure Functions.

There are 4 types of Durable Functions:

Orchestrator Functions

You can use Orchestrator Functions if you want to orchestrate the execution of other durable functions. So this type of function can have different types of actions including Activity Functions, Entity Functions or HTTP/Timer, etc.

The trigger here is Orchestration Trigger.

Activity Functions

Activity is functions or tasks that are orchestrated during the process. Every single task is Activity Function.

Consider a scenario in e-Commerce sites, where user can place the orders. Customer places order, then availability of items is checked, if all items are available then customer is charged, and an email is sent to user with the bill details.

The whole flow can be an Orchestration Function while every step in the function can be an Activity Function.

Please note that Activity Function can not be triggered directly. Use Activity Trigger to Define Activity Functions.

Entity Functions

These functions define operation which interact with small pieces of state. These stateful entities are called as Durable Entities. Use Entity Trigger to write such functions. These functions can be called either from Client Functions or from Orchestrator Function.

This Durable Function type is available only in Durable Functions 2.0 or above.

Client Functions

So far, we have understood that Orchestrator or Entity Functions can interact with each other. Activity Functions are singular tasks which can be called from Orchestrator functions.

There is one more interesting thing: The orchestrator triggers or entity triggers react only to those messages which are already placed on Task Hub.

The primary way to deliver these messages is by using an orchestrator client binding or an entity client binding from within a client function

Any function which is not Orchestrator Function or Entity Function can be a client Function.

These functions can use Durable Client Binding to interact with running orchestrations or entity functions.

Durable Function Patterns

As you might have figured out by now, the primary focus of durable function is to simplify complex stateful coordination in serverless applications.

Function Chaining

In this pattern, there are multiple functions. Output of (n)th function is passed as input to (n+1)th function.

In below example code, you can see that this is Orchestrator Function using Orchestration Trigger. Each function’s output is fed as input to the next function.

[FunctionName("Chaining")]
public static async Task<object> Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    try
    {
        var x = await context.CallActivityAsync<object>("F1", null);
        var y = await context.CallActivityAsync<object>("F2", x);
        var z = await context.CallActivityAsync<object>("F3", y);
        return  await context.CallActivityAsync<object>("F4", z);
    }
    catch (Exception)
    {
        // Error handling or compensation goes here.
    }
}

Fan-out/Fan-in

In this pattern, the orchestrator executes multiple Functions in parallel and then the output of those functions is aggregated somehow to form the final output.

In below example code, you can see the code is using TPL to run multiple tasks in parallel. The output is aggregated and passed to a function and its output is returned as final output.

[FunctionName("FanOutFanIn")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var parallelTasks = new List<Task<int>>();

    // Get a list of N work items to process in parallel.
    object[] workBatch = await context.CallActivityAsync<object[]>("F1", null);
    for (int i = 0; i < workBatch.Length; i++)
    {
        Task<int> task = context.CallActivityAsync<int>("F2", workBatch[i]);
        parallelTasks.Add(task);
    }

    await Task.WhenAll(parallelTasks);

    // Aggregate all N outputs and send the result to F3.
    int sum = parallelTasks.Sum(t => t.Result);
    await context.CallActivityAsync("F3", sum);
}

Async HTTP APIs

Let’s assume that you have an application which needs to call multiple HTTP APIs and need to aggregate the outcome. Traditionally, you can create a method which is triggered by an HTTP call and then keep on polling until the method is done its execution.

With durable functions, it is very easy and it eliminates the need of code to interact with different HTTP requests. Because the Durable Functions runtime manages state for you, you don’t need to implement your own status-tracking mechanism.

Monitoring

Monitoring is simple polling, until certain condition is met. Below is example of simple monitoring function. It waits for either condition to met and it keeps on waiting until the current time is more than the expiry time.

[FunctionName("MonitorJobStatus")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    int jobId = context.GetInput<int>();
    int pollingInterval = GetPollingInterval();
    DateTime expiryTime = GetExpiryTime();

    while (context.CurrentUtcDateTime < expiryTime)
    {
        var jobStatus = await context.CallActivityAsync<string>("GetJobStatus", jobId);
        if (jobStatus == "Completed")
        {
            // Perform an action when a condition is met.
            await context.CallActivityAsync("SendAlert", machineId);
            break;
        }

        // Orchestration sleeps until this time.
        var nextCheck = context.CurrentUtcDateTime.AddSeconds(pollingInterval);
        await context.CreateTimer(nextCheck, CancellationToken.None);
    }

    // Perform more work here, or let the orchestration end.
}

Human Interactions

In real world applications, many times human interactions are required in the workflow. You can implement it by using Orchestrator Function.

Below function checks if the approval event is raised within 72 hours of Approval workflow start.

The approval event can be raised either by HTTP API or by using client.RaiseEventAsync method.

[FunctionName("ApprovalWorkflow")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    await context.CallActivityAsync("RequestApproval", null);
    using (var timeoutCts = new CancellationTokenSource())
    {
        DateTime dueTime = context.CurrentUtcDateTime.AddHours(72);
        Task durableTimeout = context.CreateTimer(dueTime, timeoutCts.Token);

        Task<bool> approvalEvent = context.WaitForExternalEvent<bool>("ApprovalEvent");
        if (approvalEvent == await Task.WhenAny(approvalEvent, durableTimeout))
        {
            timeoutCts.Cancel();
            await context.CallActivityAsync("ProcessApproval", approvalEvent.Result);
        }
        else
        {
            await context.CallActivityAsync("Escalate", null);
        }
    }
}

Aggregations

In this pattern the data from multiple functions into a single, addressable entity. The data may come from multiple sources, it may come in batches, or it may come in very large duration of time.

You can use durable entities to implement this pattern as single function. Below is a durable entity with add/reset/get operations.

[FunctionName("Counter")]
public static void Counter([EntityTrigger] IDurableEntityContext ctx)
{
    int currentValue = ctx.GetState<int>();
    switch (ctx.OperationName.ToLowerInvariant())
    {
        case "add":
            int amount = ctx.GetInput<int>();
            ctx.SetState(currentValue + amount);
            break;
        case "reset":
            ctx.SetState(0);
            break;
        case "get":
            ctx.Return(currentValue);
            break;
    }
}

The below code uses the durable entity and tries to update it using entity client binding.

[FunctionName("EventHubTriggerCSharp")]
public static async Task Run(
    [EventHubTrigger("device-sensor-events")] EventData eventData,
    [DurableClient] IDurableOrchestrationClient entityClient)
{
    var metricType = (string)eventData.Properties["metric"];
    var delta = BitConverter.ToInt32(eventData.Body, eventData.Body.Offset);

    // The "Counter/{metricType}" entity is created on-demand.
    var entityId = new EntityId("Counter", metricType);
    await entityClient.SignalEntityAsync(entityId, "add", delta);
}

I hope this article has provided basic information about the Durable Functions. In next article of AZ-203 series, we will try to create a basic Durable Function app.

Leave a Reply