diff --git a/internal/manager/api_impl/interfaces.go b/internal/manager/api_impl/interfaces.go index d2de0437..8c67ba5f 100644 --- a/internal/manager/api_impl/interfaces.go +++ b/internal/manager/api_impl/interfaces.go @@ -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. diff --git a/internal/manager/api_impl/jobs.go b/internal/manager/api_impl/jobs.go index 45da9242..71eed734 100644 --- a/internal/manager/api_impl/jobs.go +++ b/internal/manager/api_impl/jobs.go @@ -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, + } +} diff --git a/internal/manager/api_impl/jobs_query.go b/internal/manager/api_impl/jobs_query.go index c1cc368c..4751b4ae 100644 --- a/internal/manager/api_impl/jobs_query.go +++ b/internal/manager/api_impl/jobs_query.go @@ -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) } diff --git a/internal/manager/api_impl/jobs_query_test.go b/internal/manager/api_impl/jobs_query_test.go index ee0b5946..1258d95e 100644 --- a/internal/manager/api_impl/jobs_query_test.go +++ b/internal/manager/api_impl/jobs_query_test.go @@ -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) diff --git a/internal/manager/api_impl/mocks/api_impl_mock.gen.go b/internal/manager/api_impl/mocks/api_impl_mock.gen.go index 91ce3064..247f3ec7 100644 --- a/internal/manager/api_impl/mocks/api_impl_mock.gen.go +++ b/internal/manager/api_impl/mocks/api_impl_mock.gen.go @@ -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() diff --git a/internal/manager/persistence/jobs.go b/internal/manager/persistence/jobs.go index 29267f77..c2f2506b 100644 --- a/internal/manager/persistence/jobs.go +++ b/internal/manager/persistence/jobs.go @@ -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 +} diff --git a/internal/manager/persistence/jobs_test.go b/internal/manager/persistence/jobs_test.go index 920de094..0666d37e 100644 --- a/internal/manager/persistence/jobs_test.go +++ b/internal/manager/persistence/jobs_test.go @@ -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", diff --git a/web/app/src/components/jobs/TaskDetails.vue b/web/app/src/components/jobs/TaskDetails.vue index 8b66ec88..d57665e0 100644 --- a/web/app/src/components/jobs/TaskDetails.vue +++ b/web/app/src/components/jobs/TaskDetails.vue @@ -29,6 +29,13 @@
Last Touched by Worker
{{ datetime.relativeTime(taskData.last_touched) }}
+ +

Commands