Azure-functions-durable-extension: Question on usage of IDurableOrchestrationClient.StartNewAsync() in Durable Function 2.0.0

Created on 18 Nov 2019  路  9Comments  路  Source: Azure/azure-functions-durable-extension

Description

I just switched to Durable Functions extension 2.0. I have a simple Timer Triggered function that starts an orchestrator with some input. In v1 I had this:

int input = 1;
string instanceId = await starter.StartNewAsync("UpdateHutsOrchestrator", input);

after switching to v2 I had to change to

string instanceId = await starter.StartNewAsync("UpdateHutsOrchestrator", null, input);

for this to work again (setting the instanceId parameter to null).
However, when input is neither int nor string, for instance List<int> the old behavior still works in v2:

List<int> input = new List<int>();
string instanceId = await starter.StartNewAsync("UpdateHutsOrchestrator", input);

Could you explain why the behaviour is different for different types?

App Details

  • Durable Functions extension version (e.g. v1.8.3): 2.0.0
  • Azure Functions runtime version (1.0 or 2.0): 2.0
  • Programming language used: C#
Needs question

Most helpful comment

I think it is a matter of overloaded methods. Looks like when using a string primitive as input, it will be regarded as the instanceId:

public System.Threading.Tasks.Task<string> StartNewAsync<T> (string orchestratorFunctionName, string instanceId, T input);

In these cases, you should pass null as instanceid

See the overloads:
https://docs.microsoft.com/pt-br/dotnet/api/microsoft.azure.webjobs.extensions.durabletask.durablecontextextensions.startnewasync

All 9 comments

I think it is a matter of overloaded methods. Looks like when using a string primitive as input, it will be regarded as the instanceId:

public System.Threading.Tasks.Task<string> StartNewAsync<T> (string orchestratorFunctionName, string instanceId, T input);

In these cases, you should pass null as instanceid

See the overloads:
https://docs.microsoft.com/pt-br/dotnet/api/microsoft.azure.webjobs.extensions.durabletask.durablecontextextensions.startnewasync

We found that some users were accidentally passing what they thought was an instance ID instead as the input argument, so we accepted a PR that changed the method signature to prevent this mistake. You can effectively get the previous behavior if you call the StartNewAsync(...) overload instead of calling the non-generic overload.

This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 4 days. It will be closed if no further activity occurs within 3 days of this comment.

sorry for the late reply. It makes sense for string input. But why is the behavior the same for int input?

To elaborate, the PR required that users, when using the 2-parameter overload, specify that the 2nd parameter is the _payload_ and not the _instanceId_ (in particular in the case of string payload) by specifying the type argument (in most cases this ends up being <string> for those affected).

for instance in v1 you'd do something like this:

string input = "doThis";
string instanceId = await starter.StartNewAsync("UpdateHutsOrchestrator", input);

thinking you'd just sent the args in to your orchestrator, but in fact you send an instanceId of doThis instead, and you'd need to add the null as the second param then your argument (much like how you cited you needed to change your code).

in v2, this should get called this to your attention by giving you an "ambiguous reference" compile-time error. you'd get the old behavior by simply adding <string> to your call like this:

string input = "doThis";
string instanceId = await starter.StartNewAsync<string>("UpdateHutsOrchestrator", input);

you should _not_ have had to add anything when using any arg type other than string so it's curious to me that your argument is int and you needed to make this change.

Could you by chance share a sample that repros? Hopefully it's a misunderstanding or, at worst, something we can fix w/o a breaking change.

Thanks!

sure, we look if I can repro and post the code here then

So this works without error:
string instanceId = await starter.StartNewAsync<string>(nameof(UpdateHutsOrchestrator), "abc");

This does not:
string instanceId = await starter.StartNewAsync<int>(nameof(UpdateHutsOrchestrator), 1);

image

Ah yes... the key here lies in the definition of StartNewAsync<T>:

public static Task<string> StartNewAsync<T>(
    this IDurableOrchestrationClient client,
    string orchestratorFunctionName,
    T input)
    where T : class
...

notice the Type restriction: where T : class. In your scenario int is not a class so that's why this fails.

Why do we have this restriction?
Internally, whenever you _weren't_ passing input to a new orchestrator, we do this:

public static Task<string> StartNewAsync(
    this IDurableOrchestrationClient client,
    string orchestratorFunctionName,
    string instanceId)
{
    return client.StartNewAsync<object>(orchestratorFunctionName, instanceId, null);
}

notice the usage of null here. If your orchestrator accepted int but we sent null that would be a mismatch - or at worst it just wouldn't get fired. The type restriction : class ensures we _can_ send null to your target "safely" (assuming you handle this case).

I'm open to relaxing this restriction (wouldn't be a breaking change), but I think in your scenario what would be more ideal would be perhaps to pass a JObject that contains your int w/ a meaningful property name. e.g.

{
  "things" : 1
}

or even an anonymous type (with your orchestrator taking JObject as parameter type):

await client.StartNewAsync("myfunc", new { things = 1 });
[FunctionName("myfunc")]
public static int RunMyFunc(
    [OrchestrationTrigger]IDurableOrchestrationContext ctx)
{
    dynamic input = ctx.GetInput<JObject>();

    return (int)input.Things;
}

TBH I'm not entirely sure what would happen if your orchestrator took int, but you kicked it off w/ no parameter. I don't think it'd get triggered at all (because types wouldn't match up so reflection wouldn't find it)

Internally, when we call CreateOrchestrationInstanceAsync(), that takes an object as its parameter (not strongly typed) which is why the type restriction made sense for us:

async Task<string> IDurableOrchestrationClient.StartNewAsync<T>(string orchestratorFunctionName, string instanceId, T input)
{
    this.config.ThrowIfFunctionDoesNotExist(orchestratorFunctionName, FunctionType.Orchestrator);

    if (string.IsNullOrEmpty(instanceId))
    {
        instanceId = Guid.NewGuid().ToString("N");
    }
    else if (instanceId.StartsWith("@"))
    {
        throw new ArgumentException(nameof(instanceId), "Orchestration instance ids must not start with @.");
    }
    else if (instanceId.Any(IsInvalidCharacter))
    {
        throw new ArgumentException(nameof(instanceId), "Orchestration instance ids must not contain /, \\, #, ?, or control characters.");
    }

    if (instanceId.Length > MaxInstanceIdLength)
    {
        throw new ArgumentException($"Instance ID lengths must not exceed {MaxInstanceIdLength} characters.");
    }

    Task<OrchestrationInstance> createTask = this.client.CreateOrchestrationInstanceAsync(
        orchestratorFunctionName, DefaultVersion, instanceId, input);
...

and, unfortunately, this is part of the DurableTaskFramework:

public Task<OrchestrationInstance> CreateOrchestrationInstanceAsync(Type orchestrationType, string instanceId, object input);
public Task<OrchestrationInstance> CreateOrchestrationInstanceAsync(Type orchestrationType, string instanceId, object input, OrchestrationStatus[] dedupeStatuses);
public Task<OrchestrationInstance> CreateOrchestrationInstanceAsync(string name, string version, object input);
public Task<OrchestrationInstance> CreateOrchestrationInstanceAsync(string name, string version, string instanceId, object input);
public Task<OrchestrationInstance> CreateOrchestrationInstanceAsync(string name, string version, string instanceId, object input, IDictionary<string, string> tags);
public Task<OrchestrationInstance> CreateOrchestrationInstanceAsync(string name, string version, string instanceId, object input, IDictionary<string, string> tags, OrchestrationStatus[] dedupeStatuses);
public Task<OrchestrationInstance> CreateOrchestrationInstanceAsync(Type orchestrationType, object input);

which is a core piece of Durable Functions but also used by other libraries so would be a much "bigger" change to strongly-type it w/ generics

I know this probably isn't the answer you were looking/hoping for, but I think it's a safer implementation to keep from unwanted/unexpected behavior you'd get if you took a value type as your param and didn't send a value for it, or unintentionally somehow sent null

cc @cgillum for awareness

thanks for the deep explaination @brandonh-msft It is ok for me as it is. I just wanted to understand the behavior - and why in certain cases it seemed to differ. So I'm all good now :)

Was this page helpful?
0 / 5 - 0 ratings