Search code examples
c#azure-durable-functions

Check if orchestration function is already running, using isolated process, .net 8


[Function(nameof(FunctionNames.BuildingsStart))]
public static async Task<HttpResponseData> HttpRun(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] 
HttpRequestData req,
[DurableClient] DurableTaskClient client, FunctionContext 
executionContext)
{
 ILogger logger = executionContext.GetLogger(nameof(HttpRun));

 string instanceId = awaitclient.ScheduleNewOrchestrationInstanceAsync(nameof(FunctionNames.BuildingsOrchestration));
 logger.LogInformation("Created new orchestration with instance ID = {instanceId}", instanceId);

 return client.CreateCheckStatusResponse(req, instanceId);

}

In the above code, how to check if BuildingsOrchestration is already running, or part way through a current execution? Using .net 8, isolated process (NOT in-process)


Solution

  • Problems.

    There are ways to check if an orcestration is running before you call it, but consider that this is a distributed system where everything is asynchronous.

    Let's say you do this (imaginary code):

    if (await OrchestratorNotRunning()) // Some imaginary function.
    {
        await client.ScheduleNewOrchestrationInstanceAsync(nameof(FunctionNames.BuildingsOrchestration));
    }
    

    On line 1 you've checked it's not running, so you go into the if block. But by line 2 something else might have already re-triggered the orchestration. It might not even be you that's triggered it; maybe it's a retry proess.

    So, it's never safe to use your approach of checking in advance. It will never be reliable. Maybe that's enough for you, but it's hard to say without knowing your use-case.

    A different approach.

    Don't worry about calling the Orchestration many times. Instead, let the Orchestration stop itself from running a second instance.

    This way, your starter code (above) would call the Orchestration without bothering to check first.

    There are a few ways to do this, depending on your needs.

    Option 1: Locks

    Rethink your logic so that you allow Orchestrations to run many times, but only once for a given domain entity. (again, need to understand your use-case.)

    In the Orchestration, you'd use a Lock to make sure that it isn't already running for that entity. Locks let you take fine-grained control where you can allow it to run multiple instances as long as it's for different source conditions.

    Taking a lock might look like this:

    [Function(nameof(BuildingsOrchestration))]
    public async Task BuildingsOrchestration(
        [OrchestrationTrigger] IDurableOrchestrationContext context,
        ILogger log)
    {
        // This Orchestration is working on a specific entity
        // or piece of data, so get that ID from the parameters.
        var entityIdToProcess = context.GetInput<MyBuildingOrchestrationParams>()?.Id
            ?? throw new InvalidOperationException();
    
        // Check if this Orchestration is already being used by taking a Lock.
        // This lets you lock for a specific entityId (see above) so the
        // Orchestration can still run for other entities.
        using (await context.LockAsync(entityIdToProcess))
        {
            // Your actual Orchestrator code in here.
        }
    }
    

    If the Orchestrator is already running, for your chosen Entity ID, then it will immediately exit and throw an Exception. You can handle this however is appropriate for your use-case.

    https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-orchestrations?tabs=csharp-isolated#critical-sections-durable-functions-2x-currently-net-only

    Option 2: Singleton

    If you want a simpler level of control then this Singleton approach might work. (I don't have any personal experience of this.)

    Singleton info here: https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-singletons?tabs=csharp