Skip to content

when_all hangs with shared or duplicate pending task references #285

Closed

Description

@andystaples

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions