Repository navigation
when_all hangs with shared or duplicate pending task references #285
Description
Summary
when_all can remain pending after all leaf tasks complete when a pending task is shared between composite groups or appears multiple times in one group. This is the same bug addressed by microsoft/durabletask-js#383.
Confirmed against main at commit 912d33573b4c833556695950ef5ce2b9a24191e9, by executing the unmodified task classes extracted from durabletask/task.py (no scheduler required).
Reproduction
from durabletask.task import CompletableTask, when_all
# Shared pending task: constructing right overwrites a's parent.
a, b, c = CompletableTask(), CompletableTask(), CompletableTask()
left = when_all([a, b])
right = when_all([a, c])
root = when_all([left, right])
a.complete("A")
b.complete("B")
c.complete("C")
print(left.is_complete, right.is_complete, root.is_complete, left.pending_tasks)
# Actual: False True False 1
# Expected: True True True 0
# Expected root result: [["A", "B"], ["A", "C"]]
# Duplicate pending task: one completion callback accounts for only one slot.
task = CompletableTask()
group = when_all([task, task])
task.complete("V")
print(task.is_complete, group.is_complete, group.pending_tasks)
# Actual: True False 1
# Expected: True True 0
# Expected group result: ["V", "V"]Pre-completing the duplicate child before constructing when_all([task, task]) succeeds, so behavior depends on completion timing.
Root cause
Task._notify_parentnotifies a single_parent.CompositeTask.__init__assignstask._parent = self, overwriting an earlier observer.WhenAllTask.on_child_completedincrements the completed count by one, even when that child occupies multiple input positions.
The same single-parent mechanism affects overlapping when_all/when_any groups and nested shared composites, including failed children and cancellable/retryable tasks.
Impact
An orchestration yielding the stranded group may remain running indefinitely despite every scheduled activity having completed. Retrying or replaying does not repair the dependency graph.
Suggested fix and regression coverage
Support multiple dependent composites per task, account for duplicate input occurrences exactly once per terminal child, and preserve input order/duplicate result positions. Detach a completed when_any from its losing children without detaching other observers. Preserve existing wait-all failure semantics and ensure one throwing completion callback cannot prevent other observers from being notified.
Cover pending and pre-completed duplicates, all completion orders for shared groups, nested composites, overlapping all/any groups, success/failure/cancellation/retry exhaustion, and an orchestration-level regression proving completion without extra activity scheduling.
Activity