@@ -57,6 +57,11 @@ type dagContext struct {
5757 // are only computed once per operation
5858 dependsLogic map [string ]string
5959
60+ // taskGroupsToComplete collects the names of TaskGroup nodes that assessDAGPhase
61+ // found stuck Running with all of their children fulfilled, mapped to the phase
62+ // they should complete with. executeDAG marks them once assessment is done.
63+ taskGroupsToComplete map [string ]wfv1.NodePhase
64+
6065 // used for logging in the dag
6166 log logging.Logger
6267}
@@ -169,11 +174,23 @@ func (d *dagContext) assessDAGPhase(ctx context.Context, targetTasks []string, n
169174 branchPhase := curr .phase
170175
171176 if ! node .Fulfilled () {
172- return wfv1 .NodeRunning , nil
173- }
174-
175- // Only overwrite the branchPhase if this node completed. (If it didn't we can just inherit our parent's branchPhase).
176- if node .Completed () {
177+ // A fan-out TaskGroup can be left Running with every expanded child
178+ // already fulfilled, for example when a retry resets the group but never
179+ // re-runs it because its dependents have already completed. executeDAGTask
180+ // only visits unfulfilled tasks, so it never revisits such a group, which
181+ // would then hold the DAG Running forever. Complete it from its children
182+ // instead of blocking here.
183+ groupPhase , ok := completableTaskGroupPhase (node , nodes )
184+ if ! ok {
185+ return wfv1 .NodeRunning , nil
186+ }
187+ if d .taskGroupsToComplete == nil {
188+ d .taskGroupsToComplete = make (map [string ]wfv1.NodePhase )
189+ }
190+ d .taskGroupsToComplete [node .Name ] = groupPhase
191+ branchPhase = groupPhase
192+ } else if node .Completed () {
193+ // Only overwrite the branchPhase if this node completed. (If it didn't we can just inherit our parent's branchPhase).
177194 branchPhase = node .Phase
178195 }
179196
@@ -227,6 +244,28 @@ func (d *dagContext) assessDAGPhase(ctx context.Context, targetTasks []string, n
227244 return result , nil
228245}
229246
247+ // completableTaskGroupPhase reports whether node is a TaskGroup that is not yet
248+ // fulfilled even though all of its expanded children are, and if so the phase it
249+ // should complete with (Succeeded unless a child failed or errored, matching the
250+ // aggregation executeDAGTask uses). Such a group is never revisited by
251+ // executeDAGTask, so it must be completed during DAG assessment.
252+ func completableTaskGroupPhase (node * wfv1.NodeStatus , nodes wfv1.Nodes ) (wfv1.NodePhase , bool ) {
253+ if node .Type != wfv1 .NodeTypeTaskGroup || len (node .Children ) == 0 {
254+ return "" , false
255+ }
256+ phase := wfv1 .NodeSucceeded
257+ for _ , childID := range node .Children {
258+ child , err := nodes .Get (childID )
259+ if err != nil || ! child .Fulfilled () {
260+ return "" , false
261+ }
262+ if child .FailedOrError () {
263+ phase = child .Phase
264+ }
265+ }
266+ return phase , true
267+ }
268+
230269func (woc * wfOperationCtx ) executeDAG (ctx context.Context , nodeName string , tmplCtx * templateresolution.TemplateContext , templateScope string , tmpl * wfv1.Template , orgTmpl wfv1.TemplateReferenceHolder , opts * executeTemplateOpts ) (* wfv1.NodeStatus , error ) {
231270
232271 node , err := woc .wf .GetNodeByName (nodeName )
@@ -320,6 +359,13 @@ func (woc *wfOperationCtx) executeDAG(ctx context.Context, nodeName string, tmpl
320359 return nil , err
321360 }
322361
362+ // Complete any orphaned TaskGroups that assessment found stuck Running with all
363+ // children fulfilled. Done regardless of the overall DAG phase so a group is
364+ // healed even while other tasks are still legitimately running.
365+ for name , phase := range dagCtx .taskGroupsToComplete {
366+ woc .markNodePhase (ctx , name , phase )
367+ }
368+
323369 switch dagPhase {
324370 case wfv1 .NodeRunning :
325371 return node , nil
0 commit comments