Edit

Share via


Eternal orchestrations

Eternal orchestrations are orchestrator functions that never end. They're useful when you want to use Durable Functions for aggregators, and any scenario that requires an infinite loop.

Eternal orchestrations are orchestrations that never end. They're useful when you want to use durable orchestrations for aggregators and any scenario that requires an infinite loop.

Important

Currently, the PowerShell Durable Task SDK isn't available.

Orchestration history

As explained in the orchestration history topic, the Durable Task Framework keeps track of the history of each function orchestration. This history grows continuously as long as the orchestrator function schedules new work. If the orchestrator function goes into an infinite loop and continuously schedules work, the history can grow critically large and cause significant performance problems. The eternal orchestration concept was designed to mitigate these kinds of problems for applications that need infinite loops.

The Durable Task SDKs keep track of the history of each orchestration. This history grows continuously as long as the orchestration schedules new work. If the orchestration goes into an infinite loop and continuously schedules work, the history can grow critically large and cause significant performance problems. The eternal orchestration concept was designed to mitigate these kinds of problems for applications that need infinite loops.

Resetting and restarting

Instead of using infinite loops, orchestrator functions reset their state by calling the continue-as-new method of the orchestration trigger binding. This method takes a JSON-serializable parameter that becomes the new input for the next orchestrator function generation.

When you call continue-as-new, the orchestration instance restarts itself with the new input value. The same instance ID is kept, but the orchestrator function's history resets.

Instead of using infinite loops, orchestrations reset their state by calling the continue-as-new method on the orchestration context. This method takes a JSON-serializable parameter that becomes the new input for the next orchestration generation.

When you call continue-as-new, the orchestration instance restarts itself with the new input value. The same instance ID is kept, but the orchestration's history resets.

Eternal orchestration considerations

Keep these considerations in mind when using the continue-as-new method in an orchestration:

  • When an orchestrator function is reset by using the continue-as-new method, the Durable Task Framework maintains the same instance ID but internally creates and uses a new execution ID going forward. This execution ID isn't exposed externally, but it's useful when debugging orchestration execution.

  • When an unhandled exception occurs during execution, the orchestration enters a failed state and execution terminates. In this state, a call to continue-as-new from the finally block of a try-catch statement can't restart the orchestration.

Important

If the orchestration encounters an uncaught exception during execution, the orchestration enters a "failed" state and execution completes. In particular, this means that a call to continue-as-new, even in a finally block, does not restart the orchestration in the case of an uncaught exception.

Keep these considerations in mind when using the continue-as-new method in an orchestration:

  • When an orchestration is reset by using the continue-as-new method, the Durable Task SDKs maintain the same instance ID but internally create and use a new execution ID going forward. This execution ID isn't exposed externally, but it can be useful when debugging orchestration execution.

  • When an unhandled exception occurs during execution, the orchestration enters a failed state and execution terminates. In this state, a call to continue-as-new from the finally block of a try-catch statement can't restart the orchestration.

  • The results of any incomplete tasks are discarded when an orchestration calls continue-as-new. For example, if a timer is scheduled and then continue-as-new is called before the timer fires, the timer event is discarded.

  • You can optionally preserve unprocessed external events across continue-as-new restarts. In .NET and Java, continue-as-new preserves unprocessed events by default. In Python, continue_as_new doesn't preserve events unless save_events=True. In JavaScript, continueAsNew requires a saveEvents parameter (true or false) to control this behavior. In all cases, unprocessed events are delivered when the orchestration next calls waitForExternalEvent or wait_for_external_event.

Important

If the orchestration encounters an uncaught exception during execution, the orchestration enters a "failed" state and execution completes. In particular, this means that a call to continue-as-new, even in a finally block, does not restart the orchestration in the case of an uncaught exception.

Periodic work example

One use case for eternal orchestrations is code that does periodic work indefinitely.

[FunctionName("Periodic_Cleanup_Loop")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    await context.CallActivityAsync("DoCleanup", null);

    // sleep for one hour between cleanups
    DateTime nextCleanup = context.CurrentUtcDateTime.AddHours(1);
    await context.CreateTimer(nextCleanup, CancellationToken.None);

    context.ContinueAsNew(null);
}

Note

The previous C# example is for Durable Functions 2.x. For Durable Functions 1.x, you must use DurableOrchestrationContext instead of IDurableOrchestrationContext. For more information about the differences between versions, see the Durable Functions versions article.

The difference between this example and a timer-triggered function is that cleanup trigger times aren't based on a schedule. For example, a CRON schedule that runs a function every hour runs at 1:00, 2:00, 3:00, and so on, and could potentially run into overlap issues. In this example, if the cleanup takes 30 minutes, then it schedules at 1:00, 2:30, 4:00, and so on, and there's no chance of overlap.

public class PeriodicCleanupLoop : TaskOrchestrator<object?, object?>
{
    public override async Task<object?> RunAsync(TaskOrchestrationContext context, object? input)
    {
        await context.CallActivityAsync("DoCleanup");

        // sleep for one hour between cleanups
        await context.CreateTimer(TimeSpan.FromHours(1), CancellationToken.None);

        context.ContinueAsNew(null);
        return null;
    }
}

The difference between this example and a timer-based approach is that cleanup trigger times aren't based on a schedule. For example, a schedule that runs every hour runs at 1:00, 2:00, 3:00, and so on, and could potentially run into overlap issues. In this example, if the cleanup takes 30 minutes, then it schedules at 1:00, 2:30, 4:00, and so on, and there's no chance of overlap.

Start an eternal orchestration

Use the start-new or schedule-new durable client method to start an eternal orchestration, just like you would for any other orchestration function.

Note

If you need to ensure a singleton eternal orchestration is running, maintain the same instance id when starting the orchestration. For more information, see Instance management.

[FunctionName("Trigger_Eternal_Orchestration")]
public static async Task<HttpResponseMessage> OrchestrationTrigger(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestMessage request,
    [DurableClient] IDurableOrchestrationClient client)
{
    string instanceId = "StaticId";

    await client.StartNewAsync("Periodic_Cleanup_Loop", instanceId); 
    return client.CreateCheckStatusResponse(request, instanceId);
}

Note

The previous code is for Durable Functions 2.x. For Durable Functions 1.x, use the OrchestrationClient attribute instead of the DurableClient attribute, and use the DurableOrchestrationClient parameter type instead of IDurableOrchestrationClient. For more information about the differences between versions, see Durable Functions versions.

Use the schedule-new client method to start an eternal orchestration, just like you would for any other orchestration.

Note

If you need to ensure a singleton eternal orchestration is running, maintain the same instance id when starting the orchestration.

string instanceId = "StaticId";
await client.ScheduleNewOrchestrationInstanceAsync(
    "PeriodicCleanupLoop",
    null,
    new StartOrchestrationOptions { InstanceId = instanceId });

Exit from an eternal orchestration

If an orchestrator function needs to eventually complete, don't call ContinueAsNew and let the function exit.

If an orchestrator function is in an infinite loop and needs to be stopped, use the terminate API of the orchestration client binding to stop it. For more information, see Instance management.

If an orchestration needs to eventually complete, don't call continue-as-new and let the orchestration exit.

If an orchestration is in an infinite loop and needs to be stopped, use the terminate API on the durable task client to stop it.

await client.TerminateInstanceAsync(instanceId, "Cleanup no longer needed");

Next steps