Logic Apps has a default limit of 120 seconds on synchronous actions. This is already quite long; it does not make sense to perform actions in a synchronous fashion if they take longer. For such long running tasks, Logic Apps provides two options:
- Polling action pattern: initiate the long running action and interrogate its status on regular time intervals. This is probably the easiest way to implement it, but not the most resource-friendly one.
- Webhook action pattern: initiate the long running action and make sure that after the long running action is executed, a callback URI gets invoked, so the Logic App can continue processing.
As these patterns must execute tasks that require custom code and they need to run in an asynchronous way, potentially for a long time, Azure Durable Functions is definetely the way to go. This blog post describes how we can implement a generic way of handling long-running tasks via Durable Functions, via the webhook action pattern.
The provided code is just there as a starting point, it only focusses on the happy path and does not take into account error handling.
Design
The Logic Apps webhook action invokes the HTTP-trigger starter function. It passes the name of the task that must be executed, the callbackUrl that needs to be invoked when the task is done and the taskDetails required to perform the task. The starter function instantiates a durable orchestrator function and returns some details to monitor the long running process. The orchestrator executes first dynamically the activity function, that is labeled with the provided taskName. Afterwards, the perform callback activity function is called in order to provide status feedback to the Logic App.
This is a very generic design. For each type of long running task that needs to performed, one activity function must be developed. All the rest of the plumbing is reused.
Starter Function
Durable Functions always need to be started from within a “normal” event-trigger Function. When using Logic Apps, it’s normal to go for a HTTP trigger function. This function extracts the required query parameters, reeds the body and instantiates a durable orchestrator. As a response, it returns the default durable function management payload, that is handly to monitor the process afterwards.
[FunctionName("F_TaskExecutorStarter")] public static IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = "task.start")]HttpRequest req, [OrchestrationClient] DurableOrchestrationClient starter, TraceWriter log) { log.Info("C# HTTP trigger function processed a request."); //Get generic parameters from query string string taskName = req.Query["taskName"]; string callbackUrl = req.Query["callbackUrl"]; //Get request body string requestBody = new StreamReader(req.Body).ReadToEnd(); //Create task info to pass along var taskInfo = new TaskInfo(taskName, callbackUrl, requestBody); //Start durable orchestration asynchronously var orchestrationId = starter.StartNewAsync("O_TaskExecutor", taskInfo).Result; //Return response with orchestration mgmt info return (ActionResult)new OkObjectResult(starter.CreateHttpManagementPayload(orchestrationId)); }
Orchestrator Function
The orchestrator function should be non-deterministic. This orchestrator implements the function-chaining pattern. First, an activity function gets started dynamically, based on the taskName value. After that, the perform callback activity function gets invoked. Under the hood, everything is managed by the Durable Task Framework: asynchronously via queues and table storage.
[FunctionName("O_TaskExecutor")] public static async Task<object> TaskExecutor([OrchestrationTrigger]DurableOrchestrationContext ctx, TraceWriter log) { var taskInfo = ctx.GetInput<TaskInfo>(); if (ctx.IsReplaying == false) log.Info(String.Format("Starting activity {0}", taskInfo.Name)); //Call first activity function dynamically var result = await ctx.CallActivityAsync<object>("A_" + taskInfo.Name, taskInfo.Input); //Call next the perform callback activity function result = await ctx.CallActivityAsync<object>("A_PerformCallback", taskInfo.CallbackUrl); return true; }
Activity Functions
The first activity function is just one to simulate a long-running task. It expects the taskDurationInSeconds value and it sleeps for the provided duration. For each long-running task type that you want to support, one activity function must be developed.
[FunctionName("A_SimulateLongRunningTask")] public static async Task<object> TaskExecutor([ActivityTrigger] string taskInput, TraceWriter log) { dynamic longRunningTask = JsonConvert.DeserializeObject(taskInput); //Simulate a long running task, based on the provided duration await Task.Delay(TimeSpan.FromSeconds((int)longRunningTask.taskDurationInSeconds)); return true; }
The second activity function just performs an HTTP request on the provided callbackUrl. Remark that a static HTTP client is used, which is a best practice to reuse clients within Azure Functions.
private static HttpClient httpClient = new HttpClient(); [FunctionName("A_PerformCallback")] public static async Task<object> PerformCallback([ActivityTrigger] string callbackUrl, TraceWriter log) { //Perform an HTTP post on the provided URL var request = new HttpRequestMessage(HttpMethod.Post, callbackUrl); request.Content = new StringContent("OK"); await httpClient.SendAsync(request); return true; }
The Logic App webhook action
From within the Logic App, we need to invoke the Azure Function. Two query parameters need to be provided:
- taskName = SimulateLongRunningTask
- callbackUrl = @{encodeUriComponent(listCallbackUrl())}
The subscribe body must the required info to execute the requested task. In this case, we request a task duration of 700 seconds. This is longer than the Logic Apps action timeout and also longer that the normal Azure Functions timeout.
The Logic App invokes the Azure Functions and waits until the callbackUrl gets invoked.
After the provided duration, the Logic App continues its processing.
Conclusion
Azure Durable Functions is a very powerful serverless framework that allows to run asynchronous and potentially long-running processes. It nicely integrate with Logic Apps, which makes it a perfect combo to execute long running tasks.
There’s one downside about this solution and that’s the monitoring experience of the HTTP Webhook action. When the HTTP Webhook action is waiting, you do not see the URL that was orignally invoked, neither you see its response. These are only visible once the webhook has been completed, which is of course too late for troubleshooting.
It would be good if we could the the webhook request already when the HTTP webhook action is still waiting for the callback.
It contains very valuable information for troubleshooting:
The statusQueryGetUri is for example very valuable to know in what state the process is:
Hopefully this was a useful post! Sharing is caring!
Cheers,
Toon