Show task failure list in the web frontend

Show the task failure list in the web frontend's `TaskDetails` component.
This commit is contained in:
Sybren A. Stüvel 2022-06-17 11:34:07 +02:00
parent 7f14dac62f
commit 81f81d0e0a
8 changed files with 117 additions and 13 deletions

@ -31,6 +31,7 @@ type PersistenceService interface {
FetchJob(ctx context.Context, jobID string) (*persistence.Job, error)
// FetchTask fetches the given task and the accompanying job.
FetchTask(ctx context.Context, taskID string) (*persistence.Task, error)
FetchTaskFailureList(context.Context, *persistence.Task) ([]*persistence.Worker, error)
SaveTask(ctx context.Context, task *persistence.Task) error
SaveTaskActivity(ctx context.Context, t *persistence.Task) error
// TaskTouchedByWorker marks the task as 'touched' by a worker. This is used for timeout detection.

@ -275,20 +275,13 @@ func taskDBtoAPI(dbTask *persistence.Task) api.Task {
Status: dbTask.Status,
Activity: dbTask.Activity,
Commands: make([]api.Command, len(dbTask.Commands)),
Worker: workerToTaskWorker(dbTask.Worker),
}
if dbTask.Job != nil {
apiTask.JobId = dbTask.Job.UUID
}
if dbTask.Worker != nil {
apiTask.Worker = &api.TaskWorker{
Id: dbTask.Worker.UUID,
Name: dbTask.Worker.Name,
Address: dbTask.Worker.Address,
}
}
if !dbTask.LastTouchedAt.IsZero() {
apiTask.LastTouched = &dbTask.LastTouchedAt
}
@ -306,3 +299,15 @@ func commandDBtoAPI(dbCommand persistence.Command) api.Command {
Parameters: dbCommand.Parameters,
}
}
// workerToTaskWorker is nil-safe.
func workerToTaskWorker(worker *persistence.Worker) *api.TaskWorker {
if worker == nil {
return nil
}
return &api.TaskWorker{
Id: worker.UUID,
Name: worker.Name,
Address: worker.Address,
}
}

@ -2,6 +2,7 @@
package api_impl
import (
"errors"
"fmt"
"net/http"
@ -99,8 +100,9 @@ func (f *Flamenco) FetchTask(e echo.Context, taskID string) error {
return sendAPIError(e, http.StatusBadRequest, "job ID not valid")
}
// Fetch & convert the task.
task, err := f.persist.FetchTask(ctx, taskID)
if err == persistence.ErrTaskNotFound {
if errors.Is(err, persistence.ErrTaskNotFound) {
logger.Debug().Msg("non-existent task requested")
return sendAPIError(e, http.StatusNotFound, "no such task")
}
@ -108,8 +110,20 @@ func (f *Flamenco) FetchTask(e echo.Context, taskID string) error {
logger.Warn().Err(err).Msg("error fetching task")
return sendAPIError(e, http.StatusInternalServerError, "error fetching task")
}
apiTask := taskDBtoAPI(task)
// Fetch & convert the failure list.
failedWorkers, err := f.persist.FetchTaskFailureList(ctx, task)
if err != nil {
logger.Warn().Err(err).Msg("error fetching task failure list")
return sendAPIError(e, http.StatusInternalServerError, "error fetching task failure list")
}
failedTaskWorkers := make([]api.TaskWorker, len(failedWorkers))
for idx, worker := range failedWorkers {
failedTaskWorkers[idx] = *workerToTaskWorker(worker)
}
apiTask.FailedByWorkers = &failedTaskWorkers
return e.JSON(http.StatusOK, apiTask)
}

@ -22,6 +22,8 @@ func TestFetchTask(t *testing.T) {
workerUUID := "b5725bb3-d540-4070-a2b6-7b4b26925f94"
jobUUID := "8b179118-0189-478a-b463-73798409898c"
taskWorker := persistence.Worker{UUID: workerUUID, Name: "Radnik", Address: "Slapić"}
dbTask := persistence.Task{
Model: persistence.Model{
ID: 327,
@ -36,7 +38,7 @@ func TestFetchTask(t *testing.T) {
Priority: 47,
Status: api.TaskStatusQueued,
WorkerID: new(uint),
Worker: &persistence.Worker{UUID: workerUUID, Name: "Radnik", Address: "Slapić"},
Worker: &taskWorker,
Dependencies: []*persistence.Task{},
Activity: "used in unit test",
@ -68,11 +70,18 @@ func TestFetchTask(t *testing.T) {
"src": "/render/_flamenco/tests/renders/2022-04-29 Weekly/2022-04-29_140531__intermediate-2022-04-29_140531",
}},
},
FailedByWorkers: ptr([]api.TaskWorker{
{Id: workerUUID, Name: "Radnik", Address: "Slapić"},
}),
}
mf.persistence.EXPECT().FetchTask(gomock.Any(), taskUUID).Return(&dbTask, nil)
echoCtx := mf.prepareMockedRequest(nil)
ctx := echoCtx.Request().Context()
mf.persistence.EXPECT().FetchTask(ctx, taskUUID).Return(&dbTask, nil)
mf.persistence.EXPECT().FetchTaskFailureList(ctx, &dbTask).
Return([]*persistence.Worker{&taskWorker}, nil)
err := mf.flamenco.FetchTask(echoCtx, taskUUID)
assert.NoError(t, err)

@ -127,6 +127,21 @@ func (mr *MockPersistenceServiceMockRecorder) FetchTask(arg0, arg1 interface{})
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchTask", reflect.TypeOf((*MockPersistenceService)(nil).FetchTask), arg0, arg1)
}
// FetchTaskFailureList mocks base method.
func (m *MockPersistenceService) FetchTaskFailureList(arg0 context.Context, arg1 *persistence.Task) ([]*persistence.Worker, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchTaskFailureList", arg0, arg1)
ret0, _ := ret[0].([]*persistence.Worker)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FetchTaskFailureList indicates an expected call of FetchTaskFailureList.
func (mr *MockPersistenceServiceMockRecorder) FetchTaskFailureList(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchTaskFailureList", reflect.TypeOf((*MockPersistenceService)(nil).FetchTaskFailureList), arg0, arg1)
}
// FetchWorker mocks base method.
func (m *MockPersistenceService) FetchWorker(arg0 context.Context, arg1 string) (*persistence.Worker, error) {
m.ctrl.T.Helper()

@ -482,3 +482,15 @@ func (db *DB) ClearFailureListOfJob(ctx context.Context, j *Job) error {
Delete(&TaskFailure{})
return tx.Error
}
func (db *DB) FetchTaskFailureList(ctx context.Context, t *Task) ([]*Worker, error) {
var workers []*Worker
tx := db.gormDB.WithContext(ctx).
Model(&Worker{}).
Joins("inner join task_failures TF on TF.worker_id = workers.id").
Where("TF.task_id = ?", t.ID).
Scan(&workers)
return workers, tx.Error
}

@ -379,6 +379,47 @@ func TestClearFailureListOfJob(t *testing.T) {
}
}
func TestFetchTaskFailureList(t *testing.T) {
ctx, close, db, _, authoredJob1 := jobTasksTestFixtures(t)
defer close()
// Test with non-existing task.
fakeTask := Task{Model: Model{ID: 327}}
failures, err := db.FetchTaskFailureList(ctx, &fakeTask)
assert.NoError(t, err)
assert.Empty(t, failures)
task1_1, _ := db.FetchTask(ctx, authoredJob1.Tasks[1].UUID)
task1_2, _ := db.FetchTask(ctx, authoredJob1.Tasks[2].UUID)
// Test without failures.
failures, err = db.FetchTaskFailureList(ctx, task1_1)
assert.NoError(t, err)
assert.Empty(t, failures)
worker1 := createWorker(ctx, t, db)
worker2 := createWorkerFrom(ctx, t, db, *worker1)
// Store some failures for different tasks and jobs
_, _ = db.AddWorkerToTaskFailedList(ctx, task1_1, worker1)
_, _ = db.AddWorkerToTaskFailedList(ctx, task1_1, worker2)
_, _ = db.AddWorkerToTaskFailedList(ctx, task1_2, worker1)
// Fetch one task's failure list.
failures, err = db.FetchTaskFailureList(ctx, task1_1)
assert.NoError(t, err)
if assert.Len(t, failures, 2) {
assert.Equal(t, worker1.UUID, failures[0].UUID)
assert.Equal(t, worker1.Name, failures[0].Name)
assert.Equal(t, worker1.Address, failures[0].Address)
assert.Equal(t, worker2.UUID, failures[1].UUID)
assert.Equal(t, worker2.Name, failures[1].Name)
assert.Equal(t, worker2.Address, failures[1].Address)
}
}
func createTestAuthoredJobWithTasks() job_compilers.AuthoredJob {
task1 := job_compilers.AuthoredTask{
Name: "render-1-3",

@ -29,6 +29,13 @@
<dt class="field-last-touched">Last Touched by Worker</dt>
<dd>{{ datetime.relativeTime(taskData.last_touched) }}</dd>
<template v-if="taskData.failed_by_workers.length > 0">
<dt class="field-failed-by-workers">Failed by Workers</dt>
<dd v-for="worker in taskData.failed_by_workers">
<router-link :to="{ name: 'workers', params: { workerID: worker.id } }">{{ worker.name }} ({{ worker.address }})</router-link>
</dd>
</template>
</dl>
<h3 class="sub-title">Commands</h3>