Azure / azure-functions-durable-extension

Durable Task Framework extension for Azure Functions
MIT License
713 stars 268 forks source link

Terminating "completed" or non-existent durable function throws Grpc.Core.Exception #2891

Open KristianJakubik opened 1 month ago

KristianJakubik commented 1 month ago

Description

Terminating durable function that has completed (RuntimeStatus=Failed | Completed | Terminated) or for non-existent instance id throws Grpc.Core.Exception exception with message Status(StatusCode="Unknown", Detail="Exception was thrown by handler.") For functions that are in running status, the termination works as expected and function is successfully terminated.

Expected behavior

Terminating completed or non-existent durable function will fail silently as documentation says. image

Actual behavior

Terminating completed durable function or non-existent throws Grpc.Core.Exception exception with message Status(StatusCode="Unknown", Detail="Exception was thrown by handler.")

Relevant source code snippets

Basically try to create any function in the mentioned "completed" state and try to terminate it.

using System.Threading.Tasks;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;

namespace Functions..TestTask;

public class Function
{
    [Function("TestTaskOrchestrator")]
    public async Task Orchestrator(
        [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        await context.CallActivityAsync("TestTaskInitialize");
    }

    [Function("TestTaskInitialize")]
    public async Task Initialize([ActivityTrigger] object p)
    {
        //throw new Exception("test");
        await Task.Delay(10_000);
    }

    [Function( "Test_HttpTrigger")]
    public async Task<HttpResponseData> HttpTrigger(
        [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData request,
        [DurableClient] DurableTaskClient starter)
    {
        var options = new StartOrchestrationOptions { InstanceId = "testTaskInstanceId" };
        var intanceId = await starter.ScheduleNewOrchestrationInstanceAsync("TestTaskOrchestrator", options);
        return HttpResponseData.CreateResponse(request);
    }

    [Function("Test_HttpTrigger2")]
    public async Task<HttpResponseData> HttpTrigger2(
        [HttpTrigger(AuthorizationLevel.Function, "get")]
        HttpRequestData request,
        [DurableClient]
        DurableTaskClient starter)
    {
        await starter.TerminateInstanceAsync("testTaskInstanceId");
        return HttpResponseData.CreateResponse(request);
    }
}

Known workarounds

The most intuitive workaround is to get the instance, check if it is running and then call the terminationInstanceAsync method. Just a note that there is place for race condition, when the function will complete after GetInstanceAsync call, then the exception will be thrown anyway and we are not handling case with the non-existent function. This workaround is fragile but good enough for our use case for now.

        var taskInstance = await durableClient.GetInstanceAsync(instanceId);
        if (taskInstance.IsRunning)
        {
            await durableClient.TerminateInstanceAsync(instanceId, new TerminateInstanceOptions { Output = reason, Recursive = true });
        }

App Details

lilyjma commented 1 month ago

thanks for reporting this! I've marked it as a bug, so we can prioritize it when we have cycles.