diff --git a/app/lib/components/status_table.dart b/app/lib/components/status_table.dart
index 06cfdd46d4..022bc47c0a 100644
--- a/app/lib/components/status_table.dart
+++ b/app/lib/components/status_table.dart
@@ -67,6 +67,7 @@ import 'package:http/http.dart' as http;
+ {{getAttempts(status.checklist.checklist.commit.sha, metaTask.name)}}
|
@@ -141,6 +142,8 @@ class StatusTable implements OnInit {
String cssClass;
if (taskStatus == 'Succeeded' && attempts > 1) {
cssClass = 'task-succeeded-but-flaky';
+ } else if (taskStatus == 'New' && attempts > 1) {
+ cssClass = 'task-underperformed';
} else {
cssClass = statusMap[taskStatus] ?? 'task-unknown';
}
@@ -166,6 +169,17 @@ class StatusTable implements OnInit {
return taskStatusToCssStyle(taskEntity.task.status, taskEntity.task.attempts);
}
+ String getAttempts(String sha, String taskName) {
+ TaskEntity taskEntity = _findTask(sha, taskName);
+
+ if (taskEntity == null)
+ return '';
+
+ int attempts = taskEntity.task.attempts;
+ bool succeededImmediately = attempts == 1 && taskEntity.task.status == 'Succeeded';
+ return attempts == 0 || succeededImmediately ? '' : '${attempts}';
+ }
+
List getAgentStyle(AgentStatus status) {
return [
'agent-chip',
diff --git a/app/web/buildStyles.css b/app/web/buildStyles.css
index 5dfda47353..855c411d4f 100644
--- a/app/web/buildStyles.css
+++ b/app/web/buildStyles.css
@@ -284,14 +284,16 @@ td.stats-value {
border-radius: 10px;
height: 20px;
width: 20px;
+ text-align: center;
+ color: white;
}
.task-new {
background-color: #CCCCFF;
}
.task-in-progress {
- background: linear-gradient(0deg, blue 0%, blue 40%, white 41%, white 59%, blue 60%, blue 100%);;
- animation: inProgressAnimation 2s infinite linear;
+ background-color: blue;
+ animation: inProgressAnimation 3s infinite linear;
cursor: pointer;
}
.task-succeeded {
@@ -323,7 +325,7 @@ td.stats-value {
transform: rotateZ(0deg);
}
100% {
- transform: rotateZ(180deg);
+ transform: rotateZ(360deg);
}
}
diff --git a/commands/get_status.go b/commands/get_status.go
index 4d53355ea2..ad34b4c8d6 100644
--- a/commands/get_status.go
+++ b/commands/get_status.go
@@ -25,7 +25,9 @@ type BuildStatus struct {
// GetStatus returns current build status.
func GetStatus(c *db.Cocoon, inputJSON []byte) (interface{}, error) {
var err error
- checklists, err := c.QueryLatestChecklists()
+
+ const maxStatusesToReturn = 50
+ checklists, err := c.QueryLatestChecklists(maxStatusesToReturn)
if err != nil {
return nil, err
diff --git a/commands/reserve_task.go b/commands/reserve_task.go
index e34121eb7f..15b0420639 100644
--- a/commands/reserve_task.go
+++ b/commands/reserve_task.go
@@ -8,6 +8,7 @@ import (
"cocoon/db"
"encoding/json"
"fmt"
+ "sort"
"golang.org/x/net/context"
@@ -86,7 +87,8 @@ func ReserveTask(cocoon *db.Cocoon, inputJSON []byte) (interface{}, error) {
}
func findNextTaskToRun(cocoon *db.Cocoon, agent *db.Agent) (*db.TaskEntity, *db.ChecklistEntity, error) {
- checklists, err := cocoon.QueryLatestChecklists()
+ const maxChecklistsToScan = 20
+ checklists, err := cocoon.QueryLatestChecklists(maxChecklistsToScan)
if err != nil {
return nil, nil, err
@@ -110,7 +112,7 @@ func findNextTaskToRun(cocoon *db.Cocoon, agent *db.Agent) (*db.TaskEntity, *db.
// these tasks for agents.
continue
}
- for _, taskEntity := range stage.Tasks {
+ for _, taskEntity := range sortByAttemptCount(stage.Tasks) {
task := taskEntity.Task
if len(task.RequiredCapabilities) == 0 {
@@ -175,3 +177,19 @@ func allPrimaryStagesSuccessful(stages []*db.Stage) bool {
}
return db.Every(len(stages), func(i int) interface{} { return stages[i] }, isSuccessfulPrimaryOrAnySecondary)
}
+
+// Run tasks with fewest prior attempts first.
+func sortByAttemptCount(tasks []*db.TaskEntity) []*db.TaskEntity {
+ sorted := make([]*db.TaskEntity, len(tasks))
+ copy(sorted, tasks)
+ sort.Sort(byAttemptCount(sorted))
+ return sorted
+}
+
+type byAttemptCount []*db.TaskEntity
+
+func (tasks byAttemptCount) Len() int { return len(tasks) }
+func (tasks byAttemptCount) Swap(i, j int) { tasks[i], tasks[j] = tasks[j], tasks[i] }
+func (tasks byAttemptCount) Less(i, j int) bool {
+ return tasks[i].Task.Attempts < tasks[j].Task.Attempts
+}
diff --git a/db/db.go b/db/db.go
index b69cec726b..61f981fd00 100644
--- a/db/db.go
+++ b/db/db.go
@@ -87,8 +87,8 @@ func (c *Cocoon) GetChecklist(key *datastore.Key) (*ChecklistEntity, error) {
// QueryLatestChecklists queries the datastore for the latest checklists sorted
// by CreateTimestamp in descending order. Returns up to 20 entities.
-func (c *Cocoon) QueryLatestChecklists() ([]*ChecklistEntity, error) {
- query := datastore.NewQuery("Checklist").Order("-CreateTimestamp").Limit(20)
+func (c *Cocoon) QueryLatestChecklists(limit int) ([]*ChecklistEntity, error) {
+ query := datastore.NewQuery("Checklist").Order("-CreateTimestamp").Limit(limit)
var buffer []*ChecklistEntity
for iter := query.Run(c.Ctx); ; {
var checklist Checklist
@@ -193,7 +193,8 @@ func (c *Cocoon) QueryAllPendingTasks() ([]*FullTask, error) {
//
// See also IsFinal.
func (c *Cocoon) QueryPendingTasks(taskName string) ([]*FullTask, error) {
- checklists, err := c.QueryLatestChecklists()
+ const maxChecklistsToScan = 20
+ checklists, err := c.QueryLatestChecklists(maxChecklistsToScan)
if err != nil {
return nil, err