1

As a general practice, we have been injecting our own "service" classes to all our function apps, and we want to do the same thing for the Orchestrator.

Example:

  public async Task<string> Run(
            [OrchestrationTrigger] IDurableOrchestrationContext context,
            ILogger log)
        {
            try
            {
                // get input
                var input = context.GetInput<MyInputType>();

                // do some stuff #1

                var input1 = new BlahBlahOne();
                await context.CallActivityWithRetryAsync<string>("activityfn1", retryOptions, input1);

                // do some stuff #2

                var input1 = new BlahBlahTwo();
                await context.CallActivityWithRetryAsync<string>("activityfn3", retryOptions, input1);

                // do some stuff #3

                var input1 = new BlahBlahThree();
                await context.CallActivityWithRetryAsync<string>("activityfn3", retryOptions, input1);

                // do some stuff #4

                return "I'm done";
            }
            catch (Exception ex)
            {
                log.LogError(ex, "unexpected error");
                throw;
            }
        }

We'd like to do something like this:

  public async Task<string> Run(
            [OrchestrationTrigger] IDurableOrchestrationContext context,
            ILogger log)
        {
            try
            {
                string output = await _myOrchestratorService.RunAsync(context); // NOT allowed! 
                return output
            }
            catch (Exception ex)
            {
                log.LogError(ex, "unexpected error");
                throw;
            }
        }

However, Note that we can't use 'await' as per Durable Functions code constraints on multi-threading. So I tried below, but how do I code it? Calling .Result makes the code 'hang' on the Activity function. What am I doing wrong?

public string Run(IDurableOrchestrationContext context)
{
       // code like before, but then how do I call this?
       // await context.CallActivityWithRetryAsync<string>("activityfn1", retryOptions, input1);

       // I tried this, doesn't work, will hang on the first activity function
       context.CallActivityWithRetryAsync<string>("activityfn1", retryOptions, input1).Result; 
}
Raymond
  • 3,382
  • 5
  • 43
  • 67

2 Answers2

3

To clarify, the restriction on the use of await in Orchestrator functions only applies to tasks that are not generated by IDurableOrchestrationContext APIs. To quote the Durable Functions orchestration code constraint documentation:

Orchestrator code must never start any async operation except by using the IDurableOrchestrationContext API or the context.df object's API. For example, you can't use Task.Run, Task.Delay, and HttpClient.SendAsync in .NET or setTimeout and setInterval in JavaScript. The Durable Task Framework runs orchestrator code on a single thread. It can't interact with any other threads that might be called by other async APIs.

As this answer shows, asynchronous helper methods that only call await on Task objects created by IDurableOrchestrationContext are technically safe to await on as well. That means that your call to await _myOrchestratorService.RunAsync(context); may be alright, as long as that asynchronous method follows all of the normal orchestration code constraints.

All of that being said, I am not entirely sure what you gain by injecting a service that appears to only have a single method that contains all of the logic that would normally live in your orchestration method. That abstraction doesn't appear to improve the testability of the code, and the extra layer of abstraction may confuse our Durable Functions analyzer that is helpful in diagnosing when your code violates orchestration constraints.

Connor McMahon
  • 1,318
  • 7
  • 15
  • the example was trivialized, there is a lot more logic. We use dependency injection etc, so we often also like to unit test the service without testing the Function App specifically. (Yes, I know we can mock out IDurableOrchestrationContext, so it isn't the end of the world). FYI, I've confirmed that the call to await _myOrchestratorService.RunAsync(context); will raise an exception. – Raymond May 11 '20 at 19:21
  • That is very odd, as I have now linked to an answer from another question that verifies that async helper methods should be safe to use. Is the exception you are getting a multithreaded exception? Are you sure that there is nothing else is going on in your RunAsync() method that could cause a multithreaded exception? If not, I would recommend opening a GitHub issue on the [Durable Functions repo](https://github.com/Azure/azure-functions-durable-extension/issues). – Connor McMahon May 12 '20 at 15:59
  • Seems like it DOES work... I had an error in my Service class that was the problem. Inside the Service class, I made a parallel call to my Activity Functions, e.g."myTasks.Add(await context.CallActivityWithRetryAsync...", and then an await Task.WhenAll(myTasks); ... the await inside the "myTasks.Add" was the culprit – Raymond May 15 '20 at 13:45
  • Glad to here it! – Connor McMahon May 15 '20 at 15:44
  • If you do this, _don't_ use `ConfigureAwait(false)`. Otherwise you may get a `MultiThreaded Execution was detected` error, as the DTF will no longer consider it on the orchestrator context. – Arithmomaniac Oct 19 '20 at 12:11
0

Just make it async Task<string>. It should work

public async Task<string> RunAsync(IDurableOrchestrationContext context)
{    
    var result = await context.CallActivityWithRetryAsync<string>("activityfn1", retryOptions, input1);
    return result;
}
Nafis Islam
  • 1,483
  • 1
  • 14
  • 34
  • Thank you for the reply, but the issue is I can't call RunAsync in my Orchestrator because of durable functions code constraints. – Raymond May 11 '20 at 19:23
  • Turns out this does work, see the accepted answer/comments below. Thank you! – Raymond May 15 '20 at 13:45