From 225a163a8823c72118520776c333c7692f6d2be5 Mon Sep 17 00:00:00 2001 From: Dean Chen <862469039@qq.com> Date: Mon, 24 Aug 2026 02:41:22 +0500 Subject: [PATCH] service/progress: treat completed one-shot tasks as converged stack deploy --detach=false waited forever for replicated services whose tasks exit 0 under restart_policy none/on-failure, because the waiter only counted Running. Swarm already marks those tasks DesiredState=Complete; count that as done too. Fixes #5299 Signed-off-by: Dean Chen <862469039@qq.com> --- cli/command/service/progress/progress.go | 41 ++++++++++++++-- cli/command/service/progress/progress_test.go | 48 +++++++++++++++++++ 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/cli/command/service/progress/progress.go b/cli/command/service/progress/progress.go index 36f5d19e465a..f59925219197 100644 --- a/cli/command/service/progress/progress.go +++ b/cli/command/service/progress/progress.go @@ -68,6 +68,17 @@ func terminalState(state swarm.TaskState) bool { return numberedStates[state] > numberedStates[swarm.TaskStateRunning] } +// taskConverged reports whether a slot/node has reached a state that +// --detach=false can treat as done. Running is the usual case; Complete is +// for one-shot tasks (restart_policy none/on-failure, process exited 0) +// whose DesiredState the orchestrator has already set to complete. +func taskConverged(task swarm.Task) bool { + if !terminalState(task.DesiredState) && task.Status.State == swarm.TaskStateRunning { + return true + } + return task.DesiredState == swarm.TaskStateComplete && task.Status.State == swarm.TaskStateComplete +} + // ServiceProgress outputs progress information for convergence of a service. // //nolint:gocyclo @@ -316,7 +327,7 @@ func (u *replicatedProgressUpdater) update(service swarm.Service, tasks []swarm. // If we had reached a converged state, check if we are still converged. if u.done { for _, task := range tasksBySlot { - if task.Status.State != swarm.TaskStateRunning { + if !taskConverged(task) { u.done = false break } @@ -332,7 +343,7 @@ func (u *replicatedProgressUpdater) update(service swarm.Service, tasks []swarm. u.slotMap[task.Slot] = mappedSlot } - if !terminalState(task.DesiredState) && task.Status.State == swarm.TaskStateRunning { + if taskConverged(task) { running++ } @@ -395,6 +406,17 @@ func (u *replicatedProgressUpdater) writeTaskProgress(task swarm.Task, mappedSlo return } + if task.DesiredState == swarm.TaskStateComplete && task.Status.State == swarm.TaskStateComplete { + u.progressOut.WriteProgress(progress.Progress{ + ID: fmt.Sprintf("%d/%d", mappedSlot, replicas), + Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State), + Current: maxProgress, + Total: maxProgress, + HideCounts: true, + }) + return + } + if !terminalState(task.DesiredState) && !terminalState(task.Status.State) { u.progressOut.WriteProgress(progress.Progress{ ID: fmt.Sprintf("%d/%d", mappedSlot, replicas), @@ -441,7 +463,7 @@ func (u *globalProgressUpdater) update(_ swarm.Service, tasks []swarm.Task, acti // If we had reached a converged state, check if we are still converged. if u.done { for _, task := range tasksByNode { - if task.Status.State != swarm.TaskStateRunning { + if !taskConverged(task) { u.done = false break } @@ -452,7 +474,7 @@ func (u *globalProgressUpdater) update(_ swarm.Service, tasks []swarm.Task, acti for _, task := range tasksByNode { if _, nodeActive := activeNodes[task.NodeID]; nodeActive { - if !terminalState(task.DesiredState) && task.Status.State == swarm.TaskStateRunning { + if taskConverged(task) { running++ } @@ -512,6 +534,17 @@ func (u *globalProgressUpdater) writeTaskProgress(task swarm.Task, nodeCount int return } + if task.DesiredState == swarm.TaskStateComplete && task.Status.State == swarm.TaskStateComplete { + u.progressOut.WriteProgress(progress.Progress{ + ID: formatter.TruncateID(task.NodeID), + Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State), + Current: maxProgress, + Total: maxProgress, + HideCounts: true, + }) + return + } + if !terminalState(task.DesiredState) && !terminalState(task.Status.State) { u.progressOut.WriteProgress(progress.Progress{ ID: formatter.TruncateID(task.NodeID), diff --git a/cli/command/service/progress/progress_test.go b/cli/command/service/progress/progress_test.go index c6addc39fd54..27c0fe7d63b5 100644 --- a/cli/command/service/progress/progress_test.go +++ b/cli/command/service/progress/progress_test.go @@ -169,6 +169,54 @@ func TestReplicatedProgressUpdaterOneReplica(t *testing.T) { }) } +func TestReplicatedProgressUpdaterCompleteOneShot(t *testing.T) { + replicas := uint64(1) + + service := swarm.Service{ + Spec: swarm.ServiceSpec{ + Mode: swarm.ServiceMode{ + Replicated: &swarm.ReplicatedService{ + Replicas: &replicas, + }, + }, + }, + } + + p := &mockProgress{} + ut := updaterTester{ + t: t, + updater: &replicatedProgressUpdater{ + progressOut: p, + }, + p: p, + activeNodes: map[string]struct{}{"a": {}}, + service: service, + } + + // One-shot task (restart_policy none/on-failure, process exited 0): + // swarm sets DesiredState=Complete. --detach=false used to hang here. + tasks := []swarm.Task{ + { + ID: "1", + NodeID: "a", + Slot: 1, + DesiredState: swarm.TaskStateComplete, + Status: swarm.TaskStatus{State: swarm.TaskStateComplete}, + }, + } + completeBar := fmt.Sprintf("%-[1]*s", longestState, swarm.TaskStateComplete) + ut.testUpdater(tasks, true, + []progress.Progress{ + {ID: "overall progress", Action: "0 out of 1 tasks"}, + {ID: "1/1", Action: " "}, + {ID: "1/1", Action: completeBar, Current: 9, Total: 9, HideCounts: true}, + {ID: "overall progress", Action: "1 out of 1 tasks"}, + }) + + // Stay converged on the next tick; Complete is not Running. + ut.testUpdater(tasks, true, nil) +} + func TestReplicatedProgressUpdaterManyReplicas(t *testing.T) { replicas := uint64(50)