| OLD | NEW |
| 1 // Copyright 2015 The LUCI Authors. All rights reserved. | 1 // Copyright 2015 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 | 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. | 3 // that can be found in the LICENSE file. |
| 4 | 4 |
| 5 package ui | 5 package ui |
| 6 | 6 |
| 7 import ( | 7 import ( |
| 8 "bytes" | 8 "bytes" |
| 9 "fmt" | 9 "fmt" |
| 10 "sort" | 10 "sort" |
| 11 "strings" | 11 "strings" |
| 12 "time" | 12 "time" |
| 13 | 13 |
| 14 "github.com/dustin/go-humanize" | 14 "github.com/dustin/go-humanize" |
| 15 "github.com/golang/protobuf/proto" | 15 "github.com/golang/protobuf/proto" |
| 16 "golang.org/x/net/context" | 16 "golang.org/x/net/context" |
| 17 | 17 |
| 18 "github.com/luci/luci-go/common/clock" | 18 "github.com/luci/luci-go/common/clock" |
| 19 "github.com/luci/luci-go/common/logging" | 19 "github.com/luci/luci-go/common/logging" |
| 20 | 20 |
| 21 "github.com/luci/luci-go/common/data/sortby" | 21 "github.com/luci/luci-go/common/data/sortby" |
| 22 "github.com/luci/luci-go/scheduler/appengine/catalog" | 22 "github.com/luci/luci-go/scheduler/appengine/catalog" |
| 23 "github.com/luci/luci-go/scheduler/appengine/engine" | 23 "github.com/luci/luci-go/scheduler/appengine/engine" |
| 24 "github.com/luci/luci-go/scheduler/appengine/messages" | 24 "github.com/luci/luci-go/scheduler/appengine/messages" |
| 25 "github.com/luci/luci-go/scheduler/appengine/presentation" |
| 25 "github.com/luci/luci-go/scheduler/appengine/schedule" | 26 "github.com/luci/luci-go/scheduler/appengine/schedule" |
| 26 "github.com/luci/luci-go/scheduler/appengine/task" | 27 "github.com/luci/luci-go/scheduler/appengine/task" |
| 27 ) | 28 ) |
| 28 | 29 |
| 29 // schedulerJob is UI representation of engine.Job entity. | 30 // schedulerJob is UI representation of engine.Job entity. |
| 30 type schedulerJob struct { | 31 type schedulerJob struct { |
| 31 ProjectID string | 32 ProjectID string |
| 32 JobID string | 33 JobID string |
| 33 Schedule string | 34 Schedule string |
| 34 Definition string | 35 Definition string |
| 35 Revision string | 36 Revision string |
| 36 RevisionURL string | 37 RevisionURL string |
| 37 State string | 38 State string |
| 38 Overruns int | 39 Overruns int |
| 39 NextRun string | 40 NextRun string |
| 40 Paused bool | 41 Paused bool |
| 41 LabelClass string | 42 LabelClass string |
| 42 JobFlavorIcon string | 43 JobFlavorIcon string |
| 43 JobFlavorTitle string | 44 JobFlavorTitle string |
| 44 | 45 |
| 45 sortGroup string // used only for sorting, doesn't show up in UI | 46 sortGroup string // used only for sorting, doesn't show up in UI |
| 46 now time.Time // as passed to makeJob | 47 now time.Time // as passed to makeJob |
| 47 traits task.Traits // as extracted from corresponding task.Manager | 48 traits task.Traits // as extracted from corresponding task.Manager |
| 48 } | 49 } |
| 49 | 50 |
| 50 var stateToLabelClass = map[engine.StateKind]string{ | 51 var stateToLabelClass = map[presentation.PublicStateKind]string{ |
| 51 » engine.JobStateDisabled: "label-default", | 52 » presentation.PublicStateRetrying: "label-danger", |
| 52 » engine.JobStateScheduled: "label-primary", | 53 » presentation.PublicStatePaused: "label-default", |
| 53 » engine.JobStateSuspended: "label-default", | 54 » presentation.PublicStateScheduled: "label-primary", |
| 54 » engine.JobStateRunning: "label-info", | 55 » presentation.PublicStateSuspended: "label-default", |
| 55 » engine.JobStateOverrun: "label-warning", | 56 » presentation.PublicStateRunning: "label-info", |
| 57 » presentation.PublicStateOverrun: "label-warning", |
| 58 » presentation.PublicStateWaiting: "label-warning", |
| 59 » presentation.PublicStateStarting: "label-default", |
| 56 } | 60 } |
| 57 | 61 |
| 58 var flavorToIconClass = []string{ | 62 var flavorToIconClass = []string{ |
| 59 catalog.JobFlavorPeriodic: "glyphicon-time", | 63 catalog.JobFlavorPeriodic: "glyphicon-time", |
| 60 catalog.JobFlavorTriggered: "glyphicon-flash", | 64 catalog.JobFlavorTriggered: "glyphicon-flash", |
| 61 catalog.JobFlavorTrigger: "glyphicon-bell", | 65 catalog.JobFlavorTrigger: "glyphicon-bell", |
| 62 } | 66 } |
| 63 | 67 |
| 64 var flavorToTitle = []string{ | 68 var flavorToTitle = []string{ |
| 65 catalog.JobFlavorPeriodic: "Periodic job", | 69 catalog.JobFlavorPeriodic: "Periodic job", |
| 66 catalog.JobFlavorTriggered: "Triggered job", | 70 catalog.JobFlavorTriggered: "Triggered job", |
| 67 catalog.JobFlavorTrigger: "Triggering job", | 71 catalog.JobFlavorTrigger: "Triggering job", |
| 68 } | 72 } |
| 69 | 73 |
| 70 // makeJob builds UI presentation for engine.Job. | 74 // makeJob builds UI presentation for engine.Job. |
| 71 func makeJob(c context.Context, j *engine.Job) *schedulerJob { | 75 func makeJob(c context.Context, j *engine.Job) *schedulerJob { |
| 72 » // Grab the task.Manager that's responsible for handling this job and | 76 » traits, err := presentation.GetJobTraits(c, config(c).Catalog, j) |
| 73 » // query if for the list of traits applying to this job. | |
| 74 » var manager task.Manager | |
| 75 » cat := config(c).Catalog | |
| 76 » taskDef, err := cat.UnmarshalTask(j.Task) | |
| 77 if err != nil { | 77 if err != nil { |
| 78 » » logging.WithError(err).Warningf(c, "Failed to unmarshal task pro
to for %s", j.JobID) | 78 » » logging.WithError(err).Warningf(c, "Failed to get task traits fo
r %s", j.JobID) |
| 79 » } else { | |
| 80 » » manager = cat.GetTaskManager(taskDef) | |
| 81 » } | |
| 82 » traits := task.Traits{} | |
| 83 » if manager != nil { | |
| 84 » » traits = manager.Traits() | |
| 85 } | 79 } |
| 86 | 80 |
| 87 now := clock.Now(c).UTC() | 81 now := clock.Now(c).UTC() |
| 88 nextRun := "" | 82 nextRun := "" |
| 89 switch ts := j.State.TickTime; { | 83 switch ts := j.State.TickTime; { |
| 90 case ts == schedule.DistantFuture: | 84 case ts == schedule.DistantFuture: |
| 91 nextRun = "-" | 85 nextRun = "-" |
| 92 case !ts.IsZero(): | 86 case !ts.IsZero(): |
| 93 nextRun = humanize.RelTime(ts, now, "ago", "from now") | 87 nextRun = humanize.RelTime(ts, now, "ago", "from now") |
| 94 default: | 88 default: |
| 95 nextRun = "not scheduled yet" | 89 nextRun = "not scheduled yet" |
| 96 } | 90 } |
| 97 | 91 |
| 98 // Internal state names aren't very user friendly. Introduce some aliase
s. | 92 // Internal state names aren't very user friendly. Introduce some aliase
s. |
| 99 » state := "" | 93 » state := presentation.GetPublicStateKind(j, traits) |
| 100 » labelClass := "" | 94 » labelClass := stateToLabelClass[state] |
| 101 » switch { | 95 » if j.State.State == engine.JobStateSlowQueue { |
| 102 » case j.State.IsRetrying(): | |
| 103 » » // Retries happen when invocation fails to launch (move from "ST
ARTING" to | |
| 104 » » // "RUNNING" state). Such invocation is retried (as new invocati
on). When | |
| 105 » » // a retry is enqueued, we display the job state as "RETRYING" (
even though | |
| 106 » » // technically it is still "QUEUED"). | |
| 107 » » state = "RETRYING" | |
| 108 » » labelClass = "label-danger" | |
| 109 » case !traits.Multistage && j.State.InvocationID != 0: | |
| 110 » » // The job has an active invocation and the engine has called La
unchTask for | |
| 111 » » // it already. Jobs with Multistage == false trait do all their
work in | |
| 112 » » // LaunchTask, so we display them as "RUNNING" (instead of "STAR
TING"). | |
| 113 » » state = "RUNNING" | |
| 114 » » labelClass = "label-info" | |
| 115 » case j.State.State == engine.JobStateQueued: | |
| 116 » » // An invocation has been added to the task queue, and the engin
e hasn't | |
| 117 » » // attempted to launch it yet. | |
| 118 » » state = "STARTING" | |
| 119 » » labelClass = "label-default" | |
| 120 » case j.State.State == engine.JobStateSlowQueue: | |
| 121 // Job invocation is still in the task queue, but new invocation
should be | 96 // Job invocation is still in the task queue, but new invocation
should be |
| 122 // starting now (so the queue is lagging for some reason). | 97 // starting now (so the queue is lagging for some reason). |
| 123 state = "STARTING" | |
| 124 labelClass = "label-warning" | 98 labelClass = "label-warning" |
| 125 case j.Paused && j.State.State == engine.JobStateSuspended: | |
| 126 // Paused jobs don't have a schedule, so they are always in "SUS
PENDED" | |
| 127 // state. Make it clearer that they are just paused. This applie
s to both | |
| 128 // triggered and periodic jobs. | |
| 129 state = "PAUSED" | |
| 130 labelClass = "label-default" | |
| 131 case j.State.State == engine.JobStateSuspended && j.Flavor == catalog.Jo
bFlavorTriggered: | |
| 132 // Triggered jobs don't run on a schedule. They are in "SUSPENDE
D" state | |
| 133 // between triggering events, rename it to "WAITING" for clarity
. | |
| 134 state = "WAITING" | |
| 135 labelClass = "label-default" | |
| 136 default: | |
| 137 state = string(j.State.State) | |
| 138 labelClass = stateToLabelClass[j.State.State] | |
| 139 } | 99 } |
| 140 | |
| 141 // Put triggers after regular jobs. | 100 // Put triggers after regular jobs. |
| 142 sortGroup := "A" | 101 sortGroup := "A" |
| 143 if j.Flavor == catalog.JobFlavorTrigger { | 102 if j.Flavor == catalog.JobFlavorTrigger { |
| 144 sortGroup = "B" | 103 sortGroup = "B" |
| 145 } | 104 } |
| 146 | 105 |
| 147 // JobID has form <project>/<id>. Split it into components. | 106 // JobID has form <project>/<id>. Split it into components. |
| 148 chunks := strings.Split(j.JobID, "/") | 107 chunks := strings.Split(j.JobID, "/") |
| 149 | 108 |
| 150 return &schedulerJob{ | 109 return &schedulerJob{ |
| 151 ProjectID: chunks[0], | 110 ProjectID: chunks[0], |
| 152 JobID: chunks[1], | 111 JobID: chunks[1], |
| 153 Schedule: j.Schedule, | 112 Schedule: j.Schedule, |
| 154 Definition: taskToText(j.Task), | 113 Definition: taskToText(j.Task), |
| 155 Revision: j.Revision, | 114 Revision: j.Revision, |
| 156 RevisionURL: j.RevisionURL, | 115 RevisionURL: j.RevisionURL, |
| 157 » » State: state, | 116 » » State: string(state), |
| 158 Overruns: j.State.Overruns, | 117 Overruns: j.State.Overruns, |
| 159 NextRun: nextRun, | 118 NextRun: nextRun, |
| 160 Paused: j.Paused, | 119 Paused: j.Paused, |
| 161 LabelClass: labelClass, | 120 LabelClass: labelClass, |
| 162 JobFlavorIcon: flavorToIconClass[j.Flavor], | 121 JobFlavorIcon: flavorToIconClass[j.Flavor], |
| 163 JobFlavorTitle: flavorToTitle[j.Flavor], | 122 JobFlavorTitle: flavorToTitle[j.Flavor], |
| 164 | 123 |
| 165 sortGroup: sortGroup, | 124 sortGroup: sortGroup, |
| 166 now: now, | 125 now: now, |
| 167 traits: traits, | 126 traits: traits, |
| (...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 282 TriggeredBy: triggeredBy, | 241 TriggeredBy: triggeredBy, |
| 283 Started: humanize.RelTime(i.Started, j.now, "ago", "from now
"), | 242 Started: humanize.RelTime(i.Started, j.now, "ago", "from now
"), |
| 284 Duration: duration, | 243 Duration: duration, |
| 285 Status: string(status), | 244 Status: string(status), |
| 286 DebugLog: i.DebugLog, | 245 DebugLog: i.DebugLog, |
| 287 RowClass: statusToRowClass[status], | 246 RowClass: statusToRowClass[status], |
| 288 LabelClass: statusToLabelClass[status], | 247 LabelClass: statusToLabelClass[status], |
| 289 ViewURL: i.ViewURL, | 248 ViewURL: i.ViewURL, |
| 290 } | 249 } |
| 291 } | 250 } |
| OLD | NEW |