Skip to content

Session deadlocks (never completes, no error) when an If node's branch input comes from a chain containing an Iterate node #9484

Description

@lstein

Summary

A session deadlocksnext() returns None, is_complete() is False, has_error() is False, and nothing further will ever run — whenever an IfInvocation branch input (true_input / false_input) is fed by a chain that contains an Iterate node. Nodes in that chain are never even prepared, including the plain source node at the head of the chain.

If + Iterate is a natural combination (conditionally use a batch of results), so this is easy to hit from the workflow editor.

Found while adversarially reviewing #9483. It is not caused by that PR — reproduced identically at a21d74b8bd (its base) and at the PR head.

Repro

src → iterate → body → collect ──true_input──→ if
                       cond ─────condition───→ if
Script (run from repo root with PYTHONPATH=.)
from invokeai.app.invocations.logic import IfInvocation
from invokeai.app.invocations.primitives import BooleanInvocation
from invokeai.app.services.shared.graph import CollectInvocation, Graph, GraphExecutionState, IterateInvocation
from tests.test_nodes import (
    PromptCollectionTestInvocation,
    PromptTestInvocation,
    create_edge,
    run_session_with_mock_context,
)

g = Graph()
for n in [
    PromptCollectionTestInvocation(id="src", collection=["a", "b"]),
    IterateInvocation(id="iter"),
    PromptTestInvocation(id="body"),
    CollectInvocation(id="collect"),
    BooleanInvocation(id="cond", value=True),
    IfInvocation(id="if_node"),
]:
    g.add_node(n)
for e in [
    ("src", "collection", "iter", "collection"),
    ("iter", "item", "body", "prompt"),
    ("body", "prompt", "collect", "item"),
    ("collect", "collection", "if_node", "true_input"),
    ("cond", "value", "if_node", "condition"),
]:
    g.add_edge(create_edge(*e))

session = GraphExecutionState(graph=g)
run_session_with_mock_context(session)

print("is_complete:", session.is_complete())
print("has_error: ", session.has_error())
print("next():    ", session.next())
for node_id in g.nodes:
    prepared = session.source_prepared_mapping.get(node_id)
    if prepared is None:
        print(f"  {node_id}: never prepared")
    else:
        print(f"  {node_id}: {[session._get_prepared_exec_metadata(p).state for p in prepared]}")

Observed

is_complete: False
has_error:  False
next():     None
  src: ['pending']
  iter: never prepared
  body: never prepared
  collect: never prepared
  cond: ['executed']
  if_node: never prepared

The session stops with five of six nodes unexecuted and reports neither completion nor error. A real queue item would sit in this state forever.

Expected

The session runs src → iter → body → collect → if_node and completes, with if_node selecting the collected list.

Analysis

_IfBranchScheduler.get_branch_exclusive_sources() expands each branch input to all of its ancestors and marks the exclusive ones as deferred until the If resolves (is_deferred_by_unresolved_if). In this graph that set is {collect, body, iter, src} — the whole upstream chain, right back to the plain source node.

That deferral is only survivable if prepare() can still materialize the branch nodes, because resolving the If needs its own exec node, which needs its parents prepared. Without an iterator that works (see the negative cases below): the whole chain is prepared in one pass, the If resolves, and the deferral lifts.

With an Iterate in the chain it deadlocks, because _ExecutionMaterializer.prepare() refuses to materialize an iterator until every one of its input sources has executed:

not isinstance(self._state.graph.get_node(node_id), IterateInvocation)
or all(source_id in self._state.executed for source_id, _ in g.in_edges(node_id))

src can't execute (deferred by the unresolved If) → iter can't be prepared → collect can't be prepared → if_node can't be prepared → the If never resolves → src stays deferred. Circular wait.

Narrowing the deferral to nodes that are exclusive and not required to materialize the If itself, or resolving the If from its already-executed condition before its exec node exists, would both break the cycle.

Negative cases (all complete correctly)

Each isolates one ingredient, confirming it's the combination that deadlocks:

  • same graph without the iterator (body → collect → if) — completes
  • same graph without the If (collect → plain consumer) — completes
  • collector wired to false_input instead of true_input, no iterator — completes

Environment

Reproduced at a21d74b8bd (current main) and at abb324a7d0 (#9483 head), identical output. Python 3.12.13.

🤖 Generated with Claude Code

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