mirror of https://github.com/docker/cli.git
845 lines
24 KiB
Go
845 lines
24 KiB
Go
package progress
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/api/types/swarm"
|
|
"github.com/docker/docker/client"
|
|
"github.com/docker/docker/pkg/progress"
|
|
"github.com/docker/docker/pkg/streamformatter"
|
|
"github.com/docker/docker/pkg/stringid"
|
|
)
|
|
|
|
var (
|
|
numberedStates = map[swarm.TaskState]int64{
|
|
swarm.TaskStateNew: 1,
|
|
swarm.TaskStateAllocated: 2,
|
|
swarm.TaskStatePending: 3,
|
|
swarm.TaskStateAssigned: 4,
|
|
swarm.TaskStateAccepted: 5,
|
|
swarm.TaskStatePreparing: 6,
|
|
swarm.TaskStateReady: 7,
|
|
swarm.TaskStateStarting: 8,
|
|
swarm.TaskStateRunning: 9,
|
|
|
|
// The following states are not actually shown in progress
|
|
// output, but are used internally for ordering.
|
|
swarm.TaskStateComplete: 10,
|
|
swarm.TaskStateShutdown: 11,
|
|
swarm.TaskStateFailed: 12,
|
|
swarm.TaskStateRejected: 13,
|
|
}
|
|
|
|
longestState int
|
|
)
|
|
|
|
const (
|
|
maxProgress = 9
|
|
maxProgressBars = 20
|
|
maxJobProgress = 10
|
|
)
|
|
|
|
type progressUpdater interface {
|
|
update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]struct{}, rollback bool) (bool, error)
|
|
}
|
|
|
|
func init() {
|
|
for state := range numberedStates {
|
|
// for jobs, we use the "complete" state, and so it should be factored
|
|
// in to the computation of the longest state.
|
|
if (!terminalState(state) || state == swarm.TaskStateComplete) && len(state) > longestState {
|
|
longestState = len(state)
|
|
}
|
|
}
|
|
}
|
|
|
|
func terminalState(state swarm.TaskState) bool {
|
|
return numberedStates[state] > numberedStates[swarm.TaskStateRunning]
|
|
}
|
|
|
|
// ServiceProgress outputs progress information for convergence of a service.
|
|
//
|
|
//nolint:gocyclo
|
|
func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID string, progressWriter io.WriteCloser) error {
|
|
defer progressWriter.Close()
|
|
|
|
progressOut := streamformatter.NewJSONProgressOutput(progressWriter, false)
|
|
|
|
sigint := make(chan os.Signal, 1)
|
|
signal.Notify(sigint, os.Interrupt)
|
|
defer signal.Stop(sigint)
|
|
|
|
taskFilter := filters.NewArgs()
|
|
taskFilter.Add("service", serviceID)
|
|
taskFilter.Add("_up-to-date", "true")
|
|
|
|
getUpToDateTasks := func() ([]swarm.Task, error) {
|
|
return apiClient.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
|
|
}
|
|
|
|
var (
|
|
updater progressUpdater
|
|
converged bool
|
|
convergedAt time.Time
|
|
monitor = 5 * time.Second
|
|
rollback bool
|
|
message *progress.Progress
|
|
)
|
|
|
|
for {
|
|
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if service.Spec.UpdateConfig != nil && service.Spec.UpdateConfig.Monitor != 0 {
|
|
monitor = service.Spec.UpdateConfig.Monitor
|
|
}
|
|
|
|
if updater == nil {
|
|
updater, err = initializeUpdater(service, progressOut)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if service.UpdateStatus != nil {
|
|
switch service.UpdateStatus.State {
|
|
case swarm.UpdateStateUpdating:
|
|
rollback = false
|
|
case swarm.UpdateStateCompleted:
|
|
if !converged {
|
|
return nil
|
|
}
|
|
case swarm.UpdateStatePaused:
|
|
return fmt.Errorf("service update paused: %s", service.UpdateStatus.Message)
|
|
case swarm.UpdateStateRollbackStarted:
|
|
if !rollback && service.UpdateStatus.Message != "" {
|
|
progressOut.WriteProgress(progress.Progress{
|
|
ID: "rollback",
|
|
Action: service.UpdateStatus.Message,
|
|
})
|
|
}
|
|
rollback = true
|
|
case swarm.UpdateStateRollbackPaused:
|
|
return fmt.Errorf("service rollback paused: %s", service.UpdateStatus.Message)
|
|
case swarm.UpdateStateRollbackCompleted:
|
|
if !converged {
|
|
message = &progress.Progress{ID: "rollback", Message: service.UpdateStatus.Message}
|
|
}
|
|
rollback = true
|
|
}
|
|
}
|
|
if converged && time.Since(convergedAt) >= monitor {
|
|
progressOut.WriteProgress(progress.Progress{
|
|
ID: "verify",
|
|
Action: fmt.Sprintf("Service %s converged", serviceID),
|
|
})
|
|
if message != nil {
|
|
progressOut.WriteProgress(*message)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
tasks, err := getUpToDateTasks()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
activeNodes, err := getActiveNodes(ctx, apiClient)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
converged, err = updater.update(service, tasks, activeNodes, rollback)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if converged {
|
|
// if the service is a job, there's no need to verify it. jobs are
|
|
// stay done once they're done. skip the verification and just end
|
|
// the progress monitoring.
|
|
//
|
|
// only job services have a non-nil job status, which means we can
|
|
// use the presence of this field to check if the service is a job
|
|
// here.
|
|
if service.JobStatus != nil {
|
|
progress.Message(progressOut, "", "job complete")
|
|
return nil
|
|
}
|
|
|
|
if convergedAt.IsZero() {
|
|
convergedAt = time.Now()
|
|
}
|
|
wait := monitor - time.Since(convergedAt)
|
|
if wait >= 0 {
|
|
progressOut.WriteProgress(progress.Progress{
|
|
// Ideally this would have no ID, but
|
|
// the progress rendering code behaves
|
|
// poorly on an "action" with no ID. It
|
|
// returns the cursor to the beginning
|
|
// of the line, so the first character
|
|
// may be difficult to read. Then the
|
|
// output is overwritten by the shell
|
|
// prompt when the command finishes.
|
|
ID: "verify",
|
|
Action: fmt.Sprintf("Waiting %d seconds to verify that tasks are stable...", wait/time.Second+1),
|
|
})
|
|
}
|
|
} else {
|
|
if !convergedAt.IsZero() {
|
|
progressOut.WriteProgress(progress.Progress{
|
|
ID: "verify",
|
|
Action: "Detected task failure",
|
|
})
|
|
}
|
|
convergedAt = time.Time{}
|
|
}
|
|
|
|
select {
|
|
case <-time.After(200 * time.Millisecond):
|
|
case <-sigint:
|
|
if !converged {
|
|
progress.Message(progressOut, "", "Operation continuing in background.")
|
|
progress.Messagef(progressOut, "", "Use `docker service ps %s` to check progress.", serviceID)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func getActiveNodes(ctx context.Context, apiClient client.APIClient) (map[string]struct{}, error) {
|
|
nodes, err := apiClient.NodeList(ctx, types.NodeListOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
activeNodes := make(map[string]struct{})
|
|
for _, n := range nodes {
|
|
if n.Status.State != swarm.NodeStateDown {
|
|
activeNodes[n.ID] = struct{}{}
|
|
}
|
|
}
|
|
return activeNodes, nil
|
|
}
|
|
|
|
func initializeUpdater(service swarm.Service, progressOut progress.Output) (progressUpdater, error) {
|
|
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
|
|
return &replicatedProgressUpdater{
|
|
progressOut: progressOut,
|
|
}, nil
|
|
}
|
|
if service.Spec.Mode.Global != nil {
|
|
return &globalProgressUpdater{
|
|
progressOut: progressOut,
|
|
}, nil
|
|
}
|
|
if service.Spec.Mode.ReplicatedJob != nil {
|
|
return newReplicatedJobProgressUpdater(service, progressOut), nil
|
|
}
|
|
if service.Spec.Mode.GlobalJob != nil {
|
|
return &globalJobProgressUpdater{
|
|
progressOut: progressOut,
|
|
}, nil
|
|
}
|
|
return nil, errors.New("unrecognized service mode")
|
|
}
|
|
|
|
func writeOverallProgress(progressOut progress.Output, numerator, denominator int, rollback bool) {
|
|
if rollback {
|
|
progressOut.WriteProgress(progress.Progress{
|
|
ID: "overall progress",
|
|
Action: fmt.Sprintf("rolling back update: %d out of %d tasks", numerator, denominator),
|
|
})
|
|
return
|
|
}
|
|
progressOut.WriteProgress(progress.Progress{
|
|
ID: "overall progress",
|
|
Action: fmt.Sprintf("%d out of %d tasks", numerator, denominator),
|
|
})
|
|
}
|
|
|
|
func truncError(errMsg string) string {
|
|
// Remove newlines from the error, which corrupt the output.
|
|
errMsg = strings.ReplaceAll(errMsg, "\n", " ")
|
|
|
|
// Limit the length to 75 characters, so that even on narrow terminals
|
|
// this will not overflow to the next line.
|
|
if len(errMsg) > 75 {
|
|
errMsg = errMsg[:74] + "…"
|
|
}
|
|
return errMsg
|
|
}
|
|
|
|
type replicatedProgressUpdater struct {
|
|
progressOut progress.Output
|
|
|
|
// used for mapping slots to a contiguous space
|
|
// this also causes progress bars to appear in order
|
|
slotMap map[int]int
|
|
|
|
initialized bool
|
|
done bool
|
|
}
|
|
|
|
func (u *replicatedProgressUpdater) update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]struct{}, rollback bool) (bool, error) {
|
|
if service.Spec.Mode.Replicated == nil || service.Spec.Mode.Replicated.Replicas == nil {
|
|
return false, errors.New("no replica count")
|
|
}
|
|
replicas := *service.Spec.Mode.Replicated.Replicas
|
|
|
|
if !u.initialized {
|
|
u.slotMap = make(map[int]int)
|
|
|
|
// Draw progress bars in order
|
|
writeOverallProgress(u.progressOut, 0, int(replicas), rollback)
|
|
|
|
if replicas <= maxProgressBars {
|
|
for i := uint64(1); i <= replicas; i++ {
|
|
progress.Update(u.progressOut, fmt.Sprintf("%d/%d", i, replicas), " ")
|
|
}
|
|
}
|
|
u.initialized = true
|
|
}
|
|
|
|
tasksBySlot := u.tasksBySlot(tasks, activeNodes)
|
|
|
|
// 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 {
|
|
u.done = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
running := uint64(0)
|
|
|
|
for _, task := range tasksBySlot {
|
|
mappedSlot := u.slotMap[task.Slot]
|
|
if mappedSlot == 0 {
|
|
mappedSlot = len(u.slotMap) + 1
|
|
u.slotMap[task.Slot] = mappedSlot
|
|
}
|
|
|
|
if !terminalState(task.DesiredState) && task.Status.State == swarm.TaskStateRunning {
|
|
running++
|
|
}
|
|
|
|
u.writeTaskProgress(task, mappedSlot, replicas)
|
|
}
|
|
|
|
if !u.done {
|
|
writeOverallProgress(u.progressOut, int(running), int(replicas), rollback)
|
|
|
|
if running == replicas {
|
|
u.done = true
|
|
}
|
|
}
|
|
|
|
return running == replicas, nil
|
|
}
|
|
|
|
func (u *replicatedProgressUpdater) tasksBySlot(tasks []swarm.Task, activeNodes map[string]struct{}) map[int]swarm.Task {
|
|
// If there are multiple tasks with the same slot number, favor the one
|
|
// with the *lowest* desired state. This can happen in restart
|
|
// scenarios.
|
|
tasksBySlot := make(map[int]swarm.Task)
|
|
for _, task := range tasks {
|
|
if numberedStates[task.DesiredState] == 0 || numberedStates[task.Status.State] == 0 {
|
|
continue
|
|
}
|
|
if existingTask, ok := tasksBySlot[task.Slot]; ok {
|
|
if numberedStates[existingTask.DesiredState] < numberedStates[task.DesiredState] {
|
|
continue
|
|
}
|
|
// If the desired states match, observed state breaks
|
|
// ties. This can happen with the "start first" service
|
|
// update mode.
|
|
if numberedStates[existingTask.DesiredState] == numberedStates[task.DesiredState] &&
|
|
numberedStates[existingTask.Status.State] <= numberedStates[task.Status.State] {
|
|
continue
|
|
}
|
|
}
|
|
if task.NodeID != "" {
|
|
if _, nodeActive := activeNodes[task.NodeID]; !nodeActive {
|
|
continue
|
|
}
|
|
}
|
|
tasksBySlot[task.Slot] = task
|
|
}
|
|
|
|
return tasksBySlot
|
|
}
|
|
|
|
func (u *replicatedProgressUpdater) writeTaskProgress(task swarm.Task, mappedSlot int, replicas uint64) {
|
|
if u.done || replicas > maxProgressBars || uint64(mappedSlot) > replicas {
|
|
return
|
|
}
|
|
|
|
if task.Status.Err != "" {
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
ID: fmt.Sprintf("%d/%d", mappedSlot, replicas),
|
|
Action: truncError(task.Status.Err),
|
|
})
|
|
return
|
|
}
|
|
|
|
if !terminalState(task.DesiredState) && !terminalState(task.Status.State) {
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
ID: fmt.Sprintf("%d/%d", mappedSlot, replicas),
|
|
Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State),
|
|
Current: numberedStates[task.Status.State],
|
|
Total: maxProgress,
|
|
HideCounts: true,
|
|
})
|
|
}
|
|
}
|
|
|
|
type globalProgressUpdater struct {
|
|
progressOut progress.Output
|
|
|
|
initialized bool
|
|
done bool
|
|
}
|
|
|
|
func (u *globalProgressUpdater) update(_ swarm.Service, tasks []swarm.Task, activeNodes map[string]struct{}, rollback bool) (bool, error) {
|
|
tasksByNode := u.tasksByNode(tasks)
|
|
|
|
// We don't have perfect knowledge of how many nodes meet the
|
|
// constraints for this service. But the orchestrator creates tasks
|
|
// for all eligible nodes at the same time, so we should see all those
|
|
// nodes represented among the up-to-date tasks.
|
|
nodeCount := len(tasksByNode)
|
|
|
|
if !u.initialized {
|
|
if nodeCount == 0 {
|
|
// Two possibilities: either the orchestrator hasn't created
|
|
// the tasks yet, or the service doesn't meet constraints for
|
|
// any node. Either way, we wait.
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
ID: "overall progress",
|
|
Action: "waiting for new tasks",
|
|
})
|
|
return false, nil
|
|
}
|
|
|
|
writeOverallProgress(u.progressOut, 0, nodeCount, rollback)
|
|
u.initialized = true
|
|
}
|
|
|
|
// 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 {
|
|
u.done = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
running := 0
|
|
|
|
for _, task := range tasksByNode {
|
|
if _, nodeActive := activeNodes[task.NodeID]; nodeActive {
|
|
if !terminalState(task.DesiredState) && task.Status.State == swarm.TaskStateRunning {
|
|
running++
|
|
}
|
|
|
|
u.writeTaskProgress(task, nodeCount)
|
|
}
|
|
}
|
|
|
|
if !u.done {
|
|
writeOverallProgress(u.progressOut, running, nodeCount, rollback)
|
|
|
|
if running == nodeCount {
|
|
u.done = true
|
|
}
|
|
}
|
|
|
|
return running == nodeCount, nil
|
|
}
|
|
|
|
func (u *globalProgressUpdater) tasksByNode(tasks []swarm.Task) map[string]swarm.Task {
|
|
// If there are multiple tasks with the same node ID, favor the one
|
|
// with the *lowest* desired state. This can happen in restart
|
|
// scenarios.
|
|
tasksByNode := make(map[string]swarm.Task)
|
|
for _, task := range tasks {
|
|
if numberedStates[task.DesiredState] == 0 || numberedStates[task.Status.State] == 0 {
|
|
continue
|
|
}
|
|
if existingTask, ok := tasksByNode[task.NodeID]; ok {
|
|
if numberedStates[existingTask.DesiredState] < numberedStates[task.DesiredState] {
|
|
continue
|
|
}
|
|
|
|
// If the desired states match, observed state breaks
|
|
// ties. This can happen with the "start first" service
|
|
// update mode.
|
|
if numberedStates[existingTask.DesiredState] == numberedStates[task.DesiredState] &&
|
|
numberedStates[existingTask.Status.State] <= numberedStates[task.Status.State] {
|
|
continue
|
|
}
|
|
}
|
|
tasksByNode[task.NodeID] = task
|
|
}
|
|
|
|
return tasksByNode
|
|
}
|
|
|
|
func (u *globalProgressUpdater) writeTaskProgress(task swarm.Task, nodeCount int) {
|
|
if u.done || nodeCount > maxProgressBars {
|
|
return
|
|
}
|
|
|
|
if task.Status.Err != "" {
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
ID: stringid.TruncateID(task.NodeID),
|
|
Action: truncError(task.Status.Err),
|
|
})
|
|
return
|
|
}
|
|
|
|
if !terminalState(task.DesiredState) && !terminalState(task.Status.State) {
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
ID: stringid.TruncateID(task.NodeID),
|
|
Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State),
|
|
Current: numberedStates[task.Status.State],
|
|
Total: maxProgress,
|
|
HideCounts: true,
|
|
})
|
|
}
|
|
}
|
|
|
|
// replicatedJobProgressUpdater outputs the progress of a replicated job. This
|
|
// progress consists of a few main elements.
|
|
//
|
|
// The first is the progress bar for the job as a whole. This shows the number
|
|
// of completed out of total tasks for the job. Tasks that are currently
|
|
// running are not counted.
|
|
//
|
|
// The second is the status of the "active" tasks for the job. We count a task
|
|
// as "active" if it has any non-terminal state, not just running. This is
|
|
// shown as a fraction of the maximum concurrent tasks that can be running,
|
|
// which is the less of MaxConcurrent or TotalCompletions - completed tasks.
|
|
type replicatedJobProgressUpdater struct {
|
|
progressOut progress.Output
|
|
|
|
// jobIteration is the service's job iteration, used to exclude tasks
|
|
// belonging to earlier iterations.
|
|
jobIteration uint64
|
|
|
|
// concurrent is the value of MaxConcurrent as an int. That is, the maximum
|
|
// number of tasks allowed to be run simultaneously.
|
|
concurrent int
|
|
|
|
// total is the value of TotalCompletions, the number of complete tasks
|
|
// desired.
|
|
total int
|
|
|
|
// initialized is set to true after the first time update is called. the
|
|
// first time update is called, the components of the progress UI are all
|
|
// written out in an initial pass. this ensure that they will subsequently
|
|
// be in order, no matter how they are updated.
|
|
initialized bool
|
|
|
|
// progressDigits is the number digits in total, so that we know how much
|
|
// to pad the job progress field with.
|
|
//
|
|
// when we're writing the number of completed over total tasks, we need to
|
|
// pad the numerator with spaces, so that the bar doesn't jump around.
|
|
// we'll compute that once on init, and then reuse it over and over.
|
|
//
|
|
// we compute this in the least clever way possible: convert to string
|
|
// with strconv.Itoa, then take the len.
|
|
progressDigits int
|
|
|
|
// activeDigits is the same, but for active tasks, and it applies to both
|
|
// the numerator and denominator.
|
|
activeDigits int
|
|
}
|
|
|
|
func newReplicatedJobProgressUpdater(service swarm.Service, progressOut progress.Output) *replicatedJobProgressUpdater {
|
|
u := &replicatedJobProgressUpdater{
|
|
progressOut: progressOut,
|
|
concurrent: int(*service.Spec.Mode.ReplicatedJob.MaxConcurrent),
|
|
total: int(*service.Spec.Mode.ReplicatedJob.TotalCompletions),
|
|
jobIteration: service.JobStatus.JobIteration.Index,
|
|
}
|
|
u.progressDigits = len(strconv.Itoa(u.total))
|
|
u.activeDigits = len(strconv.Itoa(u.concurrent))
|
|
|
|
return u
|
|
}
|
|
|
|
// update writes out the progress of the replicated job.
|
|
func (u *replicatedJobProgressUpdater) update(_ swarm.Service, tasks []swarm.Task, _ map[string]struct{}, _ bool) (bool, error) {
|
|
if !u.initialized {
|
|
u.writeOverallProgress(0, 0)
|
|
|
|
// only write out progress bars if there will be less than the maximum
|
|
if u.total <= maxProgressBars {
|
|
for i := 1; i <= u.total; i++ {
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
ID: fmt.Sprintf("%d/%d", i, u.total),
|
|
Action: " ",
|
|
})
|
|
}
|
|
}
|
|
u.initialized = true
|
|
}
|
|
|
|
// tasksBySlot is a mapping of slot number to the task valid for that slot.
|
|
// it deduplicated tasks occupying the same numerical slot but in different
|
|
// states.
|
|
tasksBySlot := make(map[int]swarm.Task)
|
|
for _, task := range tasks {
|
|
// first, check if the task belongs to this service iteration. skip
|
|
// tasks belonging to other iterations.
|
|
if task.JobIteration == nil || task.JobIteration.Index != u.jobIteration {
|
|
continue
|
|
}
|
|
|
|
// then, if the task is in an unknown state, ignore it.
|
|
if numberedStates[task.DesiredState] == 0 ||
|
|
numberedStates[task.Status.State] == 0 {
|
|
continue
|
|
}
|
|
|
|
// finally, check if the task already exists in the map
|
|
if existing, ok := tasksBySlot[task.Slot]; ok {
|
|
// if so, use the task with the lower actual state
|
|
if numberedStates[existing.Status.State] > numberedStates[task.Status.State] {
|
|
tasksBySlot[task.Slot] = task
|
|
}
|
|
} else {
|
|
// otherwise, just add it to the map.
|
|
tasksBySlot[task.Slot] = task
|
|
}
|
|
}
|
|
|
|
activeTasks := 0
|
|
completeTasks := 0
|
|
|
|
for i := 0; i < len(tasksBySlot); i++ {
|
|
task := tasksBySlot[i]
|
|
u.writeTaskProgress(task)
|
|
|
|
if numberedStates[task.Status.State] < numberedStates[swarm.TaskStateComplete] {
|
|
activeTasks++
|
|
}
|
|
|
|
if task.Status.State == swarm.TaskStateComplete {
|
|
completeTasks++
|
|
}
|
|
}
|
|
|
|
u.writeOverallProgress(activeTasks, completeTasks)
|
|
|
|
return completeTasks == u.total, nil
|
|
}
|
|
|
|
func (u *replicatedJobProgressUpdater) writeOverallProgress(active, completed int) {
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
ID: "job progress",
|
|
Action: fmt.Sprintf(
|
|
// * means "use the next positional arg to compute padding"
|
|
"%*d out of %d complete", u.progressDigits, completed, u.total,
|
|
),
|
|
Current: int64(completed),
|
|
Total: int64(u.total),
|
|
HideCounts: true,
|
|
})
|
|
|
|
// actualDesired is the lesser of MaxConcurrent, or the remaining tasks
|
|
actualDesired := u.total - completed
|
|
if actualDesired > u.concurrent {
|
|
actualDesired = u.concurrent
|
|
}
|
|
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
ID: "active tasks",
|
|
Action: fmt.Sprintf(
|
|
// [n] notation lets us select a specific argument, 1-indexed
|
|
// putting the [1] before the star means "make the string this
|
|
// length". putting the [2] or the [3] means "use this argument
|
|
// here"
|
|
//
|
|
// we pad both the numerator and the denominator because, as the
|
|
// job reaches its conclusion, the number of possible concurrent
|
|
// tasks will go down, as fewer than MaxConcurrent tasks are needed
|
|
// to complete the job.
|
|
"%[1]*[2]d out of %[1]*[3]d tasks", u.activeDigits, active, actualDesired,
|
|
),
|
|
})
|
|
}
|
|
|
|
func (u *replicatedJobProgressUpdater) writeTaskProgress(task swarm.Task) {
|
|
if u.total > maxProgressBars {
|
|
return
|
|
}
|
|
|
|
if task.Status.Err != "" {
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
ID: fmt.Sprintf("%d/%d", task.Slot+1, u.total),
|
|
Action: truncError(task.Status.Err),
|
|
})
|
|
return
|
|
}
|
|
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
ID: fmt.Sprintf("%d/%d", task.Slot+1, u.total),
|
|
Action: fmt.Sprintf("%-*s", longestState, task.Status.State),
|
|
Current: numberedStates[task.Status.State],
|
|
Total: maxJobProgress,
|
|
HideCounts: true,
|
|
})
|
|
}
|
|
|
|
// globalJobProgressUpdater is the progressUpdater for GlobalJob-mode services.
|
|
// Because GlobalJob services are so much simpler than ReplicatedJob services,
|
|
// this updater is in turn simpler as well.
|
|
type globalJobProgressUpdater struct {
|
|
progressOut progress.Output
|
|
|
|
// initialized is used to detect the first pass of update, and to perform
|
|
// first time initialization logic at that time.
|
|
initialized bool
|
|
|
|
// total is the total number of tasks expected for this job
|
|
total int
|
|
|
|
// progressDigits is the number of spaces to pad the numerator of the job
|
|
// progress field
|
|
progressDigits int
|
|
|
|
taskNodes map[string]struct{}
|
|
}
|
|
|
|
func (u *globalJobProgressUpdater) update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]struct{}, _ bool) (bool, error) {
|
|
if !u.initialized {
|
|
// if there are not yet tasks, then return early.
|
|
if len(tasks) == 0 && len(activeNodes) != 0 {
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
ID: "job progress",
|
|
Action: "waiting for tasks",
|
|
})
|
|
return false, nil
|
|
}
|
|
|
|
// when a global job starts, all of its tasks are created at once, so
|
|
// we can use len(tasks) to know how many we're expecting.
|
|
u.taskNodes = map[string]struct{}{}
|
|
|
|
for _, task := range tasks {
|
|
// skip any tasks not belonging to this job iteration.
|
|
if task.JobIteration == nil || task.JobIteration.Index != service.JobStatus.JobIteration.Index {
|
|
continue
|
|
}
|
|
|
|
// collect the list of all node IDs for this service.
|
|
//
|
|
// basically, global jobs will execute on any new nodes that join
|
|
// the cluster in the future. to avoid making things complicated,
|
|
// we will only check the progress of the initial set of nodes. if
|
|
// any new nodes come online during the operation, we will ignore
|
|
// them.
|
|
u.taskNodes[task.NodeID] = struct{}{}
|
|
}
|
|
|
|
u.total = len(u.taskNodes)
|
|
u.progressDigits = len(strconv.Itoa(u.total))
|
|
|
|
u.writeOverallProgress(0)
|
|
u.initialized = true
|
|
}
|
|
|
|
// tasksByNodeID maps a NodeID to the latest task for that Node ID. this
|
|
// lets us pick only the latest task for any given node.
|
|
tasksByNodeID := map[string]swarm.Task{}
|
|
|
|
for _, task := range tasks {
|
|
// skip any tasks not belonging to this job iteration
|
|
if task.JobIteration == nil || task.JobIteration.Index != service.JobStatus.JobIteration.Index {
|
|
continue
|
|
}
|
|
|
|
// if the task is not on one of the initial set of nodes, ignore it.
|
|
if _, ok := u.taskNodes[task.NodeID]; !ok {
|
|
continue
|
|
}
|
|
|
|
// if there is already a task recorded for this node, choose the one
|
|
// with the lower state
|
|
if oldtask, ok := tasksByNodeID[task.NodeID]; ok {
|
|
if numberedStates[oldtask.Status.State] > numberedStates[task.Status.State] {
|
|
tasksByNodeID[task.NodeID] = task
|
|
}
|
|
} else {
|
|
tasksByNodeID[task.NodeID] = task
|
|
}
|
|
}
|
|
|
|
complete := 0
|
|
for _, task := range tasksByNodeID {
|
|
u.writeTaskProgress(task)
|
|
if task.Status.State == swarm.TaskStateComplete {
|
|
complete++
|
|
}
|
|
}
|
|
|
|
u.writeOverallProgress(complete)
|
|
return complete == u.total, nil
|
|
}
|
|
|
|
func (u *globalJobProgressUpdater) writeTaskProgress(task swarm.Task) {
|
|
if u.total > maxProgressBars {
|
|
return
|
|
}
|
|
|
|
if task.Status.Err != "" {
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
ID: task.NodeID,
|
|
Action: truncError(task.Status.Err),
|
|
})
|
|
return
|
|
}
|
|
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
ID: task.NodeID,
|
|
Action: fmt.Sprintf("%-*s", longestState, task.Status.State),
|
|
Current: numberedStates[task.Status.State],
|
|
Total: maxJobProgress,
|
|
HideCounts: true,
|
|
})
|
|
}
|
|
|
|
func (u *globalJobProgressUpdater) writeOverallProgress(complete int) {
|
|
// all tasks for a global job are active at once, so we only write out the
|
|
// total progress.
|
|
u.progressOut.WriteProgress(progress.Progress{
|
|
// see (*replicatedJobProgressUpdater).writeOverallProgress for an
|
|
// explanation of the advanced fmt use in this function.
|
|
ID: "job progress",
|
|
Action: fmt.Sprintf(
|
|
"%*d out of %d complete", u.progressDigits, complete, u.total,
|
|
),
|
|
Current: int64(complete),
|
|
Total: int64(u.total),
|
|
HideCounts: true,
|
|
})
|
|
}
|