dapr / dotnet-sdk

Dapr SDK for .NET

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Workflow never completes when waiting on multiple activities unless ToList is used

stuartleeks opened this issue · comments

Expected Behavior

Waiting on multiple activities with Task.WhenAll should work when passing an IEnumerable.

Actual Behavior

When capturing the Tasks from multiple activity invocations, passing the tasks to Task.WhenAll works fine if the tasks are passed as a list. If the tasks are passed as an IEnumerable<Task> then the workflow never completes.

Steps to Reproduce the Problem

I have a workflow project where the set of activities to call is determined based on the payload of an incoming request. In the processing it is possible for multiple activities to be executed in parallel:

// ...

var actionTasks = step.Actions.Select(action =>
		context.CallActivityAsync<InvokeProcessorResult>(nameof(InvokeProcessorActivity), action)
	).ToList(); // if ToList is omitted, the workflow never completes
await Task.WhenAll(actionTasks);

// ...

Release Note

RELEASE NOTE: Fix awaiting multiple activity tasks works with IEnumerable

@stuartleeks Do you have a repro project that you can share? I threw together a project but do not see that behavior; the workflow completes as expected once all the activities complete, whether .ToList() is used or not.

@philliphoff - I've created a stripped back version of the project where I hit this and pushed it here: https://github.com/stuartleeks/dapr-wf-csharp-1211

Hopefully it's easy to set up/run, but let me kjnow if you have any questions

Ok, so found the problem:

	await Task.WhenAll(activityTasks);

	var result = activityTasks.Select(t=>t.Result).ToArray();

...should instead be:

	var result = await Task.WhenAll(activityTasks);

In the original, activityTasks is a LINQ-based lazily-evaluated IEnumerable<Task>. It will first be evaluated by the Task.WhenAll() which causes the activities to be spawned and then awaited. Once that's done, it gets to the activityTasks.Select(t=>t.Result).ToArray(). This causes a second evaluation of the enumerable which spawns the activities again, but then tries to synchronously wait for each to finish (due to the use of Result), which is where I think the problem arises.

Task.WhenAll() returns the results of each awaited Task so that they can be used without reevaluating the original enumerable.

Ah, good spot 🤦

Thanks!