Async Http APIs with Azure Durable Functions (and Polling Client)

070618_1006_AsyncHttpAP1.png

Introduction

Azure Durable Functions have support for different patterns, which enable us to build serverless and stateful applications without worrying about the state management implementation details. One of these useful patterns is the Asynchronous Http APIs. This pattern comes in handy when client applications need to trigger long running processes exposed as APIs and do something else after a status is reached. You might be thinking that the best way to implement an async API is by enabling the API to raise an event once it finishes so the client can react to it. For instance via a webhook (Http callback request), an Event Grid event, or even using SignalR. That is true, however, in many cases, client apps cannot be modified, or there are security or networking restrictions which make polling the best or the only feasible alternative. In this post, I’ll describe how to implement the Asynchronous Http API pattern on Durable Functions based on the polling client approach.

Before we get into the details, it’s worth noting that this pattern can be used to implement a custom trigger or action for Logic Apps with the polling pattern.

Scenario

To demonstrate how to implement this pattern, I’ll use the scenario of “Call for Speakers” in a conference. In this, potential speakers submit a topic through an app, and they are very keen to know as soon as possible if they have been selected to present. Of course, in a real scenario there will be timelines and speakers would be notified before the agenda is published, so they wouldn’t need to keep continuously asking for the status of their submission. But I believe you get the idea that this is being used for illustration purposes only 😉

Solution Overview

The solution is based on Azure Durable Functions’ building blocks, including orchestration clients, the orchestration function, and activity functions. I’ve used dummy activity functions, as the main purpose of this post is to demonstrate the capabilities to implement an Asynchronous Http API. In case you want to understand how to implement this further, you can have a look at my previous posts on how to implement approval workflows on Durable Functions.

The diagram below shows the different components of the solution. As you can see, there is a client app which submits the application by calling the Submit Azure function, and then can get the status of the application by calling a GET Status Azure function. The orchestration function controls the process and calls the activity functions.

070618_1006_AsyncHttpAP2.png

Solution Components

The main components of the solution are described below. The comments in the code should also help you to better understand each of them. You can find the full solution sample code here.

Submit Function

Http triggered function which implements the DurableOrchestrationClient. This function receives a “Call-for-Speakers” submission as a POST with a JSON payload as the request body. Then, it starts the submission processing orchestration. Finally, it returns to the client the URL to the endpoint to get the status using the location and retry-after http headers.


using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using PacodelaCruz.DurableFunctions.AsyncHttpApi.Models;
using System.IO;
using System.Threading.Tasks;
namespace PacodelaCruz.DurableFunctions.AsyncHttpApi
{
public static class Submit
{
/// <summary>
/// HTTP Triggered Function which implements the DurableOrchestrationClient.
/// Receives a Call-for-Speakers submission as a POST with a JSON payload as the body request
/// Then starts the submission processing orchestration.
/// Returns the location to check for the status by using the location and retry-after Http headers.
/// I'm using Anonymous Aurhotisation Level for demonstration purposes. You must use a more secure approach.
/// </summary>
/// <param name="req"></param>
/// <param name="orchestrationClient"></param>
/// <param name="logger"></param>
/// <returns></returns>
[FunctionName("Submit")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, methods: "post", Route = "submit")]
HttpRequest req,
[OrchestrationClient] DurableOrchestrationClient orchestrationClient,
ILogger logger)
{
logger.LogInformation("Submission received via Http");
string requestBody = new StreamReader(req.Body).ReadToEnd();
var submission = JsonConvert.DeserializeObject<Presentation>(requestBody, new JsonSerializerSettings() { ContractResolver = new CamelCasePropertyNamesContractResolver() });
var instanceId = await orchestrationClient.StartNewAsync("ProcessSubmission", submission);
logger.LogInformation("Submission process started", instanceId);
string checkStatusLocacion = string.Format("{0}://{1}/api/status/{2}", req.Scheme, req.Host, instanceId); // To inform the client where to check the status
string message = $"Your submission has been received. To get the status, go to: {checkStatusLocacion}";
// Create an Http Response with Status Accepted (202) to let the client know that the request has been accepted but not yet processed.
ActionResult response = new AcceptedResult(checkStatusLocacion, message); // The GET status location is returned as an http header
req.HttpContext.Response.Headers.Add("retry-after", "20"); // To inform the client how long to wait in seconds before checking the status
return response;
}
}
}

view raw

Submit.cs

hosted with ❤ by GitHub

Process Submission Function

Orchestration function which implements the function chaining pattern for each of the stages of the submission approval process. It’s also in charge of updating the custom status of the instance as it progresses.


using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using PacodelaCruz.DurableFunctions.AsyncHttpApi.Models;
using System.Threading.Tasks;
namespace PacodelaCruz.DurableFunctions.AsyncHttpApi
{
public static class ProcessSubmission
{
/// <summary>
/// Durable Functions Orchestration
/// Receives a Call-for-Speaker submissions and control the approval workflow.
/// Updates the Orchestration Instance Custom Status as it progresses.
/// </summary>
/// <param name="context"></param>
/// <param name="logger"></param>
/// <returns></returns>
[FunctionName("ProcessSubmission")]
public static async Task<bool> RunOrchestrator(
[OrchestrationTrigger] DurableOrchestrationContext context,
ILogger logger)
{
Presentation presentation = context.GetInput<Presentation>();
presentation.Id = context.InstanceId;
string stage;
string status;
bool isTrackingEvent = true;
bool approved;
stage = "Moderation";
// Set the custom status for the ochestration instance.
// This can be any serialisable object. In this case it is just a string.
context.SetCustomStatus(stage);
approved = await context.CallActivityAsync<bool>("Moderate", presentation);
if (approved)
{
stage = "Shortlisting";
context.SetCustomStatus(stage);
approved = await context.CallActivityAsync<bool>("Shortlist", presentation);
if (approved)
{
stage = "Selection";
context.SetCustomStatus(stage);
approved = await context.CallActivityAsync<bool>("Select", presentation);
}
}
if (approved)
status = "Approved";
else
status = "Rejected";
context.SetCustomStatus(status);
logger.LogInformation("Submission has been {status} at stage {stage}. {presenter}, {title}, {track}, {speakerCountry}, {isTrackingEvent}", status, stage, presentation.Speaker.Email, presentation.Title, presentation.Track, presentation.Speaker.Country, isTrackingEvent);
return approved;
}
}
}

Get Status Function

This is an Http function that allows clients to get the status of an instance of a Durable Function orchestration. You can get more details of this implementation here.

In many of the samples, they explain you how to use the CreateCheckStatusResponse method to generate the response to the client. However, make sure that that you fully understand what this returns. Given that the returned payload includes management endpoints with their corresponding keys, by providing that, you would allow the clients not only to get the status of a running instance, but also to 1) get the activity execution history, 2) get the outputs of the already executed activity functions, 3) send external events to the orchestration instance, and 4) terminate that instance. If you don’t want to give those privileges to the clients, you need to expose an http function that returns only the minimum information required. In this wrapper function, you could also enrich the response if need be. If you only need to send the status back, I would recommend you to use the GetStatusAsync method instead. 

The sample function below makes use of the GetStatusAsync method of the orchestration client, and leverages the location and retry-after Http headers.


using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
namespace PacodelaCruz.DurableFunctions.AsyncHttpApi
{
public static class GetStatus
{
/// <summary>
/// Http Triggered Function which acts as a wrapper to get the status of a running Durable orchestration instance.
/// It enriches the response based on the GetStatusAsync's retruned value
/// I'm using Anonymous Aurhotisation Level for demonstration purposes. You should use a more secure approach.
/// </summary>
/// <param name="req"></param>
/// <param name="orchestrationClient"></param>
/// <param name="instanceId"></param>
/// <param name="logger"></param>
/// <returns></returns>
[FunctionName("GetStatus")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, methods: "get", Route = "status/{instanceId}")] HttpRequest req,
[OrchestrationClient] DurableOrchestrationClient orchestrationClient,
string instanceId,
ILogger logger)
{
// Get the built-in status of the orchestration instance. This status is managed by the Durable Functions Extension.
var status = await orchestrationClient.GetStatusAsync(instanceId);
if (status != null)
{
// Get the custom status of the orchestration intance. This status is set by our code.
// This can be any serialisable object. In this case, just a string.
string customStatus = (string)status.CustomStatus;
if (status.RuntimeStatus == OrchestrationRuntimeStatus.Running || status.RuntimeStatus == OrchestrationRuntimeStatus.Pending)
{
//The URL (location header) is prepared so the client know where to get the status later.
string checkStatusLocacion = string.Format("{0}://{1}/api/status/{2}", req.Scheme, req.Host, instanceId);
string message = $"Your submission is being processed. The current status is {customStatus}. To check the status later, go to: GET {checkStatusLocacion}"; // To inform the client where to check the status
// Create an Http Response with Status Accepted (202) to let the client know that the original request hasn't yet been fully processed.
ActionResult response = new AcceptedResult(checkStatusLocacion, message); // The GET status location is returned as an http header
req.HttpContext.Response.Headers.Add("retry-after", "20"); // To inform the client how long to wait before checking the status.
return response;
}
else if (status.RuntimeStatus == OrchestrationRuntimeStatus.Completed)
{
// Once the orchestration has been completed, an Http Response with Status OK (200) is created to inform the client that the original request has been fully processed.
if (customStatus == "Approved")
return new OkObjectResult($"Congratulations, your presentation with id '{instanceId}' has been accepted!");
else
return new OkObjectResult($"We are sorry! Unfortunately your presentation with id '{instanceId}' has not been accepted.");
}
}
// If status is null, then instance has not been found. Create and return an Http Response with status NotFound (404).
return new NotFoundObjectResult($"Whoops! Something went wrong. Please check if your submission Id is correct. Submission '{instanceId}' not found.");
}
}
}

view raw

GetStatus.cs

hosted with ❤ by GitHub

Wrapping Up

In this post, I’ve shown how to implement Asynchronous Http APIs using Durable Functions with the polling consumer pattern. I’ve also explain the differences between using CreateCheckStatusResponse and GetStatusAsync methods. The former prepares a response which expose different management endpoints with a key that allow clients to do much more than just getting the status, while the latter just returns the instance status. I hope you’ve enjoyed and found useful this post.

If you are interested in more posts about patterns on Durable Functions, you can also check out:

Happy clouding!

Cross-posted on Deloitte Platform Engineering Blog.
Follow me on @pacodelacruz.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s