A Clockwork Engineer

Azure Durable Functions

Azure Durable Functions

Architect serverless solutions in Azure

Durable Functions is an extension of Azure Functions that enables you to perform long-lasting, stateful operations in Azure. Azure provides the infrastructure for maintaining state information. You can use Durable Functions to orchestrate a long-running workflow. Using this approach, you get all the benefits of a serverless hosting model, while letting the Durable Functions framework take care of activity monitoring, synchronization, and runtime concerns.

Suppose your e-commerce company has a warehouse and there is a staff to ship products. We want to automate the process, but still involve humans. We can implement human interaction pattern by using an orchestrator function. The orchestrator uses a durable timer to request approval. The orchestrator escalates if timeout occurs. The orchestrator waits for an external event, such as a notification that’s generated by a human interaction.

Azure Durable Functions Diagram

Create a function app project

If you did not create a function app on Visual Studio before, please follow the steps on Create your first durable function in C#.

Copy the code below to your function.cs file.

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;

namespace OlcayFunctionAppSample
{
    public static class DurableFunction
    {
        [FunctionName("DurableFunction_Orchestrator")]
        public static async Task<List<string>> RunOrchestrator(
            [OrchestrationTrigger] DurableOrchestrationContext context)
        {
            var outputs = new List<string>();

            using (var timeoutCts = new CancellationTokenSource())
            {
                DateTime expiration = context.CurrentUtcDateTime.AddSeconds(20);
                Task timeoutTask = context.CreateTimer(expiration, timeoutCts.Token);

                Task shipTask = context.WaitForExternalEvent("Event_Ship");

                // Wait until one of the tasks is completed
                Task winner = await Task.WhenAny(shipTask, timeoutTask);
                if (winner == shipTask)
                {
                    // Event raised
                    outputs.Add(await context.CallActivityAsync<string>("DurableFunction_Ship", "shipped"));
                }
                else
                {
                    // Timeout expired
                    outputs.Add(await context.CallActivityAsync<string>("DurableFunction_Escalate", "Head of department"));
                }

                if (!timeoutTask.IsCompleted)
                {
                    // All pending timers must be complete or canceled before the function exits.
                    timeoutCts.Cancel();
                }
            }

            return outputs;
        }

        [FunctionName("DurableFunction_Ship")]
        public static string Ship([ActivityTrigger] string activityInput, ILogger log)
        {
            log.LogInformation($"The order is {activityInput}.");
            return $"The order is {activityInput}!";
        }

        [FunctionName("DurableFunction_Escalate")]
        public static string Escalate([ActivityTrigger] string activityInput, ILogger log)
        {
            log.LogInformation($"The order is escalated to {activityInput}.");
            return $"The order is escalated to {activityInput}!";
        }

        [FunctionName("DurableFunction_HttpStart")]
        public static async Task<HttpResponseMessage> HttpStart(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]
            HttpRequestMessage req,
            [OrchestrationClient] DurableOrchestrationClient starter,
            ILogger log)
        {
            string instanceId = await starter.StartNewAsync("DurableFunction_Orchestrator", null);

            log.LogInformation($"Started orchestration with ID = '{instanceId}'.");

            return starter.CreateCheckStatusResponse(req, instanceId);
        }
    }
}

Test the function locally

  1. Press F5 to start debugging.
  2. Copy the URL of your function from the Azure Functions runtime output. Azure Functions Debugging
  3. Paste the URL http://localhost:7071/api/DurableFunction_HttpStart into your browser’s address bar and execute the request.
  4. Copy the URL value for statusQueryGetUri and paste it in the browser’s address bar and execute the request. If you do it on time, you will see Pending or Running as runtimeStatus.
  5. If you wait enough to expire timeout task and refresh, the runtimeStatus would be Completed and an output message would be displayed as The order is escalated to Head of department!.
  6. If you raise ship event before the timeout expires, the runtimeStatus would be Completed and an output message would be displayed as The order is shipped!.

Raising an event

Copy the URL value for sendEventPostUri, replace {eventName} with Event_Ship and run the command below with the the URL on command prompt.

curl --request POST 'http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}/raiseEvent/Event_Ship' \ --header 'Content-Type: application/json' --data-raw ''

You can share the issues that you encountered in the comments so we can find a solution together.

The other posts in the series "Architect serverless solutions in Azure"
cloud, azure, function, durable Azure

Author: Olcay Bayram

A software enthusiast; currently a .NET Developer of an e-commerce Scrum team. Apart from the BSc, he holds a masters in information technologies.