June 28, 2023
Data Tech

How to Securely Get Azure Durable Function Runtime Status

How you can have a dedicated status url to get the status of a long-running durable function, without using the default status url
How to Securely Get Azure Durable Function Runtime Status

We make use of Azure Durable Functions for a range of long running API tasks, but one particular issue has bugged us for some time. This was how to get the status updates of a Function back to the calling code when that Function is part of a more complex (and secure) n-tier architecture.  

The built in code for a Durable Function will accept the initial request to launch processing and then return an object with a number of properties on it (instanceId, status URL etc). The status Url can be used in combination with the InstanceId to poll the Function to get progress updates, which is pretty cool, but only if you are happy exposing your function to that calling code directly or publicly.

For reference our Function API's sit behind an API Management instance, which immediately means the URL that is returned by the function app is not reachable as demonstrated below:

The status URL can be constructed by convention so you don't need to rely on the built in response that is generated for you, so you could add a dedicated back end to your API Management instance, to forward traffic to the function status URL. But in our case we deploy all of our (many) API's via terraform, having one non-conventional URL would require some nasty forks in that codebase which seemed wrong.

It appeared obvious to us that being able to just get the status of a Durable Function app was something people would want to do, but the documentation didn't seem to point to any method to do that and most forums had people searching for an answer but with no joy.

The Answer

It turns out there is a really elegant solution to this problem, which we discovered after trawling through the Azure source code and comparing the various implementations (C# vs Python).

If you have a Durable Function app, in the same way that the Durable processing can be instantiated by a HTTP POST function, the status of a Durable function can be obtained by any other Function inside the app, this is because the Durable runtime is shared. This means we can create a dedicated HTTP function that is responsible for just returning the status of an orchestration execution.

The key to creating this standalone function is to ensure the Durable Orchestration Client is injected into the sibling Function so you can leverage the get_status method that can then be returned to the calling code. In C# you can inject the client via standard IOC, with python, by setting up the injection binding we receive the Orchestration Client id (starter) which is then used to instantiate the client.

As a simple Illustration of how to do this using Python, create a new HTTP function and then:

1. Ensure thefunction.json config contains the client as a binding dependency, starter in this case is a string that is used to instantiate the Orchestration Client class.

"bindings": [ 
    { 
      "name": "starter", 
      "type": "orchestrationClient", 
      "direction": "in" 
    } 
  ]

2. The Http entry point of the function can then be extended to include that property as a parameter.

async def main(request: func.HttpRequest, starter: str) -> func.HttpResponse:

3. The OrchestrationClient can then be instantiated which will hook into the active Orchestration runtime within the parent function app. That client can then obtain the current status of an instance using the.getStatus() function.

client = df.DurableOrchestrationClient(starter) 
    status = await client.get_status(instance_id) 

Much like the status object that is returned by default from a durable function, the get_status() function returns a DurableOrchestationStatus object with all of the information we care about for progress updates. In our cause we grab the properties we care about and send those back from our Http Status function.

filtered_response = { 
        "instanceId": status.instance_id, 
        "runtimeStatus": "" 
        if not status.runtime_status 
        else status.runtime_status.name, 
        "customStatus": status.custom_status, 
        "createdTime": status.created_time.isoformat(), 
        "lastUpdatedTime": status.last_updated_time.isoformat(), 
    }

‍Note, there is a handy to_json() method on the status object to serialise that object ready for HTTP return

With this convention in play, we no longer care about the majority of the response properties we get back from our orchestration client, other than to capture the InstanceId of the runtime, so it can be used to obtain the current status.

For us our API now looks like this:

That means we maintain our API Management pattern, our Restful API implementation and we definitely do not expose our function app to the wild.

Maximise your data value

See how The Data Refinery can unlock the value in your data.
Director of Product at The Data Refinery

Check out our latest posts

Book your free 30 minute
data value review

Are you ready to supercharge your organisation’s analytics & AI capabilities,
but not quite ready to invest £100,000s for a bespoke solution?