Skip to content

Commit 64b688a

Browse files
committed
Fix when_all to wait for all tasks before surfacing failures
when_all previously failed fast on the first child task failure. It now waits for every child task to complete before surfacing the first failure, matching the semantics of .NET's Task.WhenAll.
1 parent e7b5f15 commit 64b688a

3 files changed

Lines changed: 33 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ ADDED
1111

1212
- Added `TaskHubGrpcClient.rewind_orchestration()` to rewind a failed orchestration instance to its last known good state. Failed activity and sub-orchestration results are removed from the history and the orchestration replays from the last successful checkpoint, retrying only the failed work. The in-memory testing backend supports rewind as well.
1313

14+
FIXED
15+
16+
- Fixed `task.when_all()` failing fast when one of its child tasks failed. It now waits for every child task to complete before surfacing the first failure, matching the semantics of .NET's `Task.WhenAll`.
17+
1418
## v1.7.2
1519

1620
FIXED

durabletask/task.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -562,13 +562,20 @@ def on_child_completed(self, task: Task[Any]) -> None:
562562
if self.is_complete:
563563
raise ValueError('The task has already completed.')
564564
self._completed_tasks += 1
565-
if task.is_failed and self._exception is None:
566-
self._exception = task.get_exception()
567-
self._is_complete = True
565+
if task.is_failed:
566+
self._failed_tasks += 1
567+
if self._exception is None:
568+
self._exception = task.get_exception()
568569
if self._completed_tasks == len(self._tasks):
569-
# The order of the result MUST match the order of the tasks provided to the constructor.
570-
self._result = [child.get_result() for child in self._tasks]
570+
# Only complete once every child task has completed. This matches the
571+
# semantics of .NET's Task.WhenAll: the composite task waits for all
572+
# children to finish and, if any failed, surfaces the first failure
573+
# rather than failing fast on the first error.
571574
self._is_complete = True
575+
if self._exception is None:
576+
# The order of the result MUST match the order of the tasks
577+
# provided to the constructor.
578+
self._result = [child.get_result() for child in self._tasks]
572579

573580
def get_completed_tasks(self) -> int:
574581
return self._completed_tasks

tests/durabletask/test_orchestration_executor.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1615,15 +1615,27 @@ def orchestrator(ctx: task.OrchestrationContext, _):
16151615
i + 1, activity_name, encoded_input=str(i)))
16161616

16171617
# 5 of the tasks complete successfully, 1 fails, and 4 are still running.
1618-
# The expectation is that the orchestration will fail immediately.
1619-
new_events = []
1618+
# when_all must NOT fail fast: it waits for every child task to complete
1619+
# before surfacing a failure (matching .NET's Task.WhenAll semantics), so
1620+
# the orchestration is expected to still be running with zero new actions.
1621+
ex = Exception("Kah-BOOOOM!!!")
1622+
partial_events = []
16201623
for i in range(5):
1624+
partial_events.append(helpers.new_task_completed_event(
1625+
i + 1, encoded_output=print_int(None, i)))
1626+
partial_events.append(helpers.new_task_failed_event(6, ex))
1627+
1628+
executor = worker._OrchestrationExecutor(registry, TEST_LOGGER, JsonDataConverter())
1629+
result = executor.execute(TEST_INSTANCE_ID, old_events, partial_events)
1630+
assert len(result.actions) == 0
1631+
1632+
# Once the remaining 4 tasks also complete, the orchestration fails and
1633+
# surfaces the first task failure.
1634+
new_events = list(partial_events)
1635+
for i in range(6, 10):
16211636
new_events.append(helpers.new_task_completed_event(
16221637
i + 1, encoded_output=print_int(None, i)))
1623-
ex = Exception("Kah-BOOOOM!!!")
1624-
new_events.append(helpers.new_task_failed_event(6, ex))
16251638

1626-
# Now test with the full set of new events. We expect the orchestration to complete.
16271639
executor = worker._OrchestrationExecutor(registry, TEST_LOGGER, JsonDataConverter())
16281640
result = executor.execute(TEST_INSTANCE_ID, old_events, new_events)
16291641
actions = result.actions

0 commit comments

Comments
 (0)