Understanding Azure Durable Functions - Part 5: Getting Results from Orchestrations

This is the fifth part in a series of articles.

As we learned earlier in this series, a client function is called that initiates an orchestrator function which in turn calls one or more activity functions.

This process is asynchronous in nature. If the client function is an HTTP function then the HTTP request will complete and return an HTTP 202 accepted response to the caller. As this response code suggests, the request has been successfully accepted for processing but the processing is not yet complete.

Take the following client function that triggers an orchestration:

[FunctionName("ClientFunction")]
public static async Task<HttpResponseMessage> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req,
    [OrchestrationClient]DurableOrchestrationClient starter,
    ILogger log)
{
    // Function input comes from the request content.
    string instanceId = await starter.StartNewAsync("OrchestratorFunction", null);

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

    return starter.CreateCheckStatusResponse(req, instanceId);
}

Notice the line return starter.CreateCheckStatusResponse(req, instanceId); This method creates an HttpResponseMessage which is returned to the caller. This message contains information on how to check the status of the orchestration.

As an example, if the client function is called over HTTP (e.g. in local development: http://localhost:7071/api/ClientFunction) the response will look similar to the following:

{
    "id": "85ee280f20a249089ec30882bd2ea4e2",
    "statusQueryGetUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/85ee280f20a249089ec30882bd2ea4e2?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
    "sendEventPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/85ee280f20a249089ec30882bd2ea4e2/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
    "terminatePostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/85ee280f20a249089ec30882bd2ea4e2/terminate?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
    "rewindPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/85ee280f20a249089ec30882bd2ea4e2/rewind?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
    "purgeHistoryDeleteUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/85ee280f20a249089ec30882bd2ea4e2?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA=="
}

This JSON returns the id of the orchestration instance along with a number of URLs that can be used to interact with the orchestration instance.

Checking the Status of a Durable Functions Orchestration

To check the status of an orchestration instance, an HTTP GET can be sent to the following URL:  http://localhost:7071/runtime/webhooks/durabletask/instances/85ee280f20a249089ec30882bd2ea4e2?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==

Assuming the following orchestrator and activity functions:

[FunctionName("OrchestratorFunction")]
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>("ActivityFunction", "Tokyo"));
    outputs.Add(await context.CallActivityAsync<string>("ActivityFunction", "Seattle"));
    outputs.Add(await context.CallActivityAsync<string>("ActivityFunction", "London"));

    // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
    return outputs;
}

[FunctionName("ActivityFunction")]
public static string SayHello([ActivityTrigger] string name, ILogger log)
{
    Thread.Sleep(5000); // simulate longer processing delay

    log.LogInformation($"Saying hello to {name}.");
    return $"Hello {name}!";
}

If the orchestration is still running, then querying the status URL will return the following:

{
    "name": "OrchestratorFunction",
    "instanceId": "85ee280f20a249089ec30882bd2ea4e2",
    "runtimeStatus": "Running",
    "input": null,
    "customStatus": null,
    "output": null,
    "createdTime": "2019-08-07T03:50:39Z",
    "lastUpdatedTime": "2019-08-07T03:50:39Z"
}

Notice the runtimeStatus of “Running”, meaning that the orchestration is not yet complete, also notice output is null.

If we wait for the orchestration to complete and call the URL again we get the following:

{
    "name": "OrchestratorFunction",
    "instanceId": "35d752392e934df994d01951102e50e8",
    "runtimeStatus": "Completed",
    "input": null,
    "customStatus": null,
    "output": [
        "Hello Tokyo!",
        "Hello Seattle!",
        "Hello London!"
    ],
    "createdTime": "2019-08-07T03:50:39Z",
    "lastUpdatedTime": "2019-08-07T03:50:55Z"
}

Notice this time that the runtimeStatus is now “Completed” and the output gives us the results returned from the orchestrator function in the line: return outputs;

In addition to getting the status you can use the other URLs to terminate a running orchestration,  purge history for instances, send event notifications to orchestrations, and replay (rewind) a failed orchestration into a running state (currently in preview). You can check out the complete API reference in the documentation.

Running Durable Functions Synchronously

Currently the client function is starting a new orchestration instance with the code: string instanceId = await starter.StartNewAsync("OrchestratorFunction", null);

In this code, the method StartNewAsync starts the orchestration asynchronously and returns the instance id.

If you want the orchestration to run synchronously, and for the client function to wait around until a result is available, the (rather verbose) WaitForCompletionOrCreateCheckStatusResponseAsync method can be used as the following code demonstrates:

[FunctionName("ClientFunctionSync")]
public static async Task<HttpResponseMessage> HttpStartSync(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req,
    [OrchestrationClient]DurableOrchestrationClient starter,
    ILogger log)
{
    string instanceId = await starter.StartNewAsync("OrchestratorFunction", null);
    log.LogInformation($"Started orchestration with ID = '{instanceId}'.");

    var timeout = TimeSpan.FromSeconds(20);
    var retryInterval = TimeSpan.FromSeconds(1); // How often to check the orchestration instance for completion

    return await starter.WaitForCompletionOrCreateCheckStatusResponseAsync(req,instanceId,timeout,retryInterval);
}

If we call this modified HTTP client function, the HTTP request will complete after approximately 15 seconds with a 200 OK status code and the following response:

[
    "Hello Tokyo!",
    "Hello Seattle!",
    "Hello London!"
]

The reason the response takes 15 seconds is that the activity function has a 5 second delay in it (Thread.Sleep(5000); // simulate longer processing delay) and the orchestrator is calling this function 3 times.

If we reduce the timeout to 5 seconds (var timeout = TimeSpan.FromSeconds(5);) and call the client HTTP function again, once again we get a 202 Accepted and we get the following returned:

{
    "id": "8f88192ecfb3440199e572e93c478906",
    "statusQueryGetUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/8f88192ecfb3440199e572e93c478906?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
    "sendEventPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/8f88192ecfb3440199e572e93c478906/raiseEvent/{eventName}?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
    "terminatePostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/8f88192ecfb3440199e572e93c478906/terminate?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
    "rewindPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/8f88192ecfb3440199e572e93c478906/rewind?reason={text}&taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA==",
    "purgeHistoryDeleteUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/8f88192ecfb3440199e572e93c478906?taskHub=DurableFunctionsHub&connection=Storage&code=ZOBhsHdAnHuXA6s2FMCcmcgW2XLFOVpQ5Hfob5CWYcyi2c5Al0DyjA=="
}

Now the client can use the status URL to poll for completion as before.

You will want to make sure that the end client that makes the initial HTTP call to start the orchestration doesn’t have an HTTP timeout implemented that is shorter than the timeout specified in the call to WaitForCompletionOrCreateCheckStatusResponseAsync (plus some extra time for the overhead of starting the orchestration etc) otherwise this initial call will always timeout on the client side.

Adding Custom Status Information to a Durable Functions Orchestration

In addition to the built-in status information, you can also set custom status information in the orchestrator function.

To do this the SetCustomStatus method of the DurableOrchestrationContext can be used, this method takes an object. The following is a modified version of the orchestrator function that gives us a rough idea of what % of processing has been completed:

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

    context.SetCustomStatus("0% complete");            
    outputs.Add(await context.CallActivityAsync<string>("ActivityFunction", "Tokyo"));
    context.SetCustomStatus("33% complete");

    outputs.Add(await context.CallActivityAsync<string>("ActivityFunction", "Seattle"));
    context.SetCustomStatus("66% complete");

    outputs.Add(await context.CallActivityAsync<string>("ActivityFunction", "London"));
    context.SetCustomStatus("100% complete");

    // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
    return outputs;
}

If we call the client HTTP function and then periodically call the status URL we get the following:

{
    "name": "OrchestratorFunction",
    "instanceId": "84d370b3833d4563b3cc1b1bab285787",
    "runtimeStatus": "Running",
    "input": null,
    "customStatus": "33% complete",
    "output": null,
    "createdTime": "2019-08-07T05:07:41Z",
    "lastUpdatedTime": "2019-08-07T05:07:56Z"
}

 

{
    "name": "OrchestratorFunction",
    "instanceId": "84d370b3833d4563b3cc1b1bab285787",
    "runtimeStatus": "Running",
    "input": null,
    "customStatus": "66% complete",
    "output": null,
    "createdTime": "2019-08-07T05:07:41Z",
    "lastUpdatedTime": "2019-08-07T05:08:12Z"
}
{
    "name": "OrchestratorFunction",
    "instanceId": "84d370b3833d4563b3cc1b1bab285787",
    "runtimeStatus": "Completed",
    "input": null,
    "customStatus": "100% complete",
    "output": [
        "Hello Tokyo!",
        "Hello Seattle!",
        "Hello London!"
    ],
    "createdTime": "2019-08-07T05:07:41Z",
    "lastUpdatedTime": "2019-08-07T05:08:27Z"
}

Notice in the preceding statuses that the customStatus has been populated. You can output anything in this, for example you could output an estimated time remaining to be displayed in the end client to give the end user an idea of when the request will be complete.

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

Pingbacks and trackbacks (2)+

Add comment

Loading