Azure / azure-functions-durable-extension

Durable Task Framework extension for Azure Functions
MIT License
716 stars 271 forks source link

IDurableOrchestrationClient.GetStatusAsync(string instanceId) returns null if orchestration threw an exception #1449

Open dixonte opened 4 years ago

dixonte commented 4 years ago

Description

I have a long running durable function I'm calling from a logic app. Due to issue #1446, I have created my own 202 Accepted response code and an endpoint that reports the status of the orchestration via GetStatusAsync.

Not sure if it's relevant, but I am using an app service plan and linux hosting, but the same issue happens when running the function locally.

Expected behavior

I would expect GetStatusAsync to return description of the failure via a DurableOrchestrationStatus object when called after an instance has failed.

Actual behavior

GetStatusAsync returns null, as if the instance is being immediately deleted upon failure.

Relevant source code snippets

            // Function input comes from the request content.
            string instanceId = await starter.StartNewAsync(nameof(ApplyPnPTemplates_Orchestration), null, input);

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

            return await starter.WaitForCompletionOrCreateCheckStatusResponseAsync(req, instanceId, timeout).OverrideCheckStatusResponse(instanceId);
    public static class HttpResponseHelper
    {
        public static async Task<HttpResponseMessage> OverrideCheckStatusResponse(this Task<HttpResponseMessage> task, string instanceId)
        {
            var res = await task;
            var req = res.RequestMessage;

            if (res.StatusCode == System.Net.HttpStatusCode.Accepted)
            {
                res = req.CreateAcceptedResponse(instanceId);
            }

            return res;
        }

        public static HttpResponseMessage CreateAcceptedResponse(this HttpRequestMessage req, string instanceId)
        {
            var port = req.RequestUri.IsDefaultPort ? "" : $":{req.RequestUri.Port}";
            var checkStatusUri = $"{(SettingsHelper.ForceHTTPS ? "https" : req.RequestUri.Scheme)}://{req.RequestUri.Host}{port}/api/Status/{instanceId}";

            var response = new HttpResponseMessage(System.Net.HttpStatusCode.Accepted)
            {
                Content = new StringContent(JsonConvert.SerializeObject(new
                {
                    Id = instanceId,
                    StatusQueryGetUri = checkStatusUri
                }), Encoding.UTF8, "application/json")
            };

            if (req.Headers.Contains("Request-Context"))
                response.Headers.Add("Request-Context", req.Headers.GetValues("Request-Context").First());
            response.Headers.Add("Location", checkStatusUri);
            response.Headers.Add("Retry-After", "10");

            return response;
        }
    }
    public static class Status
    {
        [FunctionName(nameof(Status))]
        public static async Task<HttpResponseMessage> GetStatus(
            [HttpTrigger(AuthorizationLevel.Anonymous, methods: "get", Route = "Status/{instanceId}")] HttpRequestMessage req,
            [DurableClient] IDurableOrchestrationClient orchestrationClient,
            string instanceId,
            ILogger logger)
        {
            logger.LogInformation($"Getting status for instance {instanceId}");

            // Get the built-in status of the orchestration instance. This status is managed by the Durable Functions Extension. 
            var status = await orchestrationClient.GetStatusAsync(instanceId);

            if (status != null)
            {
                logger.LogInformation($"Status for instance {instanceId} is {status}");

                if (status.RuntimeStatus == OrchestrationRuntimeStatus.Running || status.RuntimeStatus == OrchestrationRuntimeStatus.Pending)
                {
                    return req.CreateAcceptedResponse(instanceId);
                }
                else if (status.RuntimeStatus == OrchestrationRuntimeStatus.Completed)
                {
                    logger.LogInformation("Returning status");

                    return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(JsonConvert.SerializeObject(status), Encoding.UTF8, "application/json") };
                }
            }

            // If status is null, then instance has not been found. Create and return an Http Response with status NotFound (404). 
            //return req.CreateResponse(HttpStatusCode.NotFound, $"Instance '{instanceId}' not found.");
            return new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent($"Instance '{instanceId}' not found.", Encoding.UTF8, "text/plain") };
        }
    }

Known workarounds

Not aware of any workarounds.

App Details

Screenshots

N/A

If deployed to Azure

We have access to a lot of telemetry that can help with investigations. Please provide as much of the following information as you can to help us investigate!

If you don't want to share your Function App or storage account name GitHub, please at least share the orchestration instance ID. Otherwise it's extremely difficult to look up information.

dixonte commented 4 years ago

Additional, the documentation says that the instance should complete with a failed status (https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-error-handling?tabs=csharp#unhandled-exceptions). I would expect to be able to retrieve information on this failed instance.