Summary
A session deadlocks — next() 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
Summary
A session deadlocks —
next()returnsNone,is_complete()isFalse,has_error()isFalse, and nothing further will ever run — whenever anIfInvocationbranch input (true_input/false_input) is fed by a chain that contains anIteratenode. Nodes in that chain are never even prepared, including the plain source node at the head of the chain.If+Iterateis 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
Script (run from repo root with
PYTHONPATH=.)Observed
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_nodeand completes, withif_nodeselecting 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 theIfresolves (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 theIfneeds 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, theIfresolves, and the deferral lifts.With an
Iteratein the chain it deadlocks, because_ExecutionMaterializer.prepare()refuses to materialize an iterator until every one of its input sources has executed:srccan't execute (deferred by the unresolvedIf) →itercan't be prepared →collectcan't be prepared →if_nodecan't be prepared → theIfnever resolves →srcstays deferred. Circular wait.Narrowing the deferral to nodes that are exclusive and not required to materialize the
Ifitself, or resolving theIffrom its already-executedconditionbefore 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:
body → collect → if) — completesIf(collect → plain consumer) — completesfalse_inputinstead oftrue_input, no iterator — completesEnvironment
Reproduced at
a21d74b8bd(currentmain) and atabb324a7d0(#9483 head), identical output. Python 3.12.13.🤖 Generated with Claude Code