Transition from ex-GORM structs to sqlc structs (5/5)

Replace old used-to-be-GORM datastructures (#104305) with sqlc-generated
structs. This also makes it possible to use more specific structs that
are more taylored to the specific queries, increasing efficiency.

This commit deals with the remaining areas, like the job deleter, task
timeout checker, and task state machine. And anything else to get things
running again.

Functional changes are kept to a minimum, as the API still serves the
same data.

Because this work covers so much of Flamenco's code, it's been split up
into different commits. Each commit brings Flamenco to a state where it
compiles and unit tests pass. Only the result of the final commit has
actually been tested properly.

Ref: #104343
This commit is contained in:
Sybren A. Stüvel 2024-11-12 09:55:34 +01:00
parent ddced5a823
commit 531a0184f7
48 changed files with 1474 additions and 1399 deletions

View File

@ -32,23 +32,24 @@ import (
type PersistenceService interface {
StoreAuthoredJob(ctx context.Context, authoredJob job_compilers.AuthoredJob) error
// FetchJob fetches a single job, without fetching its tasks.
FetchJob(ctx context.Context, jobID string) (*persistence.Job, error)
FetchJob(ctx context.Context, jobUUID string) (*persistence.Job, error)
FetchJobs(ctx context.Context) ([]*persistence.Job, error)
FetchJobByID(ctx context.Context, jobID int64) (*persistence.Job, error)
SaveJobPriority(ctx context.Context, job *persistence.Job) error
// FetchTask fetches the given task and the accompanying job.
FetchTask(ctx context.Context, taskID string) (*persistence.Task, error)
FetchTask(ctx context.Context, taskID string) (persistence.TaskJobWorker, error)
// FetchTaskJobUUID fetches the UUID of the job this task belongs to.
FetchTaskJobUUID(ctx context.Context, taskID string) (string, error)
FetchTaskFailureList(context.Context, *persistence.Task) ([]*persistence.Worker, error)
SaveTaskActivity(ctx context.Context, t *persistence.Task) error
// TaskTouchedByWorker marks the task as 'touched' by a worker. This is used for timeout detection.
TaskTouchedByWorker(context.Context, *persistence.Task) error
TaskTouchedByWorker(ctx context.Context, taskUUID string) error
CreateWorker(ctx context.Context, w *persistence.Worker) error
FetchWorker(ctx context.Context, uuid string) (*persistence.Worker, error)
FetchWorkers(ctx context.Context) ([]*persistence.Worker, error)
FetchWorkerTask(context.Context, *persistence.Worker) (*persistence.Task, error)
FetchWorkerTask(context.Context, *persistence.Worker) (*persistence.TaskJob, error)
SaveWorker(ctx context.Context, w *persistence.Worker) error
SaveWorkerStatus(ctx context.Context, w *persistence.Worker) error
WorkerSeen(ctx context.Context, w *persistence.Worker) error
@ -56,7 +57,7 @@ type PersistenceService interface {
// ScheduleTask finds a task to execute by the given worker, and assigns it to that worker.
// If no task is available, (nil, nil) is returned, as this is not an error situation.
ScheduleTask(ctx context.Context, w *persistence.Worker) (*persistence.Task, error)
ScheduleTask(ctx context.Context, w *persistence.Worker) (*persistence.ScheduledTask, error)
AddWorkerToTaskFailedList(context.Context, *persistence.Task, *persistence.Worker) (numFailed int, err error)
// ClearFailureListOfTask clears the list of workers that failed this task.
ClearFailureListOfTask(context.Context, *persistence.Task) error
@ -64,7 +65,7 @@ type PersistenceService interface {
ClearFailureListOfJob(context.Context, *persistence.Job) error
// AddWorkerToJobBlocklist prevents this Worker of getting any task, of this type, on this job, from the task scheduler.
AddWorkerToJobBlocklist(ctx context.Context, job *persistence.Job, worker *persistence.Worker, taskType string) error
AddWorkerToJobBlocklist(ctx context.Context, jobID int64, workerID int64, taskType string) error
FetchJobBlocklist(ctx context.Context, jobUUID string) ([]persistence.JobBlockListEntry, error)
RemoveFromJobBlocklist(ctx context.Context, jobUUID, workerUUID, taskType string) error
ClearJobBlocklist(ctx context.Context, job *persistence.Job) error
@ -73,6 +74,7 @@ type PersistenceService interface {
WorkerSetTags(ctx context.Context, worker *persistence.Worker, tagUUIDs []string) error
CreateWorkerTag(ctx context.Context, tag *persistence.WorkerTag) error
FetchWorkerTag(ctx context.Context, uuid string) (persistence.WorkerTag, error)
FetchWorkerTagByID(ctx context.Context, id int64) (persistence.WorkerTag, error)
FetchWorkerTags(ctx context.Context) ([]persistence.WorkerTag, error)
DeleteWorkerTag(ctx context.Context, uuid string) error
SaveWorkerTag(ctx context.Context, tag *persistence.WorkerTag) error
@ -81,13 +83,13 @@ type PersistenceService interface {
// WorkersLeftToRun returns a set of worker UUIDs that can run tasks of the given type on the given job.
WorkersLeftToRun(ctx context.Context, job *persistence.Job, taskType string) (map[string]bool, error)
// CountTaskFailuresOfWorker returns the number of task failures of this worker, on this particular job and task type.
CountTaskFailuresOfWorker(ctx context.Context, job *persistence.Job, worker *persistence.Worker, taskType string) (int, error)
CountTaskFailuresOfWorker(ctx context.Context, jobUUID string, workerID int64, taskType string) (int, error)
// Database queries.
QueryJobTaskSummaries(ctx context.Context, jobUUID string) ([]*persistence.Task, error)
QueryJobTaskSummaries(ctx context.Context, jobUUID string) ([]persistence.TaskSummary, error)
// SetLastRendered sets this job as the one with the most recent rendered image.
SetLastRendered(ctx context.Context, j *persistence.Job) error
SetLastRendered(ctx context.Context, jobUUID string) error
// GetLastRendered returns the UUID of the job with the most recent rendered image.
GetLastRenderedJobUUID(ctx context.Context) (string, error)
}
@ -99,10 +101,10 @@ type TaskStateMachine interface {
TaskStatusChange(ctx context.Context, task *persistence.Task, newStatus api.TaskStatus) error
// JobStatusChange gives a Job a new status, and handles the resulting status changes on its tasks.
JobStatusChange(ctx context.Context, job *persistence.Job, newJobStatus api.JobStatus, reason string) error
JobStatusChange(ctx context.Context, jobUUID string, newJobStatus api.JobStatus, reason string) error
RequeueActiveTasksOfWorker(ctx context.Context, worker *persistence.Worker, reason string) error
RequeueFailedTasksOfWorkerOfJob(ctx context.Context, worker *persistence.Worker, job *persistence.Job, reason string) error
RequeueFailedTasksOfWorkerOfJob(ctx context.Context, worker *persistence.Worker, jobUUID string, reason string) error
}
// TaskStateMachine should be a subset of task_state_machine.StateMachine.
@ -140,12 +142,12 @@ type JobCompiler interface {
// LogStorage handles incoming task logs.
type LogStorage interface {
Write(logger zerolog.Logger, jobID, taskID string, logText string) error
WriteTimestamped(logger zerolog.Logger, jobID, taskID string, logText string) error
RotateFile(logger zerolog.Logger, jobID, taskID string)
Tail(jobID, taskID string) (string, error)
TaskLogSize(jobID, taskID string) (int64, error)
Filepath(jobID, taskID string) string
Write(logger zerolog.Logger, jobUUID, taskUUID string, logText string) error
WriteTimestamped(logger zerolog.Logger, jobUUID, taskUUID string, logText string) error
RotateFile(logger zerolog.Logger, jobUUID, taskUUID string)
Tail(jobID, taskUUID string) (string, error)
TaskLogSize(jobUUID, taskUUID string) (int64, error)
Filepath(jobUUID, taskUUID string) string
}
// LastRendered processes the "last rendered" images.

View File

@ -4,6 +4,7 @@ package api_impl
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
@ -15,6 +16,7 @@ import (
"github.com/labstack/echo/v4"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"projects.blender.org/studio/flamenco/internal/manager/eventbus"
"projects.blender.org/studio/flamenco/internal/manager/job_compilers"
@ -112,7 +114,7 @@ func (f *Flamenco) SubmitJob(e echo.Context) error {
jobUpdate := eventbus.NewJobUpdate(dbJob)
f.broadcaster.BroadcastNewJob(jobUpdate)
apiJob := jobDBtoAPI(dbJob)
apiJob := jobDBtoAPI(ctx, f.persist, dbJob)
return e.JSON(http.StatusOK, apiJob)
}
@ -179,7 +181,8 @@ func (f *Flamenco) DeleteJob(e echo.Context, jobID string) error {
}
logger = logger.With().
Uint("dbID", dbJob.ID).
Int64("dbID", dbJob.ID).
Str("job", dbJob.UUID).
Str("currentstatus", string(dbJob.Status)).
Logger()
logger.Info().Msg("job deletion requested")
@ -283,9 +286,9 @@ func (f *Flamenco) DeleteJobMass(e echo.Context) error {
}
// SetJobStatus is used by the web interface to change a job's status.
func (f *Flamenco) SetJobStatus(e echo.Context, jobID string) error {
func (f *Flamenco) SetJobStatus(e echo.Context, jobUUID string) error {
logger := requestLogger(e).With().
Str("job", jobID).
Str("job", jobUUID).
Logger()
var statusChange api.SetJobStatusJSONRequestBody
@ -294,7 +297,7 @@ func (f *Flamenco) SetJobStatus(e echo.Context, jobID string) error {
return sendAPIError(e, http.StatusBadRequest, "invalid format")
}
dbJob, err := f.fetchJob(e, logger, jobID)
dbJob, err := f.fetchJob(e, logger, jobUUID)
if dbJob == nil {
// f.fetchJob already sent a response.
return err
@ -308,7 +311,7 @@ func (f *Flamenco) SetJobStatus(e echo.Context, jobID string) error {
logger.Info().Msg("job status change requested")
ctx := e.Request().Context()
err = f.stateMachine.JobStatusChange(ctx, dbJob, statusChange.Status, statusChange.Reason)
err = f.stateMachine.JobStatusChange(ctx, jobUUID, statusChange.Status, statusChange.Reason)
if err != nil {
logger.Error().Err(err).Msg("error changing job status")
return sendAPIError(e, http.StatusInternalServerError, "unexpected error changing job status")
@ -352,7 +355,7 @@ func (f *Flamenco) SetJobPriority(e echo.Context, jobID string) error {
logger = logger.With().
Str("jobName", dbJob.Name).
Int("prioCurrent", dbJob.Priority).
Int64("prioCurrent", dbJob.Priority).
Int("prioRequested", prioChange.Priority).
Logger()
logger.Info().Msg("job priority change requested")
@ -361,7 +364,7 @@ func (f *Flamenco) SetJobPriority(e echo.Context, jobID string) error {
bgCtx, bgCtxCancel := bgContext()
defer bgCtxCancel()
dbJob.Priority = prioChange.Priority
dbJob.Priority = int64(prioChange.Priority)
err = f.persist.SaveJobPriority(bgCtx, dbJob)
if err != nil {
logger.Error().Err(err).Msg("error changing job priority")
@ -388,7 +391,7 @@ func (f *Flamenco) SetTaskStatus(e echo.Context, taskID string) error {
return sendAPIError(e, http.StatusBadRequest, "invalid format")
}
dbTask, err := f.persist.FetchTask(ctx, taskID)
taskJobWorker, err := f.persist.FetchTask(ctx, taskID)
if err != nil {
if errors.Is(err, persistence.ErrTaskNotFound) {
return sendAPIError(e, http.StatusNotFound, "no such task")
@ -396,6 +399,9 @@ func (f *Flamenco) SetTaskStatus(e echo.Context, taskID string) error {
logger.Error().Err(err).Msg("error fetching task")
return sendAPIError(e, http.StatusInternalServerError, "error fetching task")
}
dbTask := &taskJobWorker.Task
// TODO: do the rest of the processing in a background context.
logger = logger.With().
Str("currentstatus", string(dbTask.Status)).
@ -660,80 +666,100 @@ func (f *Flamenco) lastRenderedInfoForJob(logger zerolog.Logger, jobUUID string)
return &info, nil
}
func jobDBtoAPI(dbJob *persistence.Job) api.Job {
// jobDBtoAPI converts the job from the database struct to the API struct.
//
// Note that this function does not connect to the database, and thus cannot
// find the job's worker tag.
func jobDBtoAPI(ctx context.Context, persist PersistenceService, dbJob *persistence.Job) api.Job {
apiJob := api.Job{
SubmittedJob: api.SubmittedJob{
Name: dbJob.Name,
Priority: dbJob.Priority,
Priority: int(dbJob.Priority),
Type: dbJob.JobType,
},
Id: dbJob.UUID,
Created: dbJob.CreatedAt,
Updated: dbJob.UpdatedAt,
Updated: dbJob.UpdatedAt.Time,
Status: api.JobStatus(dbJob.Status),
Activity: dbJob.Activity,
}
apiJob.Settings = &api.JobSettings{AdditionalProperties: dbJob.Settings}
apiJob.Metadata = &api.JobMetadata{AdditionalProperties: dbJob.Metadata}
{ // Parse job settings JSON.
settings := api.JobSettings{}
if err := json.Unmarshal(dbJob.Settings, &settings.AdditionalProperties); err != nil {
log.Error().Str("job", dbJob.UUID).AnErr("cause", err).Msg("could not parse job settings in database as JSON")
} else {
apiJob.Settings = &settings
}
}
if dbJob.Storage.ShamanCheckoutID != "" {
{ // Parse job metadata JSON.
metadata := api.JobMetadata{}
if err := json.Unmarshal(dbJob.Metadata, &metadata.AdditionalProperties); err != nil {
log.Error().Str("job", dbJob.UUID).AnErr("cause", err).Msg("could not parse job metadata in database as JSON")
} else {
apiJob.Metadata = &metadata
}
}
if dbJob.StorageShamanCheckoutID != "" {
apiJob.Storage = &api.JobStorageInfo{
ShamanCheckoutId: &dbJob.Storage.ShamanCheckoutID,
ShamanCheckoutId: &dbJob.StorageShamanCheckoutID,
}
}
if dbJob.DeleteRequestedAt.Valid {
apiJob.DeleteRequestedAt = &dbJob.DeleteRequestedAt.Time
}
if dbJob.WorkerTag != nil {
apiJob.WorkerTag = &dbJob.WorkerTag.UUID
if dbJob.WorkerTagID.Valid {
// TODO: see if this can be handled by the callers.
tag, err := persist.FetchWorkerTagByID(ctx, dbJob.WorkerTagID.Int64)
if err != nil {
log.Error().Str("job", dbJob.UUID).AnErr("cause", err).Msg("could not find job's worker tag")
}
apiJob.WorkerTag = &tag.UUID
}
return apiJob
}
func taskDBtoAPI(dbTask *persistence.Task) api.Task {
func taskJobWorkertoAPI(taskJobWorker persistence.TaskJobWorker) api.Task {
return taskToAPI(&taskJobWorker.Task, taskJobWorker.JobUUID, taskJobWorker.WorkerUUID)
}
func taskToAPI(task *persistence.Task, jobUUID, workerUUID string) api.Task {
apiTask := api.Task{
Id: dbTask.UUID,
Name: dbTask.Name,
Priority: dbTask.Priority,
TaskType: dbTask.Type,
Created: dbTask.CreatedAt,
Updated: dbTask.UpdatedAt,
Status: dbTask.Status,
Activity: dbTask.Activity,
Commands: make([]api.Command, len(dbTask.Commands)),
Id: task.UUID,
IndexInJob: int(task.IndexInJob),
JobId: jobUUID,
Name: task.Name,
Priority: int(task.Priority),
TaskType: task.Type,
Created: task.CreatedAt,
Updated: task.UpdatedAt.Time,
Status: task.Status,
Activity: task.Activity,
// TODO: convert this to just store dbTask.WorkerUUID.
Worker: workerToTaskWorker(dbTask.Worker),
JobId: dbTask.JobUUID,
IndexInJob: dbTask.IndexInJob,
// TODO: update the web frontend just use the UUID, as we have not enough
// info here to fill the name & address fields.
Worker: &api.TaskWorker{Id: workerUUID},
}
if dbTask.Job != nil {
apiTask.JobId = dbTask.Job.UUID
if err := json.Unmarshal(task.Commands, &apiTask.Commands); err != nil {
log.Error().
Str("task", task.UUID).
AnErr("cause", err).
Msg("could not parse task commands JSON")
}
if !dbTask.LastTouchedAt.IsZero() {
apiTask.LastTouched = &dbTask.LastTouchedAt
}
for i := range dbTask.Commands {
apiTask.Commands[i] = commandDBtoAPI(dbTask.Commands[i])
if task.LastTouchedAt.Valid {
apiTask.LastTouched = &task.LastTouchedAt.Time
}
return apiTask
}
func commandDBtoAPI(dbCommand persistence.Command) api.Command {
return api.Command{
Name: dbCommand.Name,
Parameters: dbCommand.Parameters,
}
}
// workerToTaskWorker is nil-safe.
func workerToTaskWorker(worker *persistence.Worker) *api.TaskWorker {
if worker == nil {

View File

@ -55,7 +55,8 @@ func (f *Flamenco) FetchJob(e echo.Context, jobID string) error {
return err
}
apiJob := jobDBtoAPI(dbJob)
ctx := e.Request().Context()
apiJob := jobDBtoAPI(ctx, f.persist, dbJob)
return e.JSON(http.StatusOK, apiJob)
}
@ -75,7 +76,7 @@ func (f *Flamenco) FetchJobs(e echo.Context) error {
apiJobs := make([]api.Job, len(dbJobs))
for i, dbJob := range dbJobs {
apiJobs[i] = jobDBtoAPI(dbJob)
apiJobs[i] = jobDBtoAPI(ctx, f.persist, dbJob)
}
result := api.JobsQueryResult{
Jobs: apiJobs,
@ -94,7 +95,7 @@ func (f *Flamenco) FetchJobTasks(e echo.Context, jobID string) error {
return sendAPIError(e, http.StatusBadRequest, "job ID not valid")
}
tasks, err := f.persist.QueryJobTaskSummaries(ctx, jobID)
dbSummaries, err := f.persist.QueryJobTaskSummaries(ctx, jobID)
switch {
case errors.Is(err, context.Canceled):
logger.Debug().AnErr("cause", err).Msg("could not fetch job tasks, remote end probably closed connection")
@ -104,12 +105,12 @@ func (f *Flamenco) FetchJobTasks(e echo.Context, jobID string) error {
return sendAPIError(e, http.StatusInternalServerError, "error fetching job tasks: %v", err)
}
summaries := make([]api.TaskSummary, len(tasks))
for i, task := range tasks {
summaries[i] = taskDBtoSummary(task)
apiSummaries := make([]api.TaskSummary, len(dbSummaries))
for i, dbSummary := range dbSummaries {
apiSummaries[i] = taskSummaryDBtoAPI(dbSummary)
}
result := api.JobTasksSummary{
Tasks: &summaries,
Tasks: &apiSummaries,
}
return e.JSON(http.StatusOK, result)
}
@ -125,8 +126,8 @@ 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)
// Fetch & convert the taskJobWorker.
taskJobWorker, err := f.persist.FetchTask(ctx, taskID)
if errors.Is(err, persistence.ErrTaskNotFound) {
logger.Debug().Msg("non-existent task requested")
return sendAPIError(e, http.StatusNotFound, "no such task")
@ -135,10 +136,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)
apiTask := taskJobWorkertoAPI(taskJobWorker)
// Fetch the worker. TODO: get rid of this conversion, just include the
// worker's UUID and let the caller fetch the worker info themselves if
// necessary.
taskWorker, err := f.persist.FetchWorker(ctx, taskJobWorker.WorkerUUID)
if err != nil {
logger.Warn().Err(err).Msg("error fetching task worker")
return sendAPIError(e, http.StatusInternalServerError, "error fetching task worker")
}
apiTask.Worker = workerToTaskWorker(taskWorker)
// Fetch & convert the failure list.
failedWorkers, err := f.persist.FetchTaskFailureList(ctx, task)
failedWorkers, err := f.persist.FetchTaskFailureList(ctx, &taskJobWorker.Task)
if err != nil {
logger.Warn().Err(err).Msg("error fetching task failure list")
return sendAPIError(e, http.StatusInternalServerError, "error fetching task failure list")
@ -152,14 +163,26 @@ func (f *Flamenco) FetchTask(e echo.Context, taskID string) error {
return e.JSON(http.StatusOK, apiTask)
}
func taskDBtoSummary(task *persistence.Task) api.TaskSummary {
func taskSummaryDBtoAPI(task persistence.TaskSummary) api.TaskSummary {
return api.TaskSummary{
Id: task.UUID,
Name: task.Name,
IndexInJob: task.IndexInJob,
Priority: task.Priority,
IndexInJob: int(task.IndexInJob),
Priority: int(task.Priority),
Status: task.Status,
TaskType: task.Type,
Updated: task.UpdatedAt,
Updated: task.UpdatedAt.Time,
}
}
func taskDBtoSummaryAPI(task persistence.Task) api.TaskSummary {
return api.TaskSummary{
Id: task.UUID,
Name: task.Name,
IndexInJob: int(task.IndexInJob),
Priority: int(task.Priority),
Status: task.Status,
TaskType: task.Type,
Updated: task.UpdatedAt.Time,
}
}

View File

@ -25,12 +25,8 @@ func TestFetchJobs(t *testing.T) {
JobType: "test",
Priority: 50,
Status: api.JobStatusActive,
Settings: persistence.StringInterfaceMap{
"result": "/render/frames/exploding.kittens",
},
Metadata: persistence.StringStringMap{
"project": "/projects/exploding-kittens",
},
Settings: []byte(`{"result": "/render/frames/exploding.kittens"}`),
Metadata: []byte(`{"project": "/projects/exploding-kittens"}`),
}
deletionRequestedAt := time.Now()
@ -76,8 +72,6 @@ func TestFetchJobs(t *testing.T) {
Name: "уходить",
Type: "test",
Priority: 75,
Settings: &api.JobSettings{},
Metadata: &api.JobMetadata{},
},
Id: "d912ac69-de48-48ba-8028-35d82cb41451",
Status: api.JobStatusCompleted,
@ -101,21 +95,20 @@ func TestFetchJob(t *testing.T) {
JobType: "test",
Priority: 50,
Status: api.JobStatusActive,
Settings: persistence.StringInterfaceMap{
"result": "/render/frames/exploding.kittens",
},
Metadata: persistence.StringStringMap{
"project": "/projects/exploding-kittens",
},
WorkerTag: &persistence.WorkerTag{
Settings: []byte(`{"result": "/render/frames/exploding.kittens"}`),
Metadata: []byte(`{"project": "/projects/exploding-kittens"}`),
WorkerTagID: sql.NullInt64{Int64: 4477, Valid: true},
}
tag := persistence.WorkerTag{
UUID: "d86e1b84-5ee2-4784-a178-65963eeb484b",
Name: "Tikkie terug Kees!",
Description: "",
},
}
echoCtx := mf.prepareMockedRequest(nil)
mf.persistence.EXPECT().FetchJob(gomock.Any(), dbJob.UUID).Return(&dbJob, nil)
mf.persistence.EXPECT().FetchWorkerTagByID(gomock.Any(), dbJob.WorkerTagID.Int64).Return(tag, nil)
require.NoError(t, mf.flamenco.FetchJob(echoCtx, dbJob.UUID))
@ -152,30 +145,27 @@ func TestFetchTask(t *testing.T) {
taskWorker := persistence.Worker{UUID: workerUUID, Name: "Radnik", Address: "Slapić"}
dbTask := persistence.Task{
Model: persistence.Model{
ID: 327,
CreatedAt: mf.clock.Now().Add(-30 * time.Second),
UpdatedAt: mf.clock.Now(),
},
UpdatedAt: sql.NullTime{Time: mf.clock.Now(), Valid: true},
UUID: taskUUID,
Name: "симпатичная задача",
Type: "misc",
JobID: 0,
Job: &persistence.Job{UUID: jobUUID},
JobID: 332277,
Priority: 47,
Status: api.TaskStatusQueued,
WorkerID: new(uint),
Worker: &taskWorker,
Dependencies: []*persistence.Task{},
WorkerID: sql.NullInt64{Int64: taskWorker.ID, Valid: true},
Activity: "used in unit test",
Commands: []persistence.Command{
{Name: "move-directory",
Parameters: map[string]interface{}{
Commands: []byte(`[
{
"name": "move-directory",
"parameters": {
"dest": "/render/_flamenco/tests/renders/2022-04-29 Weekly/2022-04-29_140531",
"src": "/render/_flamenco/tests/renders/2022-04-29 Weekly/2022-04-29_140531__intermediate-2022-04-29_140531",
}},
},
"src": "/render/_flamenco/tests/renders/2022-04-29 Weekly/2022-04-29_140531__intermediate-2022-04-29_140531"
}
}
]`),
}
expectAPITask := api.Task{
@ -187,7 +177,7 @@ func TestFetchTask(t *testing.T) {
Priority: 47,
Status: api.TaskStatusQueued,
TaskType: "misc",
Updated: dbTask.UpdatedAt,
Updated: dbTask.UpdatedAt.Time,
Worker: &api.TaskWorker{Id: workerUUID, Name: "Radnik", Address: "Slapić"},
Commands: []api.Command{
@ -203,11 +193,18 @@ func TestFetchTask(t *testing.T) {
}),
}
taskJobWorker := persistence.TaskJobWorker{
Task: dbTask,
JobUUID: jobUUID,
WorkerUUID: workerUUID,
}
echoCtx := mf.prepareMockedRequest(nil)
ctx := echoCtx.Request().Context()
mf.persistence.EXPECT().FetchTask(ctx, taskUUID).Return(&dbTask, nil)
mf.persistence.EXPECT().FetchTask(ctx, taskUUID).Return(taskJobWorker, nil)
mf.persistence.EXPECT().FetchTaskFailureList(ctx, &dbTask).
Return([]*persistence.Worker{&taskWorker}, nil)
mf.persistence.EXPECT().FetchWorker(ctx, workerUUID).Return(&taskWorker, nil)
err := mf.flamenco.FetchTask(echoCtx, taskUUID)
require.NoError(t, err)

View File

@ -3,6 +3,7 @@ package api_impl
// SPDX-License-Identifier: GPL-3.0-or-later
import (
"database/sql"
"errors"
"fmt"
"net/http"
@ -66,10 +67,8 @@ func TestSubmitJobWithoutSettings(t *testing.T) {
UUID: queuedJob.JobID,
Name: queuedJob.Name,
JobType: queuedJob.JobType,
Priority: queuedJob.Priority,
Priority: int64(queuedJob.Priority),
Status: queuedJob.Status,
Settings: persistence.StringInterfaceMap{},
Metadata: persistence.StringStringMap{},
}
mf.persistence.EXPECT().FetchJob(gomock.Any(), queuedJob.JobID).Return(&dbJob, nil)
@ -77,10 +76,10 @@ func TestSubmitJobWithoutSettings(t *testing.T) {
jobUpdate := api.EventJobUpdate{
Id: dbJob.UUID,
Name: &dbJob.Name,
Priority: dbJob.Priority,
Priority: int(dbJob.Priority),
Status: dbJob.Status,
Type: dbJob.JobType,
Updated: dbJob.UpdatedAt,
Updated: dbJob.UpdatedAt.Time,
}
mf.broadcaster.EXPECT().BroadcastNewJob(jobUpdate)
@ -155,10 +154,10 @@ func TestSubmitJobWithSettings(t *testing.T) {
UUID: queuedJob.JobID,
Name: queuedJob.Name,
JobType: queuedJob.JobType,
Priority: queuedJob.Priority,
Priority: int64(queuedJob.Priority),
Status: queuedJob.Status,
Settings: variableReplacedSettings,
Metadata: variableReplacedMetadata,
Settings: []byte(`{"result": "{frames}/exploding.kittens"}`),
Metadata: []byte(`{"project": "{projects}/exploding-kittens"}`),
}
mf.persistence.EXPECT().FetchJob(gomock.Any(), queuedJob.JobID).Return(&dbJob, nil)
@ -166,10 +165,10 @@ func TestSubmitJobWithSettings(t *testing.T) {
jobUpdate := api.EventJobUpdate{
Id: dbJob.UUID,
Name: &dbJob.Name,
Priority: dbJob.Priority,
Priority: int(dbJob.Priority),
Status: dbJob.Status,
Type: dbJob.JobType,
Updated: dbJob.UpdatedAt,
Updated: dbJob.UpdatedAt.Time,
}
mf.broadcaster.EXPECT().BroadcastNewJob(jobUpdate)
@ -226,10 +225,8 @@ func TestSubmitJobWithEtag(t *testing.T) {
UUID: authoredJob.JobID,
Name: authoredJob.Name,
JobType: authoredJob.JobType,
Priority: authoredJob.Priority,
Priority: int64(authoredJob.Priority),
Status: api.JobStatusQueued,
Settings: persistence.StringInterfaceMap{},
Metadata: persistence.StringStringMap{},
}
mf.persistence.EXPECT().FetchJob(gomock.Any(), authoredJob.JobID).Return(&dbJob, nil)
@ -293,13 +290,9 @@ func TestSubmitJobWithShamanCheckoutID(t *testing.T) {
UUID: queuedJob.JobID,
Name: queuedJob.Name,
JobType: queuedJob.JobType,
Priority: queuedJob.Priority,
Priority: int64(queuedJob.Priority),
Status: queuedJob.Status,
Settings: persistence.StringInterfaceMap{},
Metadata: persistence.StringStringMap{},
Storage: persistence.JobStorageInfo{
ShamanCheckoutID: "Весы/Синтел",
},
StorageShamanCheckoutID: "Весы/Синтел",
}
mf.persistence.EXPECT().FetchJob(gomock.Any(), queuedJob.JobID).Return(&dbJob, nil)
@ -307,10 +300,10 @@ func TestSubmitJobWithShamanCheckoutID(t *testing.T) {
jobUpdate := api.EventJobUpdate{
Id: dbJob.UUID,
Name: &dbJob.Name,
Priority: dbJob.Priority,
Priority: int(dbJob.Priority),
Status: dbJob.Status,
Type: dbJob.JobType,
Updated: dbJob.UpdatedAt,
Updated: dbJob.UpdatedAt.Time,
}
mf.broadcaster.EXPECT().BroadcastNewJob(jobUpdate)
@ -368,34 +361,30 @@ func TestSubmitJobWithWorkerTag(t *testing.T) {
queuedJob.Status = api.JobStatusQueued
mf.persistence.EXPECT().StoreAuthoredJob(gomock.Any(), queuedJob).Return(nil)
// Expect the job to be fetched from the database again:
// Expect the job to be fetched from the database again, including its tag.
dbJob := persistence.Job{
Model: persistence.Model{
ID: 47,
CreatedAt: mf.clock.Now(),
UpdatedAt: mf.clock.Now(),
},
UpdatedAt: sql.NullTime{Time: mf.clock.Now(), Valid: true},
UUID: queuedJob.JobID,
Name: queuedJob.Name,
JobType: queuedJob.JobType,
Priority: queuedJob.Priority,
Priority: int64(queuedJob.Priority),
Status: queuedJob.Status,
Settings: persistence.StringInterfaceMap{},
Metadata: persistence.StringStringMap{},
WorkerTagID: ptr(uint(tag.ID)),
WorkerTag: &tag,
WorkerTagID: sql.NullInt64{Int64: tag.ID, Valid: true},
}
mf.persistence.EXPECT().FetchJob(gomock.Any(), queuedJob.JobID).Return(&dbJob, nil)
mf.persistence.EXPECT().FetchWorkerTagByID(gomock.Any(), tag.ID).Return(tag, nil)
// Expect the new job to be broadcast.
jobUpdate := api.EventJobUpdate{
Id: dbJob.UUID,
Name: &dbJob.Name,
Priority: dbJob.Priority,
Priority: int(dbJob.Priority),
Status: dbJob.Status,
Type: dbJob.JobType,
Updated: dbJob.UpdatedAt,
Updated: dbJob.UpdatedAt.Time,
}
mf.broadcaster.EXPECT().BroadcastNewJob(jobUpdate)
@ -404,14 +393,12 @@ func TestSubmitJobWithWorkerTag(t *testing.T) {
requestWorkerStore(echoCtx, &worker)
require.NoError(t, mf.flamenco.SubmitJob(echoCtx))
submittedJob.Metadata = new(api.JobMetadata)
submittedJob.Settings = new(api.JobSettings)
submittedJob.SubmitterPlatform = "" // Not persisted in the database.
assertResponseJSON(t, echoCtx, http.StatusOK, api.Job{
SubmittedJob: submittedJob,
Id: dbJob.UUID,
Created: dbJob.CreatedAt,
Updated: dbJob.UpdatedAt,
Updated: dbJob.UpdatedAt.Time,
DeleteRequestedAt: nil,
Activity: "",
Status: api.JobStatusQueued,
@ -583,14 +570,12 @@ func TestSetJobStatus_happy(t *testing.T) {
UUID: jobID,
Name: "test job",
Status: api.JobStatusActive,
Settings: persistence.StringInterfaceMap{},
Metadata: persistence.StringStringMap{},
}
// Set up expectations.
ctx := gomock.Any()
mf.persistence.EXPECT().FetchJob(ctx, jobID).Return(&dbJob, nil)
mf.stateMachine.EXPECT().JobStatusChange(ctx, &dbJob, statusUpdate.Status, "someone pushed a button")
mf.stateMachine.EXPECT().JobStatusChange(ctx, jobID, statusUpdate.Status, "someone pushed a button")
// Going to Cancel Requested should NOT clear the failure list.
@ -635,8 +620,6 @@ func TestSetJobPrio(t *testing.T) {
UUID: jobID,
Name: "test job",
Priority: 50,
Settings: persistence.StringInterfaceMap{},
Metadata: persistence.StringStringMap{},
}
echoCtx := mf.prepareMockedJSONRequest(prioUpdate)
@ -655,7 +638,7 @@ func TestSetJobPrio(t *testing.T) {
RefreshTasks: false,
Priority: prioUpdate.Priority,
Status: dbJob.Status,
Updated: dbJob.UpdatedAt,
Updated: dbJob.UpdatedAt.Time,
}
mf.broadcaster.EXPECT().BroadcastJobUpdate(expectUpdate)
@ -680,15 +663,13 @@ func TestSetJobStatusFailedToRequeueing(t *testing.T) {
UUID: jobID,
Name: "test job",
Status: api.JobStatusFailed,
Settings: persistence.StringInterfaceMap{},
Metadata: persistence.StringStringMap{},
}
// Set up expectations.
echoCtx := mf.prepareMockedJSONRequest(statusUpdate)
ctx := echoCtx.Request().Context()
mf.persistence.EXPECT().FetchJob(moremock.ContextWithDeadline(), jobID).Return(&dbJob, nil)
mf.stateMachine.EXPECT().JobStatusChange(ctx, &dbJob, statusUpdate.Status, "someone pushed a button")
mf.stateMachine.EXPECT().JobStatusChange(ctx, jobID, statusUpdate.Status, "someone pushed a button")
mf.persistence.EXPECT().ClearFailureListOfJob(ctx, &dbJob)
mf.persistence.EXPECT().ClearJobBlocklist(ctx, &dbJob)
@ -712,31 +693,35 @@ func TestSetTaskStatusQueued(t *testing.T) {
Reason: "someone pushed a button",
}
dbJob := persistence.Job{
Model: persistence.Model{ID: 47},
ID: 47,
UUID: jobID,
Name: "test job",
Status: api.JobStatusFailed,
Settings: persistence.StringInterfaceMap{},
Metadata: persistence.StringStringMap{},
}
dbTask := persistence.Task{
UUID: taskID,
Name: "test task",
Status: api.TaskStatusFailed,
Job: &dbJob,
JobID: dbJob.ID,
}
// Set up expectations.
echoCtx := mf.prepareMockedJSONRequest(statusUpdate)
ctx := echoCtx.Request().Context()
mf.persistence.EXPECT().FetchTask(ctx, taskID).Return(&dbTask, nil)
mf.stateMachine.EXPECT().TaskStatusChange(ctx, &dbTask, statusUpdate.Status)
mf.persistence.EXPECT().ClearFailureListOfTask(ctx, &dbTask)
updatedTask := dbTask
updatedTask.Activity = "someone pushed a button"
mf.persistence.EXPECT().SaveTaskActivity(ctx, &updatedTask)
taskJobWorker := persistence.TaskJobWorker{
Task: dbTask,
JobUUID: dbJob.UUID,
WorkerUUID: "",
}
taskWithActivity := dbTask
taskWithActivity.Activity = "someone pushed a button"
mf.persistence.EXPECT().FetchTask(ctx, taskID).Return(taskJobWorker, nil)
mf.persistence.EXPECT().SaveTaskActivity(ctx, &taskWithActivity)
mf.stateMachine.EXPECT().TaskStatusChange(ctx, &taskWithActivity, statusUpdate.Status)
mf.persistence.EXPECT().ClearFailureListOfTask(ctx, &taskWithActivity)
// Do the call.
err := mf.flamenco.SetTaskStatus(echoCtx, taskID)
@ -914,12 +899,10 @@ func TestDeleteJob(t *testing.T) {
jobID := "18a9b096-d77e-438c-9be2-74397038298b"
dbJob := persistence.Job{
Model: persistence.Model{ID: 47},
ID: 47,
UUID: jobID,
Name: "test job",
Status: api.JobStatusFailed,
Settings: persistence.StringInterfaceMap{},
Metadata: persistence.StringStringMap{},
}
// Set up expectations.

View File

@ -44,7 +44,7 @@ func (m *MockPersistenceService) EXPECT() *MockPersistenceServiceMockRecorder {
}
// AddWorkerToJobBlocklist mocks base method.
func (m *MockPersistenceService) AddWorkerToJobBlocklist(arg0 context.Context, arg1 *persistence.Job, arg2 *sqlc.Worker, arg3 string) error {
func (m *MockPersistenceService) AddWorkerToJobBlocklist(arg0 context.Context, arg1, arg2 int64, arg3 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddWorkerToJobBlocklist", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(error)
@ -58,7 +58,7 @@ func (mr *MockPersistenceServiceMockRecorder) AddWorkerToJobBlocklist(arg0, arg1
}
// AddWorkerToTaskFailedList mocks base method.
func (m *MockPersistenceService) AddWorkerToTaskFailedList(arg0 context.Context, arg1 *persistence.Task, arg2 *sqlc.Worker) (int, error) {
func (m *MockPersistenceService) AddWorkerToTaskFailedList(arg0 context.Context, arg1 *sqlc.Task, arg2 *sqlc.Worker) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddWorkerToTaskFailedList", arg0, arg1, arg2)
ret0, _ := ret[0].(int)
@ -73,7 +73,7 @@ func (mr *MockPersistenceServiceMockRecorder) AddWorkerToTaskFailedList(arg0, ar
}
// ClearFailureListOfJob mocks base method.
func (m *MockPersistenceService) ClearFailureListOfJob(arg0 context.Context, arg1 *persistence.Job) error {
func (m *MockPersistenceService) ClearFailureListOfJob(arg0 context.Context, arg1 *sqlc.Job) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClearFailureListOfJob", arg0, arg1)
ret0, _ := ret[0].(error)
@ -87,7 +87,7 @@ func (mr *MockPersistenceServiceMockRecorder) ClearFailureListOfJob(arg0, arg1 i
}
// ClearFailureListOfTask mocks base method.
func (m *MockPersistenceService) ClearFailureListOfTask(arg0 context.Context, arg1 *persistence.Task) error {
func (m *MockPersistenceService) ClearFailureListOfTask(arg0 context.Context, arg1 *sqlc.Task) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClearFailureListOfTask", arg0, arg1)
ret0, _ := ret[0].(error)
@ -101,7 +101,7 @@ func (mr *MockPersistenceServiceMockRecorder) ClearFailureListOfTask(arg0, arg1
}
// ClearJobBlocklist mocks base method.
func (m *MockPersistenceService) ClearJobBlocklist(arg0 context.Context, arg1 *persistence.Job) error {
func (m *MockPersistenceService) ClearJobBlocklist(arg0 context.Context, arg1 *sqlc.Job) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClearJobBlocklist", arg0, arg1)
ret0, _ := ret[0].(error)
@ -115,7 +115,7 @@ func (mr *MockPersistenceServiceMockRecorder) ClearJobBlocklist(arg0, arg1 inter
}
// CountTaskFailuresOfWorker mocks base method.
func (m *MockPersistenceService) CountTaskFailuresOfWorker(arg0 context.Context, arg1 *persistence.Job, arg2 *sqlc.Worker, arg3 string) (int, error) {
func (m *MockPersistenceService) CountTaskFailuresOfWorker(arg0 context.Context, arg1 string, arg2 int64, arg3 string) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CountTaskFailuresOfWorker", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(int)
@ -186,10 +186,10 @@ func (mr *MockPersistenceServiceMockRecorder) DeleteWorkerTag(arg0, arg1 interfa
}
// FetchJob mocks base method.
func (m *MockPersistenceService) FetchJob(arg0 context.Context, arg1 string) (*persistence.Job, error) {
func (m *MockPersistenceService) FetchJob(arg0 context.Context, arg1 string) (*sqlc.Job, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchJob", arg0, arg1)
ret0, _ := ret[0].(*persistence.Job)
ret0, _ := ret[0].(*sqlc.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -215,11 +215,26 @@ func (mr *MockPersistenceServiceMockRecorder) FetchJobBlocklist(arg0, arg1 inter
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchJobBlocklist", reflect.TypeOf((*MockPersistenceService)(nil).FetchJobBlocklist), arg0, arg1)
}
// FetchJobByID mocks base method.
func (m *MockPersistenceService) FetchJobByID(arg0 context.Context, arg1 int64) (*sqlc.Job, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchJobByID", arg0, arg1)
ret0, _ := ret[0].(*sqlc.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FetchJobByID indicates an expected call of FetchJobByID.
func (mr *MockPersistenceServiceMockRecorder) FetchJobByID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchJobByID", reflect.TypeOf((*MockPersistenceService)(nil).FetchJobByID), arg0, arg1)
}
// FetchJobs mocks base method.
func (m *MockPersistenceService) FetchJobs(arg0 context.Context) ([]*persistence.Job, error) {
func (m *MockPersistenceService) FetchJobs(arg0 context.Context) ([]*sqlc.Job, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchJobs", arg0)
ret0, _ := ret[0].([]*persistence.Job)
ret0, _ := ret[0].([]*sqlc.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -246,10 +261,10 @@ func (mr *MockPersistenceServiceMockRecorder) FetchTagsOfWorker(arg0, arg1 inter
}
// FetchTask mocks base method.
func (m *MockPersistenceService) FetchTask(arg0 context.Context, arg1 string) (*persistence.Task, error) {
func (m *MockPersistenceService) FetchTask(arg0 context.Context, arg1 string) (persistence.TaskJobWorker, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchTask", arg0, arg1)
ret0, _ := ret[0].(*persistence.Task)
ret0, _ := ret[0].(persistence.TaskJobWorker)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -261,7 +276,7 @@ func (mr *MockPersistenceServiceMockRecorder) FetchTask(arg0, arg1 interface{})
}
// FetchTaskFailureList mocks base method.
func (m *MockPersistenceService) FetchTaskFailureList(arg0 context.Context, arg1 *persistence.Task) ([]*sqlc.Worker, error) {
func (m *MockPersistenceService) FetchTaskFailureList(arg0 context.Context, arg1 *sqlc.Task) ([]*sqlc.Worker, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchTaskFailureList", arg0, arg1)
ret0, _ := ret[0].([]*sqlc.Worker)
@ -320,6 +335,21 @@ func (mr *MockPersistenceServiceMockRecorder) FetchWorkerTag(arg0, arg1 interfac
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchWorkerTag", reflect.TypeOf((*MockPersistenceService)(nil).FetchWorkerTag), arg0, arg1)
}
// FetchWorkerTagByID mocks base method.
func (m *MockPersistenceService) FetchWorkerTagByID(arg0 context.Context, arg1 int64) (sqlc.WorkerTag, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchWorkerTagByID", arg0, arg1)
ret0, _ := ret[0].(sqlc.WorkerTag)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FetchWorkerTagByID indicates an expected call of FetchWorkerTagByID.
func (mr *MockPersistenceServiceMockRecorder) FetchWorkerTagByID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchWorkerTagByID", reflect.TypeOf((*MockPersistenceService)(nil).FetchWorkerTagByID), arg0, arg1)
}
// FetchWorkerTags mocks base method.
func (m *MockPersistenceService) FetchWorkerTags(arg0 context.Context) ([]sqlc.WorkerTag, error) {
m.ctrl.T.Helper()
@ -336,10 +366,10 @@ func (mr *MockPersistenceServiceMockRecorder) FetchWorkerTags(arg0 interface{})
}
// FetchWorkerTask mocks base method.
func (m *MockPersistenceService) FetchWorkerTask(arg0 context.Context, arg1 *sqlc.Worker) (*persistence.Task, error) {
func (m *MockPersistenceService) FetchWorkerTask(arg0 context.Context, arg1 *sqlc.Worker) (*persistence.TaskJob, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchWorkerTask", arg0, arg1)
ret0, _ := ret[0].(*persistence.Task)
ret0, _ := ret[0].(*persistence.TaskJob)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -381,10 +411,10 @@ func (mr *MockPersistenceServiceMockRecorder) GetLastRenderedJobUUID(arg0 interf
}
// QueryJobTaskSummaries mocks base method.
func (m *MockPersistenceService) QueryJobTaskSummaries(arg0 context.Context, arg1 string) ([]*persistence.Task, error) {
func (m *MockPersistenceService) QueryJobTaskSummaries(arg0 context.Context, arg1 string) ([]sqlc.QueryJobTaskSummariesRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryJobTaskSummaries", arg0, arg1)
ret0, _ := ret[0].([]*persistence.Task)
ret0, _ := ret[0].([]sqlc.QueryJobTaskSummariesRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -410,7 +440,7 @@ func (mr *MockPersistenceServiceMockRecorder) RemoveFromJobBlocklist(arg0, arg1,
}
// SaveJobPriority mocks base method.
func (m *MockPersistenceService) SaveJobPriority(arg0 context.Context, arg1 *persistence.Job) error {
func (m *MockPersistenceService) SaveJobPriority(arg0 context.Context, arg1 *sqlc.Job) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveJobPriority", arg0, arg1)
ret0, _ := ret[0].(error)
@ -424,7 +454,7 @@ func (mr *MockPersistenceServiceMockRecorder) SaveJobPriority(arg0, arg1 interfa
}
// SaveTaskActivity mocks base method.
func (m *MockPersistenceService) SaveTaskActivity(arg0 context.Context, arg1 *persistence.Task) error {
func (m *MockPersistenceService) SaveTaskActivity(arg0 context.Context, arg1 *sqlc.Task) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveTaskActivity", arg0, arg1)
ret0, _ := ret[0].(error)
@ -480,10 +510,10 @@ func (mr *MockPersistenceServiceMockRecorder) SaveWorkerTag(arg0, arg1 interface
}
// ScheduleTask mocks base method.
func (m *MockPersistenceService) ScheduleTask(arg0 context.Context, arg1 *sqlc.Worker) (*persistence.Task, error) {
func (m *MockPersistenceService) ScheduleTask(arg0 context.Context, arg1 *sqlc.Worker) (*persistence.ScheduledTask, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ScheduleTask", arg0, arg1)
ret0, _ := ret[0].(*persistence.Task)
ret0, _ := ret[0].(*persistence.ScheduledTask)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -495,7 +525,7 @@ func (mr *MockPersistenceServiceMockRecorder) ScheduleTask(arg0, arg1 interface{
}
// SetLastRendered mocks base method.
func (m *MockPersistenceService) SetLastRendered(arg0 context.Context, arg1 *persistence.Job) error {
func (m *MockPersistenceService) SetLastRendered(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetLastRendered", arg0, arg1)
ret0, _ := ret[0].(error)
@ -523,7 +553,7 @@ func (mr *MockPersistenceServiceMockRecorder) StoreAuthoredJob(arg0, arg1 interf
}
// TaskTouchedByWorker mocks base method.
func (m *MockPersistenceService) TaskTouchedByWorker(arg0 context.Context, arg1 *persistence.Task) error {
func (m *MockPersistenceService) TaskTouchedByWorker(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TaskTouchedByWorker", arg0, arg1)
ret0, _ := ret[0].(error)
@ -565,7 +595,7 @@ func (mr *MockPersistenceServiceMockRecorder) WorkerSetTags(arg0, arg1, arg2 int
}
// WorkersLeftToRun mocks base method.
func (m *MockPersistenceService) WorkersLeftToRun(arg0 context.Context, arg1 *persistence.Job, arg2 string) (map[string]bool, error) {
func (m *MockPersistenceService) WorkersLeftToRun(arg0 context.Context, arg1 *sqlc.Job, arg2 string) (map[string]bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WorkersLeftToRun", arg0, arg1, arg2)
ret0, _ := ret[0].(map[string]bool)
@ -1018,7 +1048,7 @@ func (m *MockTaskStateMachine) EXPECT() *MockTaskStateMachineMockRecorder {
}
// JobStatusChange mocks base method.
func (m *MockTaskStateMachine) JobStatusChange(arg0 context.Context, arg1 *persistence.Job, arg2 api.JobStatus, arg3 string) error {
func (m *MockTaskStateMachine) JobStatusChange(arg0 context.Context, arg1 string, arg2 api.JobStatus, arg3 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JobStatusChange", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(error)
@ -1046,7 +1076,7 @@ func (mr *MockTaskStateMachineMockRecorder) RequeueActiveTasksOfWorker(arg0, arg
}
// RequeueFailedTasksOfWorkerOfJob mocks base method.
func (m *MockTaskStateMachine) RequeueFailedTasksOfWorkerOfJob(arg0 context.Context, arg1 *sqlc.Worker, arg2 *persistence.Job, arg3 string) error {
func (m *MockTaskStateMachine) RequeueFailedTasksOfWorkerOfJob(arg0 context.Context, arg1 *sqlc.Worker, arg2, arg3 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RequeueFailedTasksOfWorkerOfJob", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(error)
@ -1060,7 +1090,7 @@ func (mr *MockTaskStateMachineMockRecorder) RequeueFailedTasksOfWorkerOfJob(arg0
}
// TaskStatusChange mocks base method.
func (m *MockTaskStateMachine) TaskStatusChange(arg0 context.Context, arg1 *persistence.Task, arg2 api.TaskStatus) error {
func (m *MockTaskStateMachine) TaskStatusChange(arg0 context.Context, arg1 *sqlc.Task, arg2 api.TaskStatus) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TaskStatusChange", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
@ -1390,7 +1420,7 @@ func (m *MockJobDeleter) EXPECT() *MockJobDeleterMockRecorder {
}
// QueueJobDeletion mocks base method.
func (m *MockJobDeleter) QueueJobDeletion(arg0 context.Context, arg1 *persistence.Job) error {
func (m *MockJobDeleter) QueueJobDeletion(arg0 context.Context, arg1 *sqlc.Job) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueueJobDeletion", arg0, arg1)
ret0, _ := ret[0].(error)
@ -1418,7 +1448,7 @@ func (mr *MockJobDeleterMockRecorder) QueueMassJobDeletion(arg0, arg1 interface{
}
// WhatWouldBeDeleted mocks base method.
func (m *MockJobDeleter) WhatWouldBeDeleted(arg0 *persistence.Job) api.JobDeletionInfo {
func (m *MockJobDeleter) WhatWouldBeDeleted(arg0 *sqlc.Job) api.JobDeletionInfo {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WhatWouldBeDeleted", arg0)
ret0, _ := ret[0].(api.JobDeletionInfo)

View File

@ -66,7 +66,7 @@ func (f *Flamenco) FetchWorker(e echo.Context, workerUUID string) error {
return sendAPIError(e, http.StatusInternalServerError, "error fetching worker tags: %v", err)
}
dbTask, err := f.persist.FetchWorkerTask(ctx, dbWorker)
taskJob, err := f.persist.FetchWorkerTask(ctx, dbWorker)
switch {
case errors.Is(err, context.Canceled):
return handleConnectionClosed(e, logger, "fetching task assigned to worker")
@ -78,10 +78,10 @@ func (f *Flamenco) FetchWorker(e echo.Context, workerUUID string) error {
logger.Debug().Msg("fetched worker")
apiWorker := workerDBtoAPI(*dbWorker)
if dbTask != nil {
if taskJob != nil {
apiWorkerTask := api.WorkerTask{
TaskSummary: taskDBtoSummary(dbTask),
JobId: dbTask.Job.UUID,
TaskSummary: taskDBtoSummaryAPI(taskJob.Task),
JobId: taskJob.JobUUID,
}
apiWorker.Task = &apiWorkerTask
}

View File

@ -96,17 +96,23 @@ func TestFetchWorker(t *testing.T) {
// Test with worker that does NOT have a status change requested, and DOES have an assigned task.
mf.persistence.EXPECT().FetchWorker(gomock.Any(), workerUUID).Return(&worker, nil)
assignedJob := persistence.Job{UUID: "f0e25ee4-0d13-4291-afc3-e9446b555aaf"}
assignedTask := persistence.Task{
UUID: "806057d5-759a-4e75-86a4-356d43f28cff",
Name: "test task",
Job: &persistence.Job{UUID: "f0e25ee4-0d13-4291-afc3-e9446b555aaf"},
Status: api.TaskStatusActive,
}
mf.persistence.EXPECT().FetchTagsOfWorker(gomock.Any(), workerUUID).Return([]persistence.WorkerTag{
{UUID: "0e701402-c4cc-49b0-8b8c-3eb8718d463a", Name: "EEVEE"},
{UUID: "59211f0a-81cc-4148-b0b7-32b3e2dcdb8f", Name: "Cycles"},
}, nil)
mf.persistence.EXPECT().FetchWorkerTask(gomock.Any(), &worker).Return(&assignedTask, nil)
assignedTaskJob := persistence.TaskJob{
Task: assignedTask,
JobUUID: assignedJob.UUID,
IsActive: true,
}
mf.persistence.EXPECT().FetchWorkerTask(gomock.Any(), &worker).Return(&assignedTaskJob, nil)
echo = mf.prepareMockedRequest(nil)
err = mf.flamenco.FetchWorker(echo, workerUUID)
@ -127,7 +133,7 @@ func TestFetchWorker(t *testing.T) {
Name: assignedTask.Name,
Status: assignedTask.Status,
},
JobId: assignedTask.Job.UUID,
JobId: assignedJob.UUID,
},
Tags: &[]api.WorkerTag{
{Id: ptr("0e701402-c4cc-49b0-8b8c-3eb8718d463a"), Name: "EEVEE"},

View File

@ -29,7 +29,7 @@ func (f *Flamenco) TaskUpdate(e echo.Context, taskID string) error {
// Fetch the task, to see if this worker is even allowed to send us updates.
ctx := e.Request().Context()
dbTask, err := f.persist.FetchTask(ctx, taskID)
taskJobWorker, err := f.persist.FetchTask(ctx, taskID)
if err != nil {
logger.Warn().Err(err).Msg("cannot fetch task")
if errors.Is(err, persistence.ErrTaskNotFound) {
@ -37,9 +37,6 @@ func (f *Flamenco) TaskUpdate(e echo.Context, taskID string) error {
}
return sendAPIError(e, http.StatusInternalServerError, "error fetching task")
}
if dbTask == nil {
panic("task could not be fetched, but database gave no error either")
}
// Decode the request body.
var taskUpdate api.TaskUpdate
@ -47,13 +44,15 @@ func (f *Flamenco) TaskUpdate(e echo.Context, taskID string) error {
logger.Warn().Err(err).Msg("bad request received")
return sendAPIError(e, http.StatusBadRequest, "invalid format")
}
if dbTask.WorkerID == nil {
if !taskJobWorker.Task.WorkerID.Valid {
logger.Warn().
Msg("worker trying to update task that's not assigned to any worker")
return sendAPIError(e, http.StatusConflict, "task %+v is not assigned to any worker, so also not to you", taskID)
}
if *dbTask.WorkerID != uint(worker.ID) {
logger.Warn().Msg("worker trying to update task that's assigned to another worker")
if taskJobWorker.Task.WorkerID.Int64 != worker.ID {
logger.Warn().
Str("assignedToWorker", taskJobWorker.WorkerUUID).
Msg("worker trying to update task that's assigned to another worker")
return sendAPIError(e, http.StatusConflict, "task %+v is not assigned to you", taskID)
}
@ -70,8 +69,8 @@ func (f *Flamenco) TaskUpdate(e echo.Context, taskID string) error {
bgCtx, bgCtxCancel := bgContext()
defer bgCtxCancel()
taskUpdateErr := f.doTaskUpdate(bgCtx, logger, worker, dbTask, taskUpdate)
workerUpdateErr := f.workerPingedTask(logger, dbTask)
taskUpdateErr := f.doTaskUpdate(bgCtx, logger, taskJobWorker.JobUUID, worker, &taskJobWorker.Task, taskUpdate)
workerUpdateErr := f.workerPingedTask(logger, taskJobWorker.Task.UUID)
workerSeenErr := f.workerSeen(logger, worker)
if taskUpdateErr != nil {
@ -91,14 +90,11 @@ func (f *Flamenco) TaskUpdate(e echo.Context, taskID string) error {
func (f *Flamenco) doTaskUpdate(
ctx context.Context,
logger zerolog.Logger,
jobUUID string,
w *persistence.Worker,
dbTask *persistence.Task,
update api.TaskUpdate,
) error {
if dbTask.Job == nil {
logger.Panic().Msg("dbTask.Job is nil, unable to continue")
}
var dbErrActivity error
if update.Activity != nil {
@ -113,7 +109,7 @@ func (f *Flamenco) doTaskUpdate(
// Manager in response to a status change, should be logged after that.
if update.Log != nil {
// Errors writing the log to disk are already logged by logStorage, and can be safely ignored here.
_ = f.logStorage.Write(logger, dbTask.Job.UUID, dbTask.UUID, *update.Log)
_ = f.logStorage.Write(logger, jobUUID, dbTask.UUID, *update.Log)
}
if update.TaskStatus == nil {
@ -124,7 +120,7 @@ func (f *Flamenco) doTaskUpdate(
var err error
if *update.TaskStatus == api.TaskStatusFailed {
// Failure is more complex than just going to the failed state.
err = f.onTaskFailed(ctx, logger, w, dbTask, update)
err = f.onTaskFailed(ctx, logger, w, dbTask, jobUUID, update)
} else {
// Just go to the given state.
err = f.stateMachine.TaskStatusChange(ctx, dbTask, *update.TaskStatus)
@ -150,6 +146,7 @@ func (f *Flamenco) onTaskFailed(
logger zerolog.Logger,
worker *persistence.Worker,
task *persistence.Task,
jobUUID string,
update api.TaskUpdate,
) error {
// Sanity check.
@ -164,18 +161,18 @@ func (f *Flamenco) onTaskFailed(
}
logger = logger.With().Str("taskType", task.Type).Logger()
wasBlacklisted, shoudlFailJob, err := f.maybeBlocklistWorker(ctx, logger, worker, task)
wasBlacklisted, shoudlFailJob, err := f.maybeBlocklistWorker(ctx, logger, worker, jobUUID, task)
if err != nil {
return fmt.Errorf("block-listing worker: %w", err)
}
if shoudlFailJob {
// There are no more workers left to finish the job.
return f.failJobAfterCatastroficTaskFailure(ctx, logger, worker, task)
return f.failJobAfterCatastroficTaskFailure(ctx, logger, worker, jobUUID, task)
}
if wasBlacklisted {
// Requeue all tasks of this job & task type that were hard-failed before by this worker.
reason := fmt.Sprintf("worker %s was blocked from tasks of type %q", worker.Name, task.Type)
err := f.stateMachine.RequeueFailedTasksOfWorkerOfJob(ctx, worker, task.Job, reason)
err := f.stateMachine.RequeueFailedTasksOfWorkerOfJob(ctx, worker, jobUUID, reason)
if err != nil {
return err
}
@ -189,7 +186,7 @@ func (f *Flamenco) onTaskFailed(
Logger()
if numFailed >= threshold {
return f.hardFailTask(ctx, logger, worker, task, numFailed)
return f.hardFailTask(ctx, logger, worker, jobUUID, task, numFailed)
}
numWorkers, err := f.numWorkersCapableOfRunningTask(ctx, task)
@ -203,9 +200,9 @@ func (f *Flamenco) onTaskFailed(
// and thus it is still counted.
// In such condition we should just fail the job itself.
if numWorkers <= 1 {
return f.failJobAfterCatastroficTaskFailure(ctx, logger, worker, task)
return f.failJobAfterCatastroficTaskFailure(ctx, logger, worker, jobUUID, task)
}
return f.softFailTask(ctx, logger, worker, task, numFailed)
return f.softFailTask(ctx, logger, worker, jobUUID, task, numFailed)
}
// maybeBlocklistWorker potentially block-lists the Worker, and checks whether
@ -218,11 +215,12 @@ func (f *Flamenco) maybeBlocklistWorker(
ctx context.Context,
logger zerolog.Logger,
worker *persistence.Worker,
jobUUID string,
task *persistence.Task,
) (wasBlacklisted, shouldFailJob bool, err error) {
numFailures, err := f.persist.CountTaskFailuresOfWorker(ctx, task.Job, worker, task.Type)
numFailures, err := f.persist.CountTaskFailuresOfWorker(ctx, jobUUID, worker.ID, task.Type)
if err != nil {
return false, false, fmt.Errorf("counting failures of worker on job %q, task type %q: %w", task.Job.UUID, task.Type, err)
return false, false, fmt.Errorf("counting failures of worker on job %q, task type %q: %w", jobUUID, task.Type, err)
}
// The received task update hasn't been persisted in the database yet,
// so we should count that too.
@ -238,7 +236,7 @@ func (f *Flamenco) maybeBlocklistWorker(
}
// Blocklist the Worker.
if err := f.blocklistWorker(ctx, logger, worker, task); err != nil {
if err := f.blocklistWorker(ctx, logger, worker, jobUUID, task); err != nil {
return true, false, err
}
@ -251,12 +249,13 @@ func (f *Flamenco) blocklistWorker(
ctx context.Context,
logger zerolog.Logger,
worker *persistence.Worker,
jobUUID string,
task *persistence.Task,
) error {
logger.Warn().
Str("job", task.Job.UUID).
Str("job", jobUUID).
Msg("block-listing worker")
err := f.persist.AddWorkerToJobBlocklist(ctx, task.Job, worker, task.Type)
err := f.persist.AddWorkerToJobBlocklist(ctx, task.JobID, worker.ID, task.Type)
if err != nil {
return fmt.Errorf("adding worker to block list: %w", err)
}
@ -264,11 +263,16 @@ func (f *Flamenco) blocklistWorker(
}
func (f *Flamenco) numWorkersCapableOfRunningTask(ctx context.Context, task *persistence.Task) (int, error) {
job, err := f.persist.FetchJobByID(ctx, task.JobID)
if err != nil {
return 0, fmt.Errorf("fetching job of task %s: %w", task.UUID, err)
}
// See which workers are left to run tasks of this type, on this job,
workersLeft, err := f.persist.WorkersLeftToRun(ctx, task.Job, task.Type)
workersLeft, err := f.persist.WorkersLeftToRun(ctx, job, task.Type)
if err != nil {
return 0, fmt.Errorf("fetching workers available to run tasks of type %q on job %q: %w",
task.Job.UUID, task.Type, err)
job.UUID, task.Type, err)
}
// Remove (from the list of available workers) those who failed this task before.
@ -290,13 +294,14 @@ func (f *Flamenco) failJobAfterCatastroficTaskFailure(
ctx context.Context,
logger zerolog.Logger,
worker *persistence.Worker,
jobUUID string,
task *persistence.Task,
) error {
taskLog := fmt.Sprintf(
"Task failed by worker %s, Manager will fail the entire job as there are no more workers left for tasks of type %q.",
worker.Identifier(), task.Type,
)
if err := f.logStorage.WriteTimestamped(logger, task.Job.UUID, task.UUID, taskLog); err != nil {
if err := f.logStorage.WriteTimestamped(logger, jobUUID, task.UUID, taskLog); err != nil {
logger.Error().Err(err).Msg("error writing failure notice to task log")
}
@ -309,17 +314,18 @@ func (f *Flamenco) failJobAfterCatastroficTaskFailure(
newJobStatus := api.JobStatusFailed
logger.Info().
Str("job", task.Job.UUID).
Str("job", jobUUID).
Str("newJobStatus", string(newJobStatus)).
Msg("no more workers left to run tasks of this type, failing the entire job")
reason := fmt.Sprintf("no more workers left to run tasks of type %q", task.Type)
return f.stateMachine.JobStatusChange(ctx, task.Job, newJobStatus, reason)
return f.stateMachine.JobStatusChange(ctx, jobUUID, newJobStatus, reason)
}
func (f *Flamenco) hardFailTask(
ctx context.Context,
logger zerolog.Logger,
worker *persistence.Worker,
jobUUID string,
task *persistence.Task,
numFailed int,
) error {
@ -329,7 +335,7 @@ func (f *Flamenco) hardFailTask(
"Task failed by %s, Manager will mark it as hard failure",
pluralizer.Pluralize("worker", numFailed, true),
)
if err := f.logStorage.WriteTimestamped(logger, task.Job.UUID, task.UUID, taskLog); err != nil {
if err := f.logStorage.WriteTimestamped(logger, jobUUID, task.UUID, taskLog); err != nil {
logger.Error().Err(err).Msg("error writing failure notice to task log")
}
@ -343,6 +349,7 @@ func (f *Flamenco) softFailTask(
ctx context.Context,
logger zerolog.Logger,
worker *persistence.Worker,
jobUUID string,
task *persistence.Task,
numFailed int,
) error {
@ -357,7 +364,7 @@ func (f *Flamenco) softFailTask(
failsToThreshold,
pluralizer.Pluralize("failure", failsToThreshold, false),
)
if err := f.logStorage.WriteTimestamped(logger, task.Job.UUID, task.UUID, taskLog); err != nil {
if err := f.logStorage.WriteTimestamped(logger, jobUUID, task.UUID, taskLog); err != nil {
logger.Error().Err(err).Msg("error writing failure notice to task log")
}

View File

@ -4,6 +4,7 @@ package api_impl
import (
"context"
"database/sql"
"testing"
"github.com/golang/mock/gomock"
@ -32,17 +33,21 @@ func TestTaskUpdate(t *testing.T) {
// Construct the task that's supposed to be updated.
taskID := "181eab68-1123-4790-93b1-94309a899411"
jobID := "e4719398-7cfa-4877-9bab-97c2d6c158b5"
mockJob := persistence.Job{UUID: jobID}
mockJob := persistence.Job{ID: 1234, UUID: jobID}
mockTask := persistence.Task{
UUID: taskID,
Worker: &worker,
WorkerID: ptr(uint(worker.ID)),
Job: &mockJob,
WorkerID: sql.NullInt64{Int64: worker.ID, Valid: true},
JobID: mockJob.ID,
Activity: "pre-update activity",
}
// Expect the task to be fetched.
mf.persistence.EXPECT().FetchTask(gomock.Any(), taskID).Return(&mockTask, nil)
taskJobWorker := persistence.TaskJobWorker{
Task: mockTask,
JobUUID: jobID,
WorkerUUID: worker.UUID,
}
mf.persistence.EXPECT().FetchTask(gomock.Any(), taskID).Return(taskJobWorker, nil)
// Expect the task status change to be handed to the state machine.
var statusChangedtask persistence.Task
@ -64,10 +69,10 @@ func TestTaskUpdate(t *testing.T) {
mf.logStorage.EXPECT().Write(gomock.Any(), jobID, taskID, "line1\nline2\n")
// Expect a 'touch' of the task.
var touchedTask persistence.Task
var touchedTaskUUID string
mf.persistence.EXPECT().TaskTouchedByWorker(gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, task *persistence.Task) error {
touchedTask = *task
func(ctx context.Context, taskUUID string) error {
touchedTaskUUID = taskUUID
return nil
})
mf.persistence.EXPECT().WorkerSeen(gomock.Any(), &worker)
@ -81,7 +86,7 @@ func TestTaskUpdate(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, mockTask.UUID, statusChangedtask.UUID)
assert.Equal(t, mockTask.UUID, actUpdatedTask.UUID)
assert.Equal(t, mockTask.UUID, touchedTask.UUID)
assert.Equal(t, mockTask.UUID, touchedTaskUUID)
assert.Equal(t, "testing", statusChangedtask.Activity)
assert.Equal(t, "testing", actUpdatedTask.Activity)
}
@ -101,12 +106,11 @@ func TestTaskUpdateFailed(t *testing.T) {
// Construct the task that's supposed to be updated.
taskID := "181eab68-1123-4790-93b1-94309a899411"
jobID := "e4719398-7cfa-4877-9bab-97c2d6c158b5"
mockJob := persistence.Job{UUID: jobID}
mockJob := persistence.Job{ID: 1234, UUID: jobID}
mockTask := persistence.Task{
UUID: taskID,
Worker: &worker,
WorkerID: ptr(uint(worker.ID)),
Job: &mockJob,
WorkerID: sql.NullInt64{Int64: worker.ID, Valid: true},
JobID: mockJob.ID,
Activity: "pre-update activity",
Type: "misc",
}
@ -121,20 +125,26 @@ func TestTaskUpdateFailed(t *testing.T) {
const numSubTests = 2
// Expect the task to be fetched for each sub-test:
mf.persistence.EXPECT().FetchTask(gomock.Any(), taskID).Return(&mockTask, nil).Times(numSubTests)
taskJobWorker := persistence.TaskJobWorker{
Task: mockTask,
JobUUID: jobID,
WorkerUUID: worker.UUID,
}
mf.persistence.EXPECT().FetchTask(gomock.Any(), taskID).Return(taskJobWorker, nil).Times(numSubTests)
// Expect a 'touch' of the task for each sub-test:
mf.persistence.EXPECT().TaskTouchedByWorker(gomock.Any(), &mockTask).Times(numSubTests)
mf.persistence.EXPECT().TaskTouchedByWorker(gomock.Any(), taskID).Times(numSubTests)
mf.persistence.EXPECT().WorkerSeen(gomock.Any(), &worker).Times(numSubTests)
// Mimick that this is always first failure of this worker/job/tasktype combo:
mf.persistence.EXPECT().CountTaskFailuresOfWorker(gomock.Any(), &mockJob, &worker, "misc").Return(0, nil).Times(numSubTests)
mf.persistence.EXPECT().CountTaskFailuresOfWorker(gomock.Any(), jobID, worker.ID, "misc").Return(0, nil).Times(numSubTests)
{
// Expect the Worker to be added to the list of failed workers.
// This returns 1, which is less than the failure threshold -> soft failure expected.
mf.persistence.EXPECT().AddWorkerToTaskFailedList(gomock.Any(), &mockTask, &worker).Return(1, nil)
mf.persistence.EXPECT().FetchJobByID(gomock.Any(), mockTask.JobID).Return(&mockJob, nil)
mf.persistence.EXPECT().WorkersLeftToRun(gomock.Any(), &mockJob, "misc").
Return(map[string]bool{"60453eec-5a26-43e9-9da2-d00506d492cc": true, "ce312357-29cd-4389-81ab-4d43e30945f8": true}, nil)
mf.persistence.EXPECT().FetchTaskFailureList(gomock.Any(), &mockTask).
@ -185,12 +195,11 @@ func TestBlockingAfterFailure(t *testing.T) {
// Construct the task that's supposed to be updated.
taskID := "181eab68-1123-4790-93b1-94309a899411"
jobID := "e4719398-7cfa-4877-9bab-97c2d6c158b5"
mockJob := persistence.Job{UUID: jobID}
mockJob := persistence.Job{ID: 1234, UUID: jobID}
mockTask := persistence.Task{
UUID: taskID,
Worker: &worker,
WorkerID: ptr(uint(worker.ID)),
Job: &mockJob,
WorkerID: sql.NullInt64{Int64: worker.ID, Valid: true},
JobID: mockJob.ID,
Activity: "pre-update activity",
Type: "misc",
}
@ -205,26 +214,32 @@ func TestBlockingAfterFailure(t *testing.T) {
const numSubTests = 3
// Expect the task to be fetched for each sub-test:
mf.persistence.EXPECT().FetchTask(gomock.Any(), taskID).Return(&mockTask, nil).Times(numSubTests)
taskJobWorker := persistence.TaskJobWorker{
Task: mockTask,
JobUUID: jobID,
WorkerUUID: worker.UUID,
}
mf.persistence.EXPECT().FetchTask(gomock.Any(), taskID).Return(taskJobWorker, nil).Times(numSubTests)
// Expect a 'touch' of the task for each sub-test:
mf.persistence.EXPECT().TaskTouchedByWorker(gomock.Any(), &mockTask).Times(numSubTests)
mf.persistence.EXPECT().TaskTouchedByWorker(gomock.Any(), taskID).Times(numSubTests)
mf.persistence.EXPECT().WorkerSeen(gomock.Any(), &worker).Times(numSubTests)
// Mimick that this is the 3rd of this worker/job/tasktype combo, and thus should trigger a block.
// Returns 2 because there have been 2 previous failures.
mf.persistence.EXPECT().
CountTaskFailuresOfWorker(gomock.Any(), &mockJob, &worker, "misc").
CountTaskFailuresOfWorker(gomock.Any(), jobID, worker.ID, "misc").
Return(2, nil).
Times(numSubTests)
// Expect the worker to be blocked.
mf.persistence.EXPECT().
AddWorkerToJobBlocklist(gomock.Any(), &mockJob, &worker, "misc").
AddWorkerToJobBlocklist(gomock.Any(), mockJob.ID, worker.ID, "misc").
Times(numSubTests)
{
// Mimick that there is another worker to work on this task, so the job should continue happily.
mf.persistence.EXPECT().FetchJobByID(gomock.Any(), mockTask.JobID).Return(&mockJob, nil).Times(2)
mf.persistence.EXPECT().WorkersLeftToRun(gomock.Any(), &mockJob, "misc").
Return(map[string]bool{"60453eec-5a26-43e9-9da2-d00506d492cc": true, "ce312357-29cd-4389-81ab-4d43e30945f8": true}, nil).Times(2)
mf.persistence.EXPECT().FetchTaskFailureList(gomock.Any(), &mockTask).
@ -242,7 +257,7 @@ func TestBlockingAfterFailure(t *testing.T) {
// Because the job didn't fail in its entirety, the tasks previously failed
// by the Worker should be requeued so they can be picked up by another.
mf.stateMachine.EXPECT().RequeueFailedTasksOfWorkerOfJob(
gomock.Any(), &worker, &mockJob,
gomock.Any(), &worker, jobID,
"worker дрон was blocked from tasks of type \"misc\"")
// Do the call.
@ -255,6 +270,7 @@ func TestBlockingAfterFailure(t *testing.T) {
{
// Test without any workers left to run these tasks on this job due to blocklisting. This should fail the entire job.
mf.persistence.EXPECT().FetchJobByID(gomock.Any(), mockTask.JobID).Return(&mockJob, nil)
mf.persistence.EXPECT().WorkersLeftToRun(gomock.Any(), &mockJob, "misc").
Return(map[string]bool{}, nil)
mf.persistence.EXPECT().FetchTaskFailureList(gomock.Any(), &mockTask).
@ -272,7 +288,7 @@ func TestBlockingAfterFailure(t *testing.T) {
// Expect failure of the job.
mf.stateMachine.EXPECT().
JobStatusChange(gomock.Any(), &mockJob, api.JobStatusFailed, "no more workers left to run tasks of type \"misc\"")
JobStatusChange(gomock.Any(), jobID, api.JobStatusFailed, "no more workers left to run tasks of type \"misc\"")
// Because the job failed, there is no need to re-queue any tasks previously failed by this worker.
@ -290,6 +306,7 @@ func TestBlockingAfterFailure(t *testing.T) {
theOtherFailingWorker := persistence.Worker{
UUID: "ce312357-29cd-4389-81ab-4d43e30945f8",
}
mf.persistence.EXPECT().FetchJobByID(gomock.Any(), mockTask.JobID).Return(&mockJob, nil)
mf.persistence.EXPECT().WorkersLeftToRun(gomock.Any(), &mockJob, "misc").
Return(map[string]bool{theOtherFailingWorker.UUID: true}, nil)
mf.persistence.EXPECT().FetchTaskFailureList(gomock.Any(), &mockTask).
@ -307,7 +324,7 @@ func TestBlockingAfterFailure(t *testing.T) {
// Expect failure of the job.
mf.stateMachine.EXPECT().
JobStatusChange(gomock.Any(), &mockJob, api.JobStatusFailed, "no more workers left to run tasks of type \"misc\"")
JobStatusChange(gomock.Any(), jobID, api.JobStatusFailed, "no more workers left to run tasks of type \"misc\"")
// Because the job failed, there is no need to re-queue any tasks previously failed by this worker.
@ -335,12 +352,11 @@ func TestJobFailureAfterWorkerTaskFailure(t *testing.T) {
// Construct the task that's supposed to be updated.
taskID := "181eab68-1123-4790-93b1-94309a899411"
jobID := "e4719398-7cfa-4877-9bab-97c2d6c158b5"
mockJob := persistence.Job{UUID: jobID}
mockJob := persistence.Job{ID: 1234, UUID: jobID}
mockTask := persistence.Task{
UUID: taskID,
Worker: &worker,
WorkerID: ptr(uint(worker.ID)),
Job: &mockJob,
WorkerID: sql.NullInt64{Int64: worker.ID, Valid: true},
JobID: mockJob.ID,
Activity: "pre-update activity",
Type: "misc",
}
@ -354,15 +370,21 @@ func TestJobFailureAfterWorkerTaskFailure(t *testing.T) {
mf.config.EXPECT().Get().Return(&conf).Times(2)
mf.persistence.EXPECT().FetchTask(gomock.Any(), taskID).Return(&mockTask, nil)
taskJobWorker := persistence.TaskJobWorker{
Task: mockTask,
JobUUID: jobID,
WorkerUUID: worker.UUID,
}
mf.persistence.EXPECT().FetchTask(gomock.Any(), taskID).Return(taskJobWorker, nil)
mf.persistence.EXPECT().TaskTouchedByWorker(gomock.Any(), &mockTask)
mf.persistence.EXPECT().TaskTouchedByWorker(gomock.Any(), taskID)
mf.persistence.EXPECT().WorkerSeen(gomock.Any(), &worker)
mf.persistence.EXPECT().CountTaskFailuresOfWorker(gomock.Any(), &mockJob, &worker, "misc").Return(0, nil)
mf.persistence.EXPECT().CountTaskFailuresOfWorker(gomock.Any(), jobID, worker.ID, "misc").Return(0, nil)
mf.persistence.EXPECT().AddWorkerToTaskFailedList(gomock.Any(), &mockTask, &worker).Return(1, nil)
mf.persistence.EXPECT().FetchJobByID(gomock.Any(), mockTask.JobID).Return(&mockJob, nil)
mf.persistence.EXPECT().WorkersLeftToRun(gomock.Any(), &mockJob, "misc").
Return(map[string]bool{"e7632d62-c3b8-4af0-9e78-01752928952c": true}, nil)
mf.persistence.EXPECT().FetchTaskFailureList(gomock.Any(), &mockTask).
@ -376,7 +398,7 @@ func TestJobFailureAfterWorkerTaskFailure(t *testing.T) {
// Expect failure of the job.
mf.stateMachine.EXPECT().
JobStatusChange(gomock.Any(), &mockJob, api.JobStatusFailed, "no more workers left to run tasks of type \"misc\"")
JobStatusChange(gomock.Any(), jobID, api.JobStatusFailed, "no more workers left to run tasks of type \"misc\"")
// Do the call
echoCtx := mf.prepareMockedJSONRequest(taskUpdate)

View File

@ -4,6 +4,7 @@ package api_impl
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
@ -12,6 +13,7 @@ import (
"github.com/labstack/echo/v4"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"projects.blender.org/studio/flamenco/internal/manager/eventbus"
"projects.blender.org/studio/flamenco/internal/manager/last_rendered"
@ -331,7 +333,7 @@ func (f *Flamenco) ScheduleTask(e echo.Context) error {
}
// Get a task to execute:
dbTask, err := f.persist.ScheduleTask(reqCtx, worker)
scheduledTask, err := f.persist.ScheduleTask(reqCtx, worker)
if err != nil {
if persistence.ErrIsDBBusy(err) {
logger.Warn().Msg("database busy scheduling task for worker")
@ -340,7 +342,7 @@ func (f *Flamenco) ScheduleTask(e echo.Context) error {
logger.Warn().Err(err).Msg("error scheduling task for worker")
return sendAPIError(e, http.StatusInternalServerError, "internal error finding a task for you: %v", err)
}
if dbTask == nil {
if scheduledTask == nil {
return e.NoContent(http.StatusNoContent)
}
@ -351,18 +353,18 @@ func (f *Flamenco) ScheduleTask(e echo.Context) error {
// Add a note to the task log about the worker assignment.
msg := fmt.Sprintf("Task assigned to worker %s (%s)", worker.Name, worker.UUID)
if err := f.logStorage.WriteTimestamped(logger, dbTask.Job.UUID, dbTask.UUID, msg); err != nil {
if err := f.logStorage.WriteTimestamped(logger, scheduledTask.JobUUID, scheduledTask.Task.UUID, msg); err != nil {
return sendAPIError(e, http.StatusInternalServerError, "internal error appending to task log: %v", err)
}
// Move the task to 'active' status so that it won't be assigned to another
// worker. This also enables the task timeout monitoring.
if err := f.stateMachine.TaskStatusChange(bgCtx, dbTask, api.TaskStatusActive); err != nil {
if err := f.stateMachine.TaskStatusChange(bgCtx, &scheduledTask.Task, api.TaskStatusActive); err != nil {
return sendAPIError(e, http.StatusInternalServerError, "internal error marking task as active: %v", err)
}
// Start timeout measurement as soon as the Worker gets the task assigned.
if err := f.workerPingedTask(logger, dbTask); err != nil {
if err := f.workerPingedTask(logger, scheduledTask.Task.UUID); err != nil {
return sendAPIError(e, http.StatusInternalServerError, "internal error updating task for timeout calculation: %v", err)
}
@ -371,23 +373,23 @@ func (f *Flamenco) ScheduleTask(e echo.Context) error {
f.broadcaster.BroadcastWorkerUpdate(update)
// Convert database objects to API objects:
apiCommands := []api.Command{}
for _, cmd := range dbTask.Commands {
apiCommands = append(apiCommands, api.Command{
Name: cmd.Name,
Parameters: cmd.Parameters,
})
}
apiTask := api.AssignedTask{
Uuid: dbTask.UUID,
Commands: apiCommands,
Job: dbTask.Job.UUID,
JobPriority: dbTask.Job.Priority,
JobType: dbTask.Job.JobType,
Name: dbTask.Name,
Priority: dbTask.Priority,
Status: api.TaskStatus(dbTask.Status),
TaskType: dbTask.Type,
Uuid: scheduledTask.Task.UUID,
Job: scheduledTask.JobUUID,
JobPriority: int(scheduledTask.JobPriority),
JobType: scheduledTask.JobType,
Name: scheduledTask.Task.Name,
Priority: int(scheduledTask.Task.Priority),
Status: api.TaskStatus(scheduledTask.Task.Status),
TaskType: scheduledTask.Task.Type,
}
if err := json.Unmarshal(scheduledTask.Task.Commands, &apiTask.Commands); err != nil {
log.Error().
Str("task", scheduledTask.Task.UUID).
AnErr("cause", err).
Msg("could not parse task commands JSON")
return sendAPIError(e, http.StatusInternalServerError, "internal error parsing task commands JSON: %v", err)
}
// Perform variable replacement before sending to the Worker.
@ -423,19 +425,17 @@ func (f *Flamenco) TaskOutputProduced(e echo.Context, taskID string) error {
}
// Fetch the task, to find its job UUID:
dbTask, err := f.persist.FetchTask(ctx, taskID)
taskJobWorker, err := f.persist.FetchTask(ctx, taskID)
switch {
case errors.Is(err, persistence.ErrTaskNotFound):
return e.JSON(http.StatusNotFound, "Task does not exist")
case err != nil:
logger.Error().Err(err).Msg("TaskOutputProduced: cannot fetch task")
return sendAPIError(e, http.StatusInternalServerError, "error fetching task")
case dbTask == nil:
panic("task could not be fetched, but database gave no error either")
}
// Include the job UUID in the logger.
jobUUID := dbTask.Job.UUID
jobUUID := taskJobWorker.JobUUID
logger = logger.With().Str("job", jobUUID).Logger()
// Read the image bytes into memory.
@ -459,7 +459,7 @@ func (f *Flamenco) TaskOutputProduced(e echo.Context, taskID string) error {
Callback: func(ctx context.Context) {
// Store this job as the last one to get a rendered image.
err := f.persist.SetLastRendered(ctx, dbTask.Job)
err := f.persist.SetLastRendered(ctx, taskJobWorker.JobUUID)
if err != nil {
logger.Error().Err(err).Msg("TaskOutputProduced: error marking this job as the last one to receive render output")
}
@ -497,12 +497,12 @@ func (f *Flamenco) TaskOutputProduced(e echo.Context, taskID string) error {
func (f *Flamenco) workerPingedTask(
logger zerolog.Logger,
task *persistence.Task,
taskUUID string,
) error {
bgCtx, bgCtxCancel := bgContext()
defer bgCtxCancel()
err := f.persist.TaskTouchedByWorker(bgCtx, task)
err := f.persist.TaskTouchedByWorker(bgCtx, taskUUID)
if err != nil {
logger.Error().Err(err).Msg("error marking task as 'touched' by worker")
return err
@ -549,7 +549,7 @@ func (f *Flamenco) MayWorkerRun(e echo.Context, taskID string) error {
// Fetch the task, to see if this worker is allowed to run it.
ctx := e.Request().Context()
dbTask, err := f.persist.FetchTask(ctx, taskID)
taskJobWorker, err := f.persist.FetchTask(ctx, taskID)
if err != nil {
if errors.Is(err, persistence.ErrTaskNotFound) {
mkr := api.MayKeepRunning{Reason: "Task not found"}
@ -558,16 +558,13 @@ func (f *Flamenco) MayWorkerRun(e echo.Context, taskID string) error {
logger.Error().Err(err).Msg("MayWorkerRun: cannot fetch task")
return sendAPIError(e, http.StatusInternalServerError, "error fetching task")
}
if dbTask == nil {
panic("task could not be fetched, but database gave no error either")
}
mkr := mayWorkerRun(worker, dbTask)
mkr := mayWorkerRun(worker, &taskJobWorker.Task)
// Errors saving the "worker pinged task" and "worker seen" fields in the
// database are just logged. It's not something to bother the worker with.
if mkr.MayKeepRunning {
_ = f.workerPingedTask(logger, dbTask)
_ = f.workerPingedTask(logger, taskJobWorker.Task.UUID)
}
_ = f.workerSeen(logger, worker)
@ -582,7 +579,7 @@ func mayWorkerRun(worker *persistence.Worker, dbTask *persistence.Task) api.MayK
StatusChangeRequested: true,
}
}
if dbTask.WorkerID == nil || *dbTask.WorkerID != uint(worker.ID) {
if !dbTask.WorkerID.Valid || dbTask.WorkerID.Int64 != worker.ID {
return api.MayKeepRunning{Reason: "task not assigned to this worker"}
}
if !task_state_machine.IsRunnableTaskStatus(dbTask.Status) {

View File

@ -5,6 +5,7 @@ package api_impl
import (
"bytes"
"context"
"database/sql"
"io"
"net/http"
"testing"
@ -32,22 +33,28 @@ func TestTaskScheduleHappy(t *testing.T) {
// Expect a call into the persistence layer, which should return a scheduled task.
job := persistence.Job{
ID: 1234,
UUID: "583a7d59-887a-4c6c-b3e4-a753018f71b0",
Priority: 47,
JobType: "simple-blender-test",
}
task := persistence.Task{
UUID: "4107c7aa-e86d-4244-858b-6c4fce2af503",
Job: &job,
Commands: []persistence.Command{
{Name: "test", Parameters: map[string]interface{}{
"param": "prefix-{variable}-suffix",
}},
},
JobID: job.ID,
Priority: 327,
Commands: []byte(`[{"name": "test", "parameters": {"param": "prefix-{variable}-suffix"}}]`),
}
scheduledTask := persistence.ScheduledTask{
Task: task,
JobUUID: job.UUID,
JobPriority: job.Priority,
JobType: job.JobType,
}
ctx := echo.Request().Context()
bgCtx := gomock.Not(ctx)
mf.persistence.EXPECT().ScheduleTask(ctx, &worker).Return(&task, nil)
mf.persistence.EXPECT().TaskTouchedByWorker(bgCtx, &task)
mf.persistence.EXPECT().ScheduleTask(ctx, &worker).Return(&scheduledTask, nil)
mf.persistence.EXPECT().TaskTouchedByWorker(bgCtx, task.UUID)
mf.persistence.EXPECT().WorkerSeen(bgCtx, &worker)
mf.expectExpandVariables(t,
config.VariableAudienceWorkers,
@ -68,6 +75,9 @@ func TestTaskScheduleHappy(t *testing.T) {
assignedTask := api.AssignedTask{
Uuid: task.UUID,
Job: job.UUID,
JobType: "simple-blender-test",
JobPriority: 47,
Priority: 327,
Commands: []api.Command{
{Name: "test", Parameters: map[string]interface{}{
"param": "prefix-value-suffix",
@ -493,17 +503,16 @@ func TestMayWorkerRun(t *testing.T) {
}
job := persistence.Job{
ID: 1234,
UUID: "583a7d59-887a-4c6c-b3e4-a753018f71b0",
}
task := persistence.Task{
UUID: "4107c7aa-e86d-4244-858b-6c4fce2af503",
Job: &job,
JobID: job.ID,
Status: api.TaskStatusActive,
}
mf.persistence.EXPECT().FetchTask(gomock.Any(), task.UUID).Return(&task, nil).AnyTimes()
// Expect the worker to be marked as 'seen' regardless of whether it may run
// its current task or not, so equal to the number of calls to
// `MayWorkerRun()` below.
@ -511,6 +520,13 @@ func TestMayWorkerRun(t *testing.T) {
// Test: unhappy, task unassigned
{
taskJobWorker := persistence.TaskJobWorker{
Task: task,
JobUUID: job.UUID,
WorkerUUID: "",
}
mf.persistence.EXPECT().FetchTask(gomock.Any(), task.UUID).Return(taskJobWorker, nil)
echo := prepareRequest()
err := mf.flamenco.MayWorkerRun(echo, task.UUID)
require.NoError(t, err)
@ -522,11 +538,18 @@ func TestMayWorkerRun(t *testing.T) {
// Test: happy, task assigned to this worker.
{
task.WorkerID = sql.NullInt64{Int64: worker.ID, Valid: true}
taskJobWorker := persistence.TaskJobWorker{
Task: task,
JobUUID: job.UUID,
WorkerUUID: worker.UUID,
}
mf.persistence.EXPECT().FetchTask(gomock.Any(), task.UUID).Return(taskJobWorker, nil)
// Expect a 'touch' of the task.
mf.persistence.EXPECT().TaskTouchedByWorker(gomock.Any(), &task).Return(nil)
mf.persistence.EXPECT().TaskTouchedByWorker(gomock.Any(), task.UUID).Return(nil)
echo := prepareRequest()
task.WorkerID = ptr(uint(worker.ID))
err := mf.flamenco.MayWorkerRun(echo, task.UUID)
require.NoError(t, err)
assertResponseJSON(t, echo, http.StatusOK, api.MayKeepRunning{
@ -536,9 +559,16 @@ func TestMayWorkerRun(t *testing.T) {
// Test: unhappy, assigned but cancelled.
{
echo := prepareRequest()
task.WorkerID = ptr(uint(worker.ID))
task.WorkerID = sql.NullInt64{Int64: worker.ID, Valid: true}
task.Status = api.TaskStatusCanceled
taskJobWorker := persistence.TaskJobWorker{
Task: task,
JobUUID: job.UUID,
WorkerUUID: worker.UUID,
}
mf.persistence.EXPECT().FetchTask(gomock.Any(), task.UUID).Return(taskJobWorker, nil)
echo := prepareRequest()
err := mf.flamenco.MayWorkerRun(echo, task.UUID)
require.NoError(t, err)
assertResponseJSON(t, echo, http.StatusOK, api.MayKeepRunning{
@ -550,9 +580,16 @@ func TestMayWorkerRun(t *testing.T) {
// Test: unhappy, assigned and runnable but worker should go to bed.
{
worker.StatusChangeRequest(api.WorkerStatusAsleep, false)
echo := prepareRequest()
task.WorkerID = ptr(uint(worker.ID))
task.WorkerID = sql.NullInt64{Int64: worker.ID, Valid: true}
task.Status = api.TaskStatusActive
taskJobWorker := persistence.TaskJobWorker{
Task: task,
JobUUID: job.UUID,
WorkerUUID: worker.UUID,
}
mf.persistence.EXPECT().FetchTask(gomock.Any(), task.UUID).Return(taskJobWorker, nil)
echo := prepareRequest()
err := mf.flamenco.MayWorkerRun(echo, task.UUID)
require.NoError(t, err)
assertResponseJSON(t, echo, http.StatusOK, api.MayKeepRunning{
@ -564,13 +601,20 @@ func TestMayWorkerRun(t *testing.T) {
// Test: happy, assigned and runnable; worker should go to bed after task is finished.
{
// Expect a 'touch' of the task.
mf.persistence.EXPECT().TaskTouchedByWorker(gomock.Any(), &task).Return(nil)
worker.StatusChangeRequest(api.WorkerStatusAsleep, true)
echo := prepareRequest()
task.WorkerID = ptr(uint(worker.ID))
task.WorkerID = sql.NullInt64{Int64: worker.ID, Valid: true}
task.Status = api.TaskStatusActive
taskJobWorker := persistence.TaskJobWorker{
Task: task,
JobUUID: job.UUID,
WorkerUUID: worker.UUID,
}
mf.persistence.EXPECT().FetchTask(gomock.Any(), task.UUID).Return(taskJobWorker, nil)
// Expect a 'touch' of the task.
mf.persistence.EXPECT().TaskTouchedByWorker(gomock.Any(), task.UUID).Return(nil)
echo := prepareRequest()
err := mf.flamenco.MayWorkerRun(echo, task.UUID)
require.NoError(t, err)
assertResponseJSON(t, echo, http.StatusOK, api.MayKeepRunning{
@ -593,13 +637,19 @@ func TestTaskOutputProduced(t *testing.T) {
}
job := persistence.Job{
ID: 1234,
UUID: "583a7d59-887a-4c6c-b3e4-a753018f71b0",
}
task := persistence.Task{
UUID: "4107c7aa-e86d-4244-858b-6c4fce2af503",
Job: &job,
JobID: job.ID,
Status: api.TaskStatusActive,
}
taskJobWorker := persistence.TaskJobWorker{
Task: task,
JobUUID: job.UUID,
WorkerUUID: worker.UUID,
}
// Mock body to use in the request.
bodyBytes := []byte("JPEG file contents")
@ -640,7 +690,7 @@ func TestTaskOutputProduced(t *testing.T) {
// Test: unhappy, wrong mime type
{
mf.persistence.EXPECT().WorkerSeen(gomock.Any(), &worker)
mf.persistence.EXPECT().FetchTask(gomock.Any(), task.UUID).Return(&task, nil)
mf.persistence.EXPECT().FetchTask(gomock.Any(), task.UUID).Return(taskJobWorker, nil)
echo := prepareRequest(bytes.NewReader(bodyBytes))
echo.Request().Header.Set("Content-Type", "image/openexr")
@ -654,7 +704,7 @@ func TestTaskOutputProduced(t *testing.T) {
// Test: unhappy, queue full
{
mf.persistence.EXPECT().WorkerSeen(gomock.Any(), &worker)
mf.persistence.EXPECT().FetchTask(gomock.Any(), task.UUID).Return(&task, nil)
mf.persistence.EXPECT().FetchTask(gomock.Any(), task.UUID).Return(taskJobWorker, nil)
echo := prepareRequest(bytes.NewReader(bodyBytes))
mf.lastRender.EXPECT().QueueImage(gomock.Any()).Return(last_rendered.ErrQueueFull)
@ -667,7 +717,7 @@ func TestTaskOutputProduced(t *testing.T) {
// Test: happy
{
mf.persistence.EXPECT().WorkerSeen(gomock.Any(), &worker)
mf.persistence.EXPECT().FetchTask(gomock.Any(), task.UUID).Return(&task, nil)
mf.persistence.EXPECT().FetchTask(gomock.Any(), task.UUID).Return(taskJobWorker, nil)
// Don't expect persistence.SetLastRendered(...) quite yet. That should be
// called after the image processing is done.
@ -691,7 +741,7 @@ func TestTaskOutputProduced(t *testing.T) {
if assert.NotNil(t, actualPayload) {
ctx := context.Background()
mf.persistence.EXPECT().SetLastRendered(ctx, &job)
mf.persistence.EXPECT().SetLastRendered(ctx, job.UUID)
expectBroadcast := api.EventLastRenderedUpdate{
JobId: job.UUID,

View File

@ -17,10 +17,10 @@ func NewJobUpdate(job *persistence.Job) api.EventJobUpdate {
jobUpdate := api.EventJobUpdate{
Id: job.UUID,
Name: &job.Name,
Updated: job.UpdatedAt,
Updated: job.UpdatedAt.Time,
Status: job.Status,
Type: job.JobType,
Priority: job.Priority,
Priority: int(job.Priority),
}
if job.DeleteRequestedAt.Valid {
@ -34,14 +34,12 @@ func NewJobUpdate(job *persistence.Job) api.EventJobUpdate {
// fills in the fields that represent the current state of the task. For
// example, it omits `PreviousStatus`. The omitted fields can be filled in by
// the caller.
//
// Assumes task.Job is not nil.
func NewTaskUpdate(task *persistence.Task) api.EventTaskUpdate {
func NewTaskUpdate(task persistence.Task, jobUUID string) api.EventTaskUpdate {
taskUpdate := api.EventTaskUpdate{
Id: task.UUID,
JobId: task.Job.UUID,
JobId: jobUUID,
Name: task.Name,
Updated: task.UpdatedAt,
Updated: task.UpdatedAt.Time,
Status: task.Status,
Activity: task.Activity,
}

View File

@ -250,7 +250,7 @@ func (s *Service) canDeleteShamanCheckout(logger zerolog.Logger, job *persistenc
return false
}
checkoutID := job.Storage.ShamanCheckoutID
checkoutID := job.StorageShamanCheckoutID
if checkoutID == "" {
logger.Debug().Msg("job deleter: job was not created with Shaman (or before Flamenco v3.2), cannot delete job files")
return false

View File

@ -10,7 +10,7 @@ import (
time "time"
gomock "github.com/golang/mock/gomock"
persistence "projects.blender.org/studio/flamenco/internal/manager/persistence"
sqlc "projects.blender.org/studio/flamenco/internal/manager/persistence/sqlc"
api "projects.blender.org/studio/flamenco/pkg/api"
)
@ -52,10 +52,10 @@ func (mr *MockPersistenceServiceMockRecorder) DeleteJob(arg0, arg1 interface{})
}
// FetchJob mocks base method.
func (m *MockPersistenceService) FetchJob(arg0 context.Context, arg1 string) (*persistence.Job, error) {
func (m *MockPersistenceService) FetchJob(arg0 context.Context, arg1 string) (*sqlc.Job, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchJob", arg0, arg1)
ret0, _ := ret[0].(*persistence.Job)
ret0, _ := ret[0].(*sqlc.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -109,7 +109,7 @@ func (mr *MockPersistenceServiceMockRecorder) RequestIntegrityCheck() *gomock.Ca
}
// RequestJobDeletion mocks base method.
func (m *MockPersistenceService) RequestJobDeletion(arg0 context.Context, arg1 *persistence.Job) error {
func (m *MockPersistenceService) RequestJobDeletion(arg0 context.Context, arg1 *sqlc.Job) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RequestJobDeletion", arg0, arg1)
ret0, _ := ret[0].(error)

View File

@ -19,69 +19,27 @@ import (
"projects.blender.org/studio/flamenco/pkg/api"
)
type Job struct {
Model
UUID string
type Job = sqlc.Job
type Task = sqlc.Task
Name string
JobType string
Priority int
Status api.JobStatus
Activity string
// TaskJobWorker represents a task, with identifieres for its job and the worker it's assigned to.
type TaskJobWorker struct {
Task Task
JobUUID string
WorkerUUID string
}
Settings StringInterfaceMap
Metadata StringStringMap
DeleteRequestedAt sql.NullTime
Storage JobStorageInfo
WorkerTagID *uint
WorkerTag *WorkerTag
// TaskJob represents a task, with identifier for its job.
type TaskJob struct {
Task Task
JobUUID string
IsActive bool // Whether the worker assigned to this task is actually working on it.
}
type StringInterfaceMap map[string]interface{}
type StringStringMap map[string]string
// DeleteRequested returns whether deletion of this job was requested.
func (j *Job) DeleteRequested() bool {
return j.DeleteRequestedAt.Valid
}
// JobStorageInfo contains info about where the job files are stored. It is
// intended to be used when removing a job, which may include the removal of its
// files.
type JobStorageInfo struct {
// ShamanCheckoutID is only set when the job was actually using Shaman storage.
ShamanCheckoutID string
}
type Task struct {
Model
UUID string
Name string
Type string
JobID uint
Job *Job
JobUUID string // Fetched by SQLC, handled by GORM in Task.AfterFind()
IndexInJob int
Priority int
Status api.TaskStatus
// Which worker is/was working on this.
WorkerID *uint
Worker *Worker
WorkerUUID string // Fetched by SQLC, handled by GORM in Task.AfterFind()
LastTouchedAt time.Time // Should contain UTC timestamps.
// Dependencies are tasks that need to be completed before this one can run.
Dependencies []*Task
Commands Commands
Activity string
}
// Commands is the schema used for (un)marshalling sqlc.Task.Commands.
type Commands []Command
type Command struct {
@ -123,15 +81,7 @@ func (js *StringStringMap) Scan(value interface{}) error {
}
// TaskFailure keeps track of which Worker failed which Task.
type TaskFailure struct {
// Don't include the standard Gorm ID, UpdatedAt, or DeletedAt fields, as they're useless here.
// Entries will never be updated, and should never be soft-deleted but just purged from existence.
CreatedAt time.Time
TaskID uint
Task *Task
WorkerID uint
Worker *Worker
}
type TaskFailure = sqlc.TaskFailure
// StoreJob stores an AuthoredJob and its tasks, and saves it to the database.
// The job will be in 'under construction' status. It is up to the caller to transition it to its desired initial status.
@ -334,7 +284,7 @@ func (db *DB) storeAuthoredJobTaks(
func (db *DB) FetchJob(ctx context.Context, jobUUID string) (*Job, error) {
queries := db.queries()
sqlcJob, err := queries.FetchJob(ctx, jobUUID)
job, err := queries.FetchJob(ctx, jobUUID)
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, ErrJobNotFound
@ -342,23 +292,22 @@ func (db *DB) FetchJob(ctx context.Context, jobUUID string) (*Job, error) {
return nil, jobError(err, "fetching job")
}
gormJob, err := convertSqlcJob(sqlcJob)
if err != nil {
return nil, err
}
return &job, nil
}
if sqlcJob.WorkerTagID.Valid {
workerTag, err := fetchWorkerTagByID(ctx, queries, sqlcJob.WorkerTagID.Int64)
// FetchJob fetches a single job by its database ID, without fetching its tasks.
func (db *DB) FetchJobByID(ctx context.Context, jobID int64) (*Job, error) {
queries := db.queries()
job, err := queries.FetchJobByID(ctx, jobID)
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, ErrWorkerTagNotFound
return nil, ErrJobNotFound
case err != nil:
return nil, workerTagError(err, "fetching worker tag of job")
}
gormJob.WorkerTag = &workerTag
return nil, jobError(err, "fetching job")
}
return &gormJob, nil
return &job, nil
}
func (db *DB) FetchJobs(ctx context.Context) ([]*Job, error) {
@ -369,28 +318,13 @@ func (db *DB) FetchJobs(ctx context.Context) ([]*Job, error) {
return nil, jobError(err, "fetching all jobs")
}
gormJobs := make([]*Job, len(sqlcJobs))
for index, sqlcJob := range sqlcJobs {
gormJob, err := convertSqlcJob(sqlcJob)
if err != nil {
return nil, err
// TODO: just return []Job instead of converting the array.
jobPointers := make([]*Job, len(sqlcJobs))
for index := range sqlcJobs {
jobPointers[index] = &sqlcJobs[index]
}
if sqlcJob.WorkerTagID.Valid {
workerTag, err := fetchWorkerTagByID(ctx, queries, sqlcJob.WorkerTagID.Int64)
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, ErrWorkerTagNotFound
case err != nil:
return nil, workerTagError(err, "fetching worker tag of job")
}
gormJob.WorkerTag = &workerTag
}
gormJobs[index] = &gormJob
}
return gormJobs, nil
return jobPointers, nil
}
// FetchJobShamanCheckoutID fetches the job's Shaman Checkout ID.
@ -492,21 +426,18 @@ func (db *DB) FetchJobsDeletionRequested(ctx context.Context) ([]string, error)
func (db *DB) FetchJobsInStatus(ctx context.Context, jobStatuses ...api.JobStatus) ([]*Job, error) {
queries := db.queries()
sqlcJobs, err := queries.FetchJobsInStatus(ctx, jobStatuses)
jobs, err := queries.FetchJobsInStatus(ctx, jobStatuses)
if err != nil {
return nil, jobError(err, "fetching jobs in status %q", jobStatuses)
}
var jobs []*Job
for index := range sqlcJobs {
job, err := convertSqlcJob(sqlcJobs[index])
if err != nil {
return nil, jobError(err, "converting fetched jobs in status %q", jobStatuses)
}
jobs = append(jobs, &job)
// TODO: just return []Job instead of converting the array.
pointers := make([]*Job, len(jobs))
for index := range jobs {
pointers[index] = &jobs[index]
}
return jobs, nil
return pointers, nil
}
// SaveJobStatus saves the job's Status and Activity fields.
@ -552,7 +483,7 @@ func (db *DB) SaveJobStorageInfo(ctx context.Context, j *Job) error {
params := sqlc.SaveJobStorageInfoParams{
ID: int64(j.ID),
StorageShamanCheckoutID: j.Storage.ShamanCheckoutID,
StorageShamanCheckoutID: j.StorageShamanCheckoutID,
}
err := queries.SaveJobStorageInfo(ctx, params)
@ -562,68 +493,21 @@ func (db *DB) SaveJobStorageInfo(ctx context.Context, j *Job) error {
return nil
}
func (db *DB) FetchTask(ctx context.Context, taskUUID string) (*Task, error) {
func (db *DB) FetchTask(ctx context.Context, taskUUID string) (TaskJobWorker, error) {
queries := db.queries()
taskRow, err := queries.FetchTask(ctx, taskUUID)
if err != nil {
return nil, taskError(err, "fetching task %s", taskUUID)
return TaskJobWorker{}, taskError(err, "fetching task %s", taskUUID)
}
return convertSqlTaskWithJobAndWorker(ctx, queries, taskRow.Task)
}
// TODO: remove this code, and let the code that calls into the persistence
// service fetch the job/worker explicitly when needed.
func convertSqlTaskWithJobAndWorker(
ctx context.Context,
queries *sqlc.Queries,
task sqlc.Task,
) (*Task, error) {
var (
gormJob Job
worker Worker
err error
)
// Fetch & convert the Job.
if task.JobID > 0 {
sqlcJob, err := queries.FetchJobByID(ctx, task.JobID)
if err != nil {
return nil, jobError(err, "fetching job of task %s", task.UUID)
taskJobWorker := TaskJobWorker{
Task: taskRow.Task,
JobUUID: taskRow.JobUUID.String,
WorkerUUID: taskRow.WorkerUUID.String,
}
gormJob, err = convertSqlcJob(sqlcJob)
if err != nil {
return nil, jobError(err, "converting job of task %s", task.UUID)
}
}
// Fetch the Worker.
if task.WorkerID.Valid && task.WorkerID.Int64 > 0 {
worker, err = queries.FetchWorkerUnconditionalByID(ctx, task.WorkerID.Int64)
if err != nil {
return nil, taskError(err, "fetching worker assigned to task %s", task.UUID)
}
}
// Convert the Task.
gormTask, err := convertSqlcTask(task, gormJob.UUID, worker.UUID)
if err != nil {
return nil, err
}
// Put the Job & Worker into the Task.
if gormJob.ID > 0 {
gormTask.Job = &gormJob
gormTask.JobUUID = gormJob.UUID
}
if worker.ID > 0 {
gormTask.Worker = &worker
gormTask.WorkerUUID = worker.UUID
}
return gormTask, nil
return taskJobWorker, nil
}
// FetchTaskJobUUID fetches the job UUID of the given task.
@ -658,29 +542,13 @@ func (db *DB) SaveTask(ctx context.Context, t *Task) error {
UpdatedAt: db.nowNullable(),
Name: t.Name,
Type: t.Type,
Priority: int64(t.Priority),
Priority: t.Priority,
Status: t.Status,
Commands: commandsJSON,
Activity: t.Activity,
ID: int64(t.ID),
}
if t.WorkerID != nil {
param.WorkerID = sql.NullInt64{
Int64: int64(*t.WorkerID),
Valid: true,
}
} else if t.Worker != nil && t.Worker.ID > 0 {
param.WorkerID = sql.NullInt64{
Int64: int64(t.Worker.ID),
Valid: true,
}
}
if !t.LastTouchedAt.IsZero() {
param.LastTouchedAt = sql.NullTime{
Time: t.LastTouchedAt,
Valid: true,
}
ID: t.ID,
WorkerID: t.WorkerID,
LastTouchedAt: t.LastTouchedAt,
}
err = queries.UpdateTask(ctx, param)
@ -724,26 +592,20 @@ func (db *DB) SaveTaskActivity(ctx context.Context, t *Task) error {
func (db *DB) TaskAssignToWorker(ctx context.Context, t *Task, w *Worker) error {
queries := db.queries()
t.WorkerID = sql.NullInt64{Int64: w.ID, Valid: true}
err := queries.TaskAssignToWorker(ctx, sqlc.TaskAssignToWorkerParams{
UpdatedAt: db.nowNullable(),
WorkerID: sql.NullInt64{
Int64: int64(w.ID),
Valid: true,
},
ID: int64(t.ID),
WorkerID: t.WorkerID,
ID: t.ID,
})
if err != nil {
return taskError(err, "assigning task %s to worker %s", t.UUID, w.UUID)
}
// Update the task itself.
t.Worker = w
t.WorkerID = ptr(uint(w.ID))
return nil
}
func (db *DB) FetchTasksOfWorkerInStatus(ctx context.Context, worker *Worker, taskStatus api.TaskStatus) ([]*Task, error) {
func (db *DB) FetchTasksOfWorkerInStatus(ctx context.Context, worker *Worker, taskStatus api.TaskStatus) ([]TaskJob, error) {
queries := db.queries()
rows, err := queries.FetchTasksOfWorkerInStatus(ctx, sqlc.FetchTasksOfWorkerInStatusParams{
@ -757,38 +619,15 @@ func (db *DB) FetchTasksOfWorkerInStatus(ctx context.Context, worker *Worker, ta
return nil, taskError(err, "finding tasks of worker %s in status %q", worker.UUID, taskStatus)
}
jobCache := make(map[uint]*Job)
result := make([]*Task, len(rows))
result := make([]TaskJob, len(rows))
for i := range rows {
jobUUID := rows[i].JobUUID.String
gormTask, err := convertSqlcTask(rows[i].Task, jobUUID, worker.UUID)
if err != nil {
return nil, err
}
gormTask.Worker = worker
gormTask.WorkerID = ptr(uint(worker.ID))
// Fetch the job, either from the cache or from the database. This is done
// here because the task_state_machine functionality expects that task.Job
// is set.
// TODO: make that code fetch the job details it needs, rather than fetching
// the entire job here.
job := jobCache[gormTask.JobID]
if job == nil {
job, err = db.FetchJob(ctx, jobUUID)
if err != nil {
return nil, jobError(err, "finding job %s of task %s", jobUUID, gormTask.UUID)
}
}
gormTask.Job = job
result[i] = gormTask
result[i].Task = rows[i].Task
result[i].JobUUID = rows[i].JobUUID
}
return result, nil
}
func (db *DB) FetchTasksOfWorkerInStatusOfJob(ctx context.Context, worker *Worker, taskStatus api.TaskStatus, job *Job) ([]*Task, error) {
func (db *DB) FetchTasksOfWorkerInStatusOfJob(ctx context.Context, worker *Worker, taskStatus api.TaskStatus, jobUUID string) ([]*Task, error) {
queries := db.queries()
rows, err := queries.FetchTasksOfWorkerInStatusOfJob(ctx, sqlc.FetchTasksOfWorkerInStatusOfJobParams{
@ -796,24 +635,17 @@ func (db *DB) FetchTasksOfWorkerInStatusOfJob(ctx context.Context, worker *Worke
Int64: int64(worker.ID),
Valid: true,
},
JobID: int64(job.ID),
JobUUID: jobUUID,
TaskStatus: taskStatus,
})
if err != nil {
return nil, taskError(err, "finding tasks of worker %s in status %q and job %s", worker.UUID, taskStatus, job.UUID)
return nil, taskError(err, "finding tasks of worker %s in status %q and job %s", worker.UUID, taskStatus, jobUUID)
}
// TODO: just return []Task instead of creating an array of pointers.
result := make([]*Task, len(rows))
for i := range rows {
gormTask, err := convertSqlcTask(rows[i].Task, job.UUID, worker.UUID)
if err != nil {
return nil, err
}
gormTask.Job = job
gormTask.JobID = job.ID
gormTask.Worker = worker
gormTask.WorkerID = ptr(uint(worker.ID))
result[i] = gormTask
result[i] = &rows[i].Task
}
return result, nil
}
@ -865,7 +697,7 @@ func (db *DB) CountTasksOfJobInStatus(
}
// FetchTaskIDsOfJob returns all tasks of the given job.
func (db *DB) FetchTasksOfJob(ctx context.Context, job *Job) ([]*Task, error) {
func (db *DB) FetchTasksOfJob(ctx context.Context, job *Job) ([]TaskJobWorker, error) {
queries := db.queries()
rows, err := queries.FetchTasksOfJob(ctx, int64(job.ID))
@ -873,20 +705,17 @@ func (db *DB) FetchTasksOfJob(ctx context.Context, job *Job) ([]*Task, error) {
return nil, taskError(err, "fetching tasks of job %s", job.UUID)
}
result := make([]*Task, len(rows))
result := make([]TaskJobWorker, len(rows))
for i := range rows {
gormTask, err := convertSqlcTask(rows[i].Task, job.UUID, rows[i].WorkerUUID.String)
if err != nil {
return nil, err
}
gormTask.Job = job
result[i] = gormTask
result[i].Task = rows[i].Task
result[i].JobUUID = job.UUID
result[i].WorkerUUID = rows[i].WorkerUUID.String
}
return result, nil
}
// FetchTasksOfJobInStatus returns those tasks of the given job that have any of the given statuses.
func (db *DB) FetchTasksOfJobInStatus(ctx context.Context, job *Job, taskStatuses ...api.TaskStatus) ([]*Task, error) {
func (db *DB) FetchTasksOfJobInStatus(ctx context.Context, job *Job, taskStatuses ...api.TaskStatus) ([]TaskJobWorker, error) {
queries := db.queries()
rows, err := queries.FetchTasksOfJobInStatus(ctx, sqlc.FetchTasksOfJobInStatusParams{
@ -897,14 +726,11 @@ func (db *DB) FetchTasksOfJobInStatus(ctx context.Context, job *Job, taskStatuse
return nil, taskError(err, "fetching tasks of job %s in status %q", job.UUID, taskStatuses)
}
result := make([]*Task, len(rows))
result := make([]TaskJobWorker, len(rows))
for i := range rows {
gormTask, err := convertSqlcTask(rows[i].Task, job.UUID, rows[i].WorkerUUID.String)
if err != nil {
return nil, err
}
gormTask.Job = job
result[i] = gormTask
result[i].Task = rows[i].Task
result[i].JobUUID = job.UUID
result[i].WorkerUUID = rows[i].WorkerUUID.String
}
return result, nil
}
@ -958,22 +784,18 @@ func (db *DB) UpdateJobsTaskStatusesConditional(ctx context.Context, job *Job,
}
// TaskTouchedByWorker marks the task as 'touched' by a worker. This is used for timeout detection.
func (db *DB) TaskTouchedByWorker(ctx context.Context, t *Task) error {
func (db *DB) TaskTouchedByWorker(ctx context.Context, taskUUID string) error {
queries := db.queries()
now := db.nowNullable()
err := queries.TaskTouchedByWorker(ctx, sqlc.TaskTouchedByWorkerParams{
UpdatedAt: now,
LastTouchedAt: now,
ID: int64(t.ID),
UUID: taskUUID,
})
if err != nil {
return taskError(err, "saving task 'last touched at'")
}
// Also update the given task, so that it's consistent with the database.
t.LastTouchedAt = now.Time
return nil
}
@ -1040,82 +862,3 @@ func (db *DB) FetchTaskFailureList(ctx context.Context, t *Task) ([]*Worker, err
}
return workers, nil
}
// convertSqlcJob converts a job from the SQLC-generated model to the model
// expected by the rest of the code. This is mostly in place to aid in the GORM
// to SQLC migration. It is intended that eventually the rest of the code will
// use the same SQLC-generated model.
func convertSqlcJob(job sqlc.Job) (Job, error) {
dbJob := Job{
Model: Model{
ID: uint(job.ID),
CreatedAt: job.CreatedAt,
UpdatedAt: job.UpdatedAt.Time,
},
UUID: job.UUID,
Name: job.Name,
JobType: job.JobType,
Priority: int(job.Priority),
Status: api.JobStatus(job.Status),
Activity: job.Activity,
DeleteRequestedAt: job.DeleteRequestedAt,
Storage: JobStorageInfo{
ShamanCheckoutID: job.StorageShamanCheckoutID,
},
}
if err := json.Unmarshal(job.Settings, &dbJob.Settings); err != nil {
return Job{}, jobError(err, fmt.Sprintf("job %s has invalid settings: %v", job.UUID, err))
}
if err := json.Unmarshal(job.Metadata, &dbJob.Metadata); err != nil {
return Job{}, jobError(err, fmt.Sprintf("job %s has invalid metadata: %v", job.UUID, err))
}
if job.WorkerTagID.Valid {
workerTagID := uint(job.WorkerTagID.Int64)
dbJob.WorkerTagID = &workerTagID
}
return dbJob, nil
}
// convertSqlcTask converts a FetchTaskRow from the SQLC-generated model to the
// model expected by the rest of the code. This is mostly in place to aid in the
// GORM to SQLC migration. It is intended that eventually the rest of the code
// will use the same SQLC-generated model.
func convertSqlcTask(task sqlc.Task, jobUUID string, workerUUID string) (*Task, error) {
dbTask := Task{
Model: Model{
ID: uint(task.ID),
CreatedAt: task.CreatedAt,
UpdatedAt: task.UpdatedAt.Time,
},
UUID: task.UUID,
Name: task.Name,
Type: task.Type,
IndexInJob: int(task.IndexInJob),
Priority: int(task.Priority),
Status: api.TaskStatus(task.Status),
LastTouchedAt: task.LastTouchedAt.Time,
Activity: task.Activity,
JobID: uint(task.JobID),
JobUUID: jobUUID,
WorkerUUID: workerUUID,
}
// TODO: convert dependencies?
if task.WorkerID.Valid {
workerID := uint(task.WorkerID.Int64)
dbTask.WorkerID = &workerID
}
if err := json.Unmarshal(task.Commands, &dbTask.Commands); err != nil {
return nil, taskError(err, "task %s of job %s has invalid commands: %v", task.UUID, jobUUID, err)
}
return &dbTask, nil
}

View File

@ -13,11 +13,11 @@ import (
type JobBlockListEntry = sqlc.FetchJobBlocklistRow
// AddWorkerToJobBlocklist prevents this Worker of getting any task, of this type, on this job, from the task scheduler.
func (db *DB) AddWorkerToJobBlocklist(ctx context.Context, job *Job, worker *Worker, taskType string) error {
if job.ID == 0 {
func (db *DB) AddWorkerToJobBlocklist(ctx context.Context, jobID int64, workerID int64, taskType string) error {
if jobID == 0 {
panic("Cannot add worker to job blocklist with zero job ID")
}
if worker.ID == 0 {
if workerID == 0 {
panic("Cannot add worker to job blocklist with zero worker ID")
}
if taskType == "" {
@ -28,8 +28,8 @@ func (db *DB) AddWorkerToJobBlocklist(ctx context.Context, job *Job, worker *Wor
return queries.AddWorkerToJobBlocklist(ctx, sqlc.AddWorkerToJobBlocklistParams{
CreatedAt: db.nowNullable().Time,
JobID: int64(job.ID),
WorkerID: int64(worker.ID),
JobID: jobID,
WorkerID: workerID,
TaskType: taskType,
})
}
@ -72,17 +72,17 @@ func (db *DB) WorkersLeftToRun(ctx context.Context, job *Job, taskType string) (
workerUUIDs []string
err error
)
if job.WorkerTagID == nil {
workerUUIDs, err = queries.WorkersLeftToRun(ctx, sqlc.WorkersLeftToRunParams{
JobID: int64(job.ID),
TaskType: taskType,
})
} else {
if job.WorkerTagID.Valid {
workerUUIDs, err = queries.WorkersLeftToRunWithWorkerTag(ctx,
sqlc.WorkersLeftToRunWithWorkerTagParams{
JobID: int64(job.ID),
JobID: job.ID,
TaskType: taskType,
WorkerTagID: job.WorkerTagID.Int64,
})
} else {
workerUUIDs, err = queries.WorkersLeftToRun(ctx, sqlc.WorkersLeftToRunParams{
JobID: job.ID,
TaskType: taskType,
WorkerTagID: int64(*job.WorkerTagID),
})
}
if err != nil {
@ -99,13 +99,13 @@ func (db *DB) WorkersLeftToRun(ctx context.Context, job *Job, taskType string) (
}
// CountTaskFailuresOfWorker returns the number of task failures of this worker, on this particular job and task type.
func (db *DB) CountTaskFailuresOfWorker(ctx context.Context, job *Job, worker *Worker, taskType string) (int, error) {
func (db *DB) CountTaskFailuresOfWorker(ctx context.Context, jobUUID string, workerID int64, taskType string) (int, error) {
var numFailures int64
queries := db.queries()
numFailures, err := queries.CountTaskFailuresOfWorker(ctx, sqlc.CountTaskFailuresOfWorkerParams{
JobID: int64(job.ID),
WorkerID: int64(worker.ID),
JobUUID: jobUUID,
WorkerID: workerID,
TaskType: taskType,
})

View File

@ -18,7 +18,7 @@ func TestAddWorkerToJobBlocklist(t *testing.T) {
{
// Add a worker to the block list.
err := db.AddWorkerToJobBlocklist(ctx, job, worker, "blender")
err := db.AddWorkerToJobBlocklist(ctx, job.ID, worker.ID, "blender")
require.NoError(t, err)
list, err := queries.Test_FetchJobBlocklist(ctx)
@ -33,7 +33,7 @@ func TestAddWorkerToJobBlocklist(t *testing.T) {
{
// Adding the same worker again should be a no-op.
err := db.AddWorkerToJobBlocklist(ctx, job, worker, "blender")
err := db.AddWorkerToJobBlocklist(ctx, job.ID, worker.ID, "blender")
require.NoError(t, err)
list, err := queries.Test_FetchJobBlocklist(ctx)
@ -48,7 +48,7 @@ func TestFetchJobBlocklist(t *testing.T) {
// Add a worker to the block list.
worker := createWorker(ctx, t, db)
err := db.AddWorkerToJobBlocklist(ctx, job, worker, "blender")
err := db.AddWorkerToJobBlocklist(ctx, job.ID, worker.ID, "blender")
require.NoError(t, err)
list, err := db.FetchJobBlocklist(ctx, job.UUID)
@ -68,9 +68,9 @@ func TestClearJobBlocklist(t *testing.T) {
// Add a worker and some entries to the block list.
worker := createWorker(ctx, t, db)
err := db.AddWorkerToJobBlocklist(ctx, job, worker, "blender")
err := db.AddWorkerToJobBlocklist(ctx, job.ID, worker.ID, "blender")
require.NoError(t, err)
err = db.AddWorkerToJobBlocklist(ctx, job, worker, "ffmpeg")
err = db.AddWorkerToJobBlocklist(ctx, job.ID, worker.ID, "ffmpeg")
require.NoError(t, err)
// Clear the blocklist.
@ -89,9 +89,9 @@ func TestRemoveFromJobBlocklist(t *testing.T) {
// Add a worker and some entries to the block list.
worker := createWorker(ctx, t, db)
err := db.AddWorkerToJobBlocklist(ctx, job, worker, "blender")
err := db.AddWorkerToJobBlocklist(ctx, job.ID, worker.ID, "blender")
require.NoError(t, err)
err = db.AddWorkerToJobBlocklist(ctx, job, worker, "ffmpeg")
err = db.AddWorkerToJobBlocklist(ctx, job.ID, worker.ID, "ffmpeg")
require.NoError(t, err)
// Remove an entry.
@ -148,20 +148,20 @@ func TestWorkersLeftToRun(t *testing.T) {
assert.Equal(t, uuidMap(worker1, worker2, workerC1), left)
// Two workers, one blocked.
_ = db.AddWorkerToJobBlocklist(ctx, job, worker1, "blender")
_ = db.AddWorkerToJobBlocklist(ctx, job.ID, worker1.ID, "blender")
left, err = db.WorkersLeftToRun(ctx, job, "blender")
require.NoError(t, err)
assert.Equal(t, uuidMap(worker2, workerC1), left)
// All workers blocked.
_ = db.AddWorkerToJobBlocklist(ctx, job, worker2, "blender")
_ = db.AddWorkerToJobBlocklist(ctx, job, workerC1, "blender")
_ = db.AddWorkerToJobBlocklist(ctx, job.ID, worker2.ID, "blender")
_ = db.AddWorkerToJobBlocklist(ctx, job.ID, workerC1.ID, "blender")
left, err = db.WorkersLeftToRun(ctx, job, "blender")
require.NoError(t, err)
assert.Empty(t, left)
// Two workers, unknown job.
fakeJob := Job{Model: Model{ID: 327}}
fakeJob := Job{ID: 327}
left, err = db.WorkersLeftToRun(ctx, &fakeJob, "blender")
require.NoError(t, err)
assert.Equal(t, uuidMap(worker1, worker2, workerC1), left)
@ -222,13 +222,13 @@ func TestWorkersLeftToRunWithTags(t *testing.T) {
assert.Equal(t, uuidMap(workerC13, workerC1), left)
// One worker blocked, one worker remain.
_ = db.AddWorkerToJobBlocklist(ctx, job, workerC1, "blender")
_ = db.AddWorkerToJobBlocklist(ctx, job.ID, workerC1.ID, "blender")
left, err = db.WorkersLeftToRun(ctx, job, "blender")
require.NoError(t, err)
assert.Equal(t, uuidMap(workerC13), left)
// All taged workers blocked.
_ = db.AddWorkerToJobBlocklist(ctx, job, workerC13, "blender")
_ = db.AddWorkerToJobBlocklist(ctx, job.ID, workerC13.ID, "blender")
left, err = db.WorkersLeftToRun(ctx, job, "blender")
require.NoError(t, err)
assert.Empty(t, left)
@ -238,13 +238,17 @@ func TestCountTaskFailuresOfWorker(t *testing.T) {
ctx, close, db, dbJob, authoredJob := jobTasksTestFixtures(t)
defer close()
task0, err := db.FetchTask(ctx, authoredJob.Tasks[0].UUID)
taskJobWorker0, err := db.FetchTask(ctx, authoredJob.Tasks[0].UUID)
require.NoError(t, err)
task1, err := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
taskJobWorker1, err := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
require.NoError(t, err)
task2, err := db.FetchTask(ctx, authoredJob.Tasks[2].UUID)
taskJobWorker2, err := db.FetchTask(ctx, authoredJob.Tasks[2].UUID)
require.NoError(t, err)
task0 := taskJobWorker0.Task
task1 := taskJobWorker1.Task
task2 := taskJobWorker2.Task
// Sanity check on the test data.
assert.Equal(t, "blender", task0.Type)
assert.Equal(t, "blender", task1.Type)
@ -254,28 +258,28 @@ func TestCountTaskFailuresOfWorker(t *testing.T) {
worker2 := createWorkerFrom(ctx, t, db, *worker1)
// Store some failures for different tasks
_, _ = db.AddWorkerToTaskFailedList(ctx, task0, worker1)
_, _ = db.AddWorkerToTaskFailedList(ctx, task1, worker1)
_, _ = db.AddWorkerToTaskFailedList(ctx, task1, worker2)
_, _ = db.AddWorkerToTaskFailedList(ctx, task2, worker1)
_, _ = db.AddWorkerToTaskFailedList(ctx, &task0, worker1)
_, _ = db.AddWorkerToTaskFailedList(ctx, &task1, worker1)
_, _ = db.AddWorkerToTaskFailedList(ctx, &task1, worker2)
_, _ = db.AddWorkerToTaskFailedList(ctx, &task2, worker1)
// Multiple failures.
numBlender1, err := db.CountTaskFailuresOfWorker(ctx, dbJob, worker1, "blender")
numBlender1, err := db.CountTaskFailuresOfWorker(ctx, dbJob.UUID, worker1.ID, "blender")
require.NoError(t, err)
assert.Equal(t, 2, numBlender1)
// Single failure, but multiple tasks exist of this type.
numBlender2, err := db.CountTaskFailuresOfWorker(ctx, dbJob, worker2, "blender")
numBlender2, err := db.CountTaskFailuresOfWorker(ctx, dbJob.UUID, worker2.ID, "blender")
require.NoError(t, err)
assert.Equal(t, 1, numBlender2)
// Single failure, only one task of this type exists.
numFFMpeg1, err := db.CountTaskFailuresOfWorker(ctx, dbJob, worker1, "ffmpeg")
numFFMpeg1, err := db.CountTaskFailuresOfWorker(ctx, dbJob.UUID, worker1.ID, "ffmpeg")
require.NoError(t, err)
assert.Equal(t, 1, numFFMpeg1)
// No failure.
numFFMpeg2, err := db.CountTaskFailuresOfWorker(ctx, dbJob, worker2, "ffmpeg")
numFFMpeg2, err := db.CountTaskFailuresOfWorker(ctx, dbJob.UUID, worker2.ID, "ffmpeg")
require.NoError(t, err)
assert.Equal(t, 0, numFFMpeg2)
}

View File

@ -5,42 +5,39 @@ import (
"context"
"github.com/rs/zerolog/log"
"projects.blender.org/studio/flamenco/internal/manager/persistence/sqlc"
"projects.blender.org/studio/flamenco/pkg/api"
)
type TaskSummary = sqlc.QueryJobTaskSummariesRow
// QueryJobTaskSummaries retrieves all tasks of the job, but not all fields of those tasks.
// Fields are synchronised with api.TaskSummary.
func (db *DB) QueryJobTaskSummaries(ctx context.Context, jobUUID string) ([]*Task, error) {
func (db *DB) QueryJobTaskSummaries(ctx context.Context, jobUUID string) ([]TaskSummary, error) {
logger := log.Ctx(ctx)
logger.Debug().Str("job", jobUUID).Msg("querying task summaries")
queries := db.queries()
sqlcPartialTasks, err := queries.QueryJobTaskSummaries(ctx, jobUUID)
summaries, err := queries.QueryJobTaskSummaries(ctx, jobUUID)
if err != nil {
return nil, err
}
// Convert to partial GORM tasks.
gormTasks := make([]*Task, len(sqlcPartialTasks))
for index, task := range sqlcPartialTasks {
gormTask := Task{
Model: Model{
ID: uint(task.ID),
UpdatedAt: task.UpdatedAt.Time,
},
result := make([]TaskSummary, len(summaries))
for index, task := range summaries {
result[index] = TaskSummary{
ID: task.ID,
UpdatedAt: task.UpdatedAt,
UUID: task.UUID,
Name: task.Name,
Type: task.Type,
IndexInJob: int(task.IndexInJob),
Priority: int(task.Priority),
IndexInJob: task.IndexInJob,
Priority: task.Priority,
Status: api.TaskStatus(task.Status),
JobUUID: jobUUID,
}
gormTasks[index] = &gormTask
}
return gormTasks, nil
return result, nil
}
// JobStatusCount is a mapping from job status to the number of jobs in that status.

View File

@ -49,7 +49,7 @@ func TestQueryJobTaskSummaries(t *testing.T) {
assert.Len(t, summaries, len(expectTaskUUIDs))
for index, summary := range summaries {
assert.True(t, expectTaskUUIDs[summary.UUID], "%q should be in %v", summary.UUID, expectTaskUUIDs)
assert.Equal(t, index+1, summary.IndexInJob)
assert.Equal(t, int64(index+1), summary.IndexInJob)
}
}

View File

@ -4,6 +4,8 @@ package persistence
// SPDX-License-Identifier: GPL-3.0-or-later
import (
"database/sql"
"encoding/json"
"fmt"
"math"
"testing"
@ -36,11 +38,17 @@ func TestStoreAuthoredJob(t *testing.T) {
assert.Equal(t, job.JobID, fetchedJob.UUID)
assert.Equal(t, job.Name, fetchedJob.Name)
assert.Equal(t, job.JobType, fetchedJob.JobType)
assert.Equal(t, job.Priority, fetchedJob.Priority)
assert.Equal(t, job.Priority, int(fetchedJob.Priority))
assert.Equal(t, api.JobStatusUnderConstruction, fetchedJob.Status)
assert.EqualValues(t, map[string]interface{}(job.Settings), fetchedJob.Settings)
assert.EqualValues(t, map[string]string(job.Metadata), fetchedJob.Metadata)
assert.Equal(t, "", fetchedJob.Storage.ShamanCheckoutID)
assert.Equal(t, "", fetchedJob.StorageShamanCheckoutID)
var parsedSettings map[string]interface{}
assert.NoError(t, json.Unmarshal(fetchedJob.Settings, &parsedSettings))
assert.EqualValues(t, map[string]interface{}(job.Settings), parsedSettings)
var parsedMetadata map[string]string
assert.NoError(t, json.Unmarshal(fetchedJob.Metadata, &parsedMetadata))
assert.EqualValues(t, map[string]string(job.Metadata), parsedMetadata)
// Fetch result of job.
result, err := queries.FetchTasksOfJob(ctx, int64(fetchedJob.ID))
@ -70,7 +78,7 @@ func TestStoreAuthoredJobWithShamanCheckoutID(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, fetchedJob)
assert.Equal(t, job.Storage.ShamanCheckoutID, fetchedJob.Storage.ShamanCheckoutID)
assert.Equal(t, job.Storage.ShamanCheckoutID, fetchedJob.StorageShamanCheckoutID)
}
func TestStoreAuthoredJobWithWorkerTag(t *testing.T) {
@ -97,12 +105,8 @@ func TestStoreAuthoredJobWithWorkerTag(t *testing.T) {
require.NotNil(t, fetchedJob)
require.NotNil(t, fetchedJob.WorkerTagID)
assert.Equal(t, int64(*fetchedJob.WorkerTagID), workerTag.ID)
require.NotNil(t, fetchedJob.WorkerTag)
assert.Equal(t, fetchedJob.WorkerTag.Name, workerTag.Name)
assert.Equal(t, fetchedJob.WorkerTag.Description, workerTag.Description)
assert.Equal(t, fetchedJob.WorkerTag.UUID, workerTagUUID)
assert.Equal(t, fetchedJob.WorkerTagID.Int64, workerTag.ID)
assert.True(t, fetchedJob.WorkerTagID.Valid)
}
func TestFetchTaskJobUUID(t *testing.T) {
@ -135,20 +139,20 @@ func TestSaveJobStorageInfo(t *testing.T) {
dbJob, err := db.FetchJob(ctx, authoredJob.JobID)
require.NoError(t, err)
assert.NotNil(t, dbJob)
assert.EqualValues(t, startTime, dbJob.UpdatedAt)
assert.EqualValues(t, startTime, dbJob.UpdatedAt.Time)
// Move the clock forward.
updateTime := time.Date(2023, time.February, 7, 15, 10, 0, 0, time.UTC)
mockNow = updateTime
// Save the storage info.
dbJob.Storage.ShamanCheckoutID = "shaman/checkout/id"
dbJob.StorageShamanCheckoutID = "shaman/checkout/id"
require.NoError(t, db.SaveJobStorageInfo(ctx, dbJob))
// Check that the UpdatedAt field wasn't touched.
updatedJob, err := db.FetchJob(ctx, authoredJob.JobID)
require.NoError(t, err)
assert.Equal(t, startTime, updatedJob.UpdatedAt, "SaveJobStorageInfo should not touch UpdatedAt")
assert.Equal(t, startTime, updatedJob.UpdatedAt.Time, "SaveJobStorageInfo should not touch UpdatedAt")
}
func TestSaveJobPriority(t *testing.T) {
@ -161,7 +165,7 @@ func TestSaveJobPriority(t *testing.T) {
require.NoError(t, err)
// Set a new priority.
newPriority := 47
newPriority := int64(47)
dbJob, err := db.FetchJob(ctx, authoredJob.JobID)
require.NoError(t, err)
require.NotEqual(t, newPriority, dbJob.Priority,
@ -316,7 +320,7 @@ func TestRequestJobMassDeletion(t *testing.T) {
// Request that "job3 and older" gets deleted.
timeOfDeleteRequest := realNowFunc()
db.nowfunc = func() time.Time { return timeOfDeleteRequest }
uuids, err := db.RequestJobMassDeletion(ctx, job3.UpdatedAt)
uuids, err := db.RequestJobMassDeletion(ctx, job3.UpdatedAt.Time)
require.NoError(t, err)
db.nowfunc = realNowFunc
@ -428,11 +432,11 @@ func TestCountTasksOfJobInStatus(t *testing.T) {
assert.Equal(t, 3, numQueued)
assert.Equal(t, 3, numTotal)
// Make one task failed.
task, err := db.FetchTask(ctx, authoredJob.Tasks[0].UUID)
// Make one taskJobWorker failed.
taskJobWorker, err := db.FetchTask(ctx, authoredJob.Tasks[0].UUID)
require.NoError(t, err)
task.Status = api.TaskStatusFailed
require.NoError(t, db.SaveTask(ctx, task))
taskJobWorker.Task.Status = api.TaskStatusFailed
require.NoError(t, db.SaveTask(ctx, &taskJobWorker.Task))
numQueued, numTotal, err = db.CountTasksOfJobInStatus(ctx, job, api.TaskStatusQueued)
require.NoError(t, err)
@ -515,33 +519,33 @@ func TestFetchTasksOfJobInStatus(t *testing.T) {
allTasks, err := db.FetchTasksOfJob(ctx, job)
require.NoError(t, err)
assert.Equal(t, job, allTasks[0].Job, "FetchTasksOfJob should set job pointer")
assert.Equal(t, job.UUID, allTasks[0].JobUUID, "FetchTasksOfJob should set job UUID")
tasks, err := db.FetchTasksOfJobInStatus(ctx, job, api.TaskStatusQueued)
tasksJobsWorkers, err := db.FetchTasksOfJobInStatus(ctx, job, api.TaskStatusQueued)
require.NoError(t, err)
assert.Equal(t, allTasks, tasks)
assert.Equal(t, job, tasks[0].Job, "FetchTasksOfJobInStatus should set job pointer")
assert.Equal(t, allTasks, tasksJobsWorkers)
assert.Equal(t, job.UUID, tasksJobsWorkers[0].JobUUID, "FetchTasksOfJobInStatus should set job UUID")
// Make one task failed.
task, err := db.FetchTask(ctx, authoredJob.Tasks[0].UUID)
// Make one taskJobWorker failed.
taskJobWorker, err := db.FetchTask(ctx, authoredJob.Tasks[0].UUID)
require.NoError(t, err)
task.Status = api.TaskStatusFailed
require.NoError(t, db.SaveTask(ctx, task))
taskJobWorker.Task.Status = api.TaskStatusFailed
require.NoError(t, db.SaveTask(ctx, &taskJobWorker.Task))
tasks, err = db.FetchTasksOfJobInStatus(ctx, job, api.TaskStatusQueued)
tasksJobsWorkers, err = db.FetchTasksOfJobInStatus(ctx, job, api.TaskStatusQueued)
require.NoError(t, err)
assert.Equal(t, []*Task{allTasks[1], allTasks[2]}, tasks)
assert.Equal(t, []TaskJobWorker{allTasks[1], allTasks[2]}, tasksJobsWorkers)
// Check the failed task. This cannot directly compare to `allTasks[0]`
// because saving the task above changed some of its fields.
tasks, err = db.FetchTasksOfJobInStatus(ctx, job, api.TaskStatusFailed)
tasksJobsWorkers, err = db.FetchTasksOfJobInStatus(ctx, job, api.TaskStatusFailed)
require.NoError(t, err)
assert.Len(t, tasks, 1)
assert.Equal(t, allTasks[0].ID, tasks[0].ID)
assert.Len(t, tasksJobsWorkers, 1)
assert.Equal(t, allTasks[0].Task.ID, tasksJobsWorkers[0].Task.ID)
tasks, err = db.FetchTasksOfJobInStatus(ctx, job, api.TaskStatusActive)
tasksJobsWorkers, err = db.FetchTasksOfJobInStatus(ctx, job, api.TaskStatusActive)
require.NoError(t, err)
assert.Empty(t, tasks)
assert.Empty(t, tasksJobsWorkers)
}
func TestSaveTaskActivity(t *testing.T) {
@ -549,16 +553,20 @@ func TestSaveTaskActivity(t *testing.T) {
defer close()
taskUUID := authoredJob.Tasks[0].UUID
task, err := db.FetchTask(ctx, taskUUID)
taskJobWorker, err := db.FetchTask(ctx, taskUUID)
require.NoError(t, err)
task := taskJobWorker.Task
require.Equal(t, api.TaskStatusQueued, task.Status)
task.Activity = "Somebody ran a ünit test"
task.Status = api.TaskStatusPaused // Should not be saved.
require.NoError(t, db.SaveTaskActivity(ctx, task))
require.NoError(t, db.SaveTaskActivity(ctx, &task))
dbTask, err := db.FetchTask(ctx, taskUUID)
dbTaskJobWorker, err := db.FetchTask(ctx, taskUUID)
require.NoError(t, err)
dbTask := dbTaskJobWorker.Task
require.Equal(t, "Somebody ran a ünit test", dbTask.Activity)
require.Equal(t, api.TaskStatusQueued, dbTask.Status,
"SaveTaskActivity() should not save the task status")
@ -568,44 +576,40 @@ func TestTaskAssignToWorker(t *testing.T) {
ctx, close, db, _, authoredJob := jobTasksTestFixtures(t)
defer close()
task, err := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
taskJobWorker, err := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
require.NoError(t, err)
assert.Zero(t, taskJobWorker.WorkerUUID)
assert.Equal(t, authoredJob.JobID, taskJobWorker.JobUUID)
w := createWorker(ctx, t, db)
require.NoError(t, db.TaskAssignToWorker(ctx, task, w))
if task.Worker == nil {
t.Error("task.Worker == nil")
} else {
assert.Equal(t, w, task.Worker)
}
if task.WorkerID == nil {
t.Error("task.WorkerID == nil")
} else {
assert.Equal(t, w.ID, int64(*task.WorkerID))
}
require.NoError(t, db.TaskAssignToWorker(ctx, &taskJobWorker.Task, w))
assert.Equal(t,
sql.NullInt64{Int64: w.ID, Valid: true},
taskJobWorker.Task.WorkerID)
}
func TestFetchTasksOfWorkerInStatus(t *testing.T) {
ctx, close, db, _, authoredJob := jobTasksTestFixtures(t)
defer close()
task, err := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
taskJobWorker, err := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
task := taskJobWorker.Task
require.NoError(t, err)
w := createWorker(ctx, t, db)
require.NoError(t, db.TaskAssignToWorker(ctx, task, w))
require.NoError(t, db.TaskAssignToWorker(ctx, &task, w))
tasks, err := db.FetchTasksOfWorkerInStatus(ctx, w, task.Status)
tasksJobsWorkers, err := db.FetchTasksOfWorkerInStatus(ctx, w, task.Status)
require.NoError(t, err)
assert.Len(t, tasks, 1, "worker should have one task in status %q", task.Status)
assert.Equal(t, task.ID, tasks[0].ID)
assert.Equal(t, task.UUID, tasks[0].UUID)
assert.Len(t, tasksJobsWorkers, 1, "worker should have one task in status %q", task.Status)
assert.Equal(t, task.ID, tasksJobsWorkers[0].Task.ID)
assert.Equal(t, task.UUID, tasksJobsWorkers[0].Task.UUID)
assert.Equal(t, authoredJob.JobID, tasksJobsWorkers[0].JobUUID)
assert.NotEqual(t, api.TaskStatusCanceled, task.Status)
tasks, err = db.FetchTasksOfWorkerInStatus(ctx, w, api.TaskStatusCanceled)
require.NotEqual(t, api.TaskStatusCanceled, task.Status)
tasksJobsWorkers, err = db.FetchTasksOfWorkerInStatus(ctx, w, api.TaskStatusCanceled)
require.NoError(t, err)
assert.Empty(t, tasks, "worker should have no task in status %q", w)
assert.Empty(t, tasksJobsWorkers, "worker should have no task in status %q", api.TaskStatusCanceled)
}
func TestFetchTasksOfWorkerInStatusOfJob(t *testing.T) {
@ -630,42 +634,42 @@ func TestFetchTasksOfWorkerInStatusOfJob(t *testing.T) {
// Assign a task from each job to each Worker.
// Also double-check the test precondition that all tasks have the same status.
{ // Job / Worker.
task1, err := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
taskJobWorker1, err := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
require.NoError(t, err)
require.NoError(t, db.TaskAssignToWorker(ctx, task1, worker))
require.Equal(t, task1.Status, api.TaskStatusQueued)
require.NoError(t, db.TaskAssignToWorker(ctx, &taskJobWorker1.Task, worker))
require.Equal(t, taskJobWorker1.Task.Status, api.TaskStatusQueued)
task2, err := db.FetchTask(ctx, authoredJob.Tasks[0].UUID)
taskJobWorker2, err := db.FetchTask(ctx, authoredJob.Tasks[0].UUID)
require.NoError(t, err)
require.NoError(t, db.TaskAssignToWorker(ctx, task2, worker))
require.Equal(t, task2.Status, api.TaskStatusQueued)
require.NoError(t, db.TaskAssignToWorker(ctx, &taskJobWorker2.Task, worker))
require.Equal(t, taskJobWorker2.Task.Status, api.TaskStatusQueued)
}
{ // Job / Other Worker.
task, err := db.FetchTask(ctx, authoredJob.Tasks[2].UUID)
taskJobWorker, err := db.FetchTask(ctx, authoredJob.Tasks[2].UUID)
require.NoError(t, err)
require.NoError(t, db.TaskAssignToWorker(ctx, task, otherWorker))
require.Equal(t, task.Status, api.TaskStatusQueued)
require.NoError(t, db.TaskAssignToWorker(ctx, &taskJobWorker.Task, otherWorker))
require.Equal(t, taskJobWorker.Task.Status, api.TaskStatusQueued)
}
{ // Other Job / Worker.
task, err := db.FetchTask(ctx, otherJob.Tasks[1].UUID)
taskJobWorker, err := db.FetchTask(ctx, otherJob.Tasks[1].UUID)
require.NoError(t, err)
require.NoError(t, db.TaskAssignToWorker(ctx, task, worker))
require.Equal(t, task.Status, api.TaskStatusQueued)
require.NoError(t, db.TaskAssignToWorker(ctx, &taskJobWorker.Task, worker))
require.Equal(t, taskJobWorker.Task.Status, api.TaskStatusQueued)
}
{ // Other Job / Other Worker.
task, err := db.FetchTask(ctx, otherJob.Tasks[2].UUID)
taskJobWorker, err := db.FetchTask(ctx, otherJob.Tasks[2].UUID)
require.NoError(t, err)
require.NoError(t, db.TaskAssignToWorker(ctx, task, otherWorker))
require.Equal(t, task.Status, api.TaskStatusQueued)
require.NoError(t, db.TaskAssignToWorker(ctx, &taskJobWorker.Task, otherWorker))
require.Equal(t, taskJobWorker.Task.Status, api.TaskStatusQueued)
}
{ // Test active tasks, should be none.
tasks, err := db.FetchTasksOfWorkerInStatusOfJob(ctx, worker, api.TaskStatusActive, dbJob)
tasks, err := db.FetchTasksOfWorkerInStatusOfJob(ctx, worker, api.TaskStatusActive, dbJob.UUID)
require.NoError(t, err)
require.Len(t, tasks, 0)
}
{ // Test queued tasks, should be two.
tasks, err := db.FetchTasksOfWorkerInStatusOfJob(ctx, worker, api.TaskStatusQueued, dbJob)
tasks, err := db.FetchTasksOfWorkerInStatusOfJob(ctx, worker, api.TaskStatusQueued, dbJob.UUID)
require.NoError(t, err)
require.Len(t, tasks, 2)
assert.Equal(t, authoredJob.Tasks[0].UUID, tasks[0].UUID)
@ -675,7 +679,7 @@ func TestFetchTasksOfWorkerInStatusOfJob(t *testing.T) {
worker := createWorker(ctx, t, db, func(worker *Worker) {
worker.UUID = "6534a1d4-f58e-4f2c-8925-4b2cd6caac22"
})
tasks, err := db.FetchTasksOfWorkerInStatusOfJob(ctx, worker, api.TaskStatusQueued, dbJob)
tasks, err := db.FetchTasksOfWorkerInStatusOfJob(ctx, worker, api.TaskStatusQueued, dbJob.UUID)
require.NoError(t, err)
require.Len(t, tasks, 0)
}
@ -685,27 +689,29 @@ func TestTaskTouchedByWorker(t *testing.T) {
ctx, close, db, _, authoredJob := jobTasksTestFixtures(t)
defer close()
task, err := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
taskJobWorker, err := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
task := taskJobWorker.Task
require.NoError(t, err)
assert.True(t, task.LastTouchedAt.IsZero())
assert.Zero(t, task.LastTouchedAt)
now := db.now()
err = db.TaskTouchedByWorker(ctx, task)
err = db.TaskTouchedByWorker(ctx, task.UUID)
require.NoError(t, err)
// Test the task instance as well as the database entry.
dbTask, err := db.FetchTask(ctx, task.UUID)
// Test the task as it is in the database.
dbTaskJobWorker, err := db.FetchTask(ctx, task.UUID)
require.NoError(t, err)
assert.WithinDuration(t, now, task.LastTouchedAt, time.Second)
assert.WithinDuration(t, now, dbTask.LastTouchedAt, time.Second)
assert.True(t, dbTaskJobWorker.Task.LastTouchedAt.Valid)
assert.WithinDuration(t, now, dbTaskJobWorker.Task.LastTouchedAt.Time, time.Second)
}
func TestAddWorkerToTaskFailedList(t *testing.T) {
ctx, close, db, _, authoredJob := jobTasksTestFixtures(t)
defer close()
task, err := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
taskJobWorker, err := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
require.NoError(t, err)
task := taskJobWorker.Task
worker1 := createWorker(ctx, t, db)
@ -719,17 +725,17 @@ func TestAddWorkerToTaskFailedList(t *testing.T) {
require.NoError(t, err)
// First failure should be registered just fine.
numFailed, err := db.AddWorkerToTaskFailedList(ctx, task, worker1)
numFailed, err := db.AddWorkerToTaskFailedList(ctx, &task, worker1)
require.NoError(t, err)
assert.Equal(t, 1, numFailed)
// Calling again should be a no-op and not cause any errors.
numFailed, err = db.AddWorkerToTaskFailedList(ctx, task, worker1)
numFailed, err = db.AddWorkerToTaskFailedList(ctx, &task, worker1)
require.NoError(t, err)
assert.Equal(t, 1, numFailed)
// Another worker should be able to fail this task as well.
numFailed, err = db.AddWorkerToTaskFailedList(ctx, task, worker2)
numFailed, err = db.AddWorkerToTaskFailedList(ctx, &task, worker2)
require.NoError(t, err)
assert.Equal(t, 2, numFailed)
@ -743,8 +749,10 @@ func TestClearFailureListOfTask(t *testing.T) {
defer close()
queries := db.queries()
task1, _ := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
task2, _ := db.FetchTask(ctx, authoredJob.Tasks[2].UUID)
taskJobWorker1, _ := db.FetchTask(ctx, authoredJob.Tasks[1].UUID)
taskJobWorker2, _ := db.FetchTask(ctx, authoredJob.Tasks[2].UUID)
task1 := taskJobWorker1.Task
task2 := taskJobWorker2.Task
worker1 := createWorker(ctx, t, db)
@ -758,12 +766,12 @@ func TestClearFailureListOfTask(t *testing.T) {
require.NoError(t, err)
// Store some failures for different tasks.
_, _ = db.AddWorkerToTaskFailedList(ctx, task1, worker1)
_, _ = db.AddWorkerToTaskFailedList(ctx, task1, worker2)
_, _ = db.AddWorkerToTaskFailedList(ctx, task2, worker1)
_, _ = db.AddWorkerToTaskFailedList(ctx, &task1, worker1)
_, _ = db.AddWorkerToTaskFailedList(ctx, &task1, worker2)
_, _ = db.AddWorkerToTaskFailedList(ctx, &task2, worker1)
// Clearing should just update this one task.
require.NoError(t, db.ClearFailureListOfTask(ctx, task1))
require.NoError(t, db.ClearFailureListOfTask(ctx, &task1))
failures, err := queries.Test_FetchTaskFailures(ctx)
require.NoError(t, err)
if assert.Len(t, failures, 1) {
@ -781,19 +789,22 @@ func TestClearFailureListOfJob(t *testing.T) {
authoredJob2 := duplicateJobAndTasks(authoredJob1)
persistAuthoredJob(t, ctx, db, authoredJob2)
task1_1, _ := db.FetchTask(ctx, authoredJob1.Tasks[1].UUID)
task1_2, _ := db.FetchTask(ctx, authoredJob1.Tasks[2].UUID)
task2_1, _ := db.FetchTask(ctx, authoredJob2.Tasks[1].UUID)
taskJobWorker1_1, _ := db.FetchTask(ctx, authoredJob1.Tasks[1].UUID)
taskJobWorker1_2, _ := db.FetchTask(ctx, authoredJob1.Tasks[2].UUID)
taskJobWorker2_1, _ := db.FetchTask(ctx, authoredJob2.Tasks[1].UUID)
task1_1 := taskJobWorker1_1.Task
task1_2 := taskJobWorker1_2.Task
task2_1 := taskJobWorker2_1.Task
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)
_, _ = db.AddWorkerToTaskFailedList(ctx, task2_1, worker1)
_, _ = db.AddWorkerToTaskFailedList(ctx, task2_1, worker2)
_, _ = db.AddWorkerToTaskFailedList(ctx, &task1_1, worker1)
_, _ = db.AddWorkerToTaskFailedList(ctx, &task1_1, worker2)
_, _ = db.AddWorkerToTaskFailedList(ctx, &task1_2, worker1)
_, _ = db.AddWorkerToTaskFailedList(ctx, &task2_1, worker1)
_, _ = db.AddWorkerToTaskFailedList(ctx, &task2_1, worker2)
// Sanity check: there should be 5 failures registered now.
assert.Equal(t, 5, countTaskFailures(ctx, db))
@ -815,16 +826,18 @@ func TestFetchTaskFailureList(t *testing.T) {
defer close()
// Test with non-existing task.
fakeTask := Task{Model: Model{ID: 327}}
fakeTask := Task{ID: 327}
failures, err := db.FetchTaskFailureList(ctx, &fakeTask)
require.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)
taskJobWorker1_1, _ := db.FetchTask(ctx, authoredJob1.Tasks[1].UUID)
taskJobWorker1_2, _ := db.FetchTask(ctx, authoredJob1.Tasks[2].UUID)
task1_1 := taskJobWorker1_1.Task
task1_2 := taskJobWorker1_2.Task
// Test without failures.
failures, err = db.FetchTaskFailureList(ctx, task1_1)
failures, err = db.FetchTaskFailureList(ctx, &task1_1)
require.NoError(t, err)
assert.Empty(t, failures)
@ -832,12 +845,12 @@ func TestFetchTaskFailureList(t *testing.T) {
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)
_, _ = 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)
failures, err = db.FetchTaskFailureList(ctx, &task1_1)
require.NoError(t, err)
if assert.Len(t, failures, 2) {

View File

@ -15,14 +15,19 @@ import (
// This is used to show the global last-rendered image in the web interface.
// SetLastRendered sets this job as the one with the most recent rendered image.
func (db *DB) SetLastRendered(ctx context.Context, j *Job) error {
func (db *DB) SetLastRendered(ctx context.Context, jobUUID string) error {
queries := db.queries()
jobID, err := queries.FetchJobIDFromUUID(ctx, jobUUID)
if err != nil {
return jobError(err, "finding job with UUID %q", jobUUID)
}
now := db.nowNullable()
return queries.SetLastRendered(ctx, sqlc.SetLastRenderedParams{
CreatedAt: now.Time,
UpdatedAt: now,
JobID: int64(j.ID),
JobID: jobID,
})
}

View File

@ -17,7 +17,7 @@ func TestSetLastRendered(t *testing.T) {
authoredJob2 := authorTestJob("1295757b-e668-4c49-8b89-f73db8270e42", "just-a-job")
job2 := persistAuthoredJob(t, ctx, db, authoredJob2)
require.NoError(t, db.SetLastRendered(ctx, job1))
require.NoError(t, db.SetLastRendered(ctx, job1.UUID))
{
entries, err := queries.Test_FetchLastRendered(ctx)
require.NoError(t, err)
@ -26,7 +26,7 @@ func TestSetLastRendered(t *testing.T) {
}
}
require.NoError(t, db.SetLastRendered(ctx, job2))
require.NoError(t, db.SetLastRendered(ctx, job2.UUID))
{
entries, err := queries.Test_FetchLastRendered(ctx)
require.NoError(t, err)
@ -49,7 +49,7 @@ func TestGetLastRenderedJobUUID(t *testing.T) {
{
// Test with first render.
require.NoError(t, db.SetLastRendered(ctx, job1))
require.NoError(t, db.SetLastRendered(ctx, job1.UUID))
lastUUID, err := db.GetLastRenderedJobUUID(ctx)
require.NoError(t, err)
assert.Equal(t, job1.UUID, lastUUID)
@ -60,7 +60,7 @@ func TestGetLastRenderedJobUUID(t *testing.T) {
authoredJob2 := authorTestJob("1295757b-e668-4c49-8b89-f73db8270e42", "just-a-job")
job2 := persistAuthoredJob(t, ctx, db, authoredJob2)
require.NoError(t, db.SetLastRendered(ctx, job2))
require.NoError(t, db.SetLastRendered(ctx, job2.UUID))
lastUUID, err := db.GetLastRenderedJobUUID(ctx)
require.NoError(t, err)
assert.Equal(t, job2.UUID, lastUUID)

View File

@ -20,6 +20,11 @@ func (ss *SleepSchedule) SetNextCheck(nextCheck time.Time) {
}
}
// DeleteRequested returns whether deletion of this job was requested.
func (j *Job) DeleteRequested() bool {
return j.DeleteRequestedAt.Valid
}
func (w *Worker) Identifier() string {
// Avoid a panic when worker.Identifier() is called on a nil pointer.
if w == nil {

View File

@ -117,18 +117,19 @@ LEFT JOIN workers ON (tasks.worker_id = workers.id)
WHERE tasks.uuid = @uuid;
-- name: FetchTasksOfWorkerInStatus :many
SELECT sqlc.embed(tasks), jobs.UUID as jobUUID
SELECT sqlc.embed(tasks), jobs.uuid as jobuuid
FROM tasks
LEFT JOIN jobs ON (tasks.job_id = jobs.id)
INNER JOIN jobs ON (tasks.job_id = jobs.id)
WHERE tasks.worker_id = @worker_id
AND tasks.status = @task_status;
-- name: FetchTasksOfWorkerInStatusOfJob :many
SELECT sqlc.embed(tasks)
FROM tasks
LEFT JOIN jobs ON (tasks.job_id = jobs.id)
WHERE tasks.worker_id = @worker_id
AND tasks.job_id = @job_id
AND tasks.status = @task_status;
AND tasks.status = @task_status
AND jobs.uuid = @jobuuid;
-- name: FetchTasksOfJob :many
SELECT sqlc.embed(tasks), workers.UUID as workerUUID
@ -199,7 +200,7 @@ WHERE id=@id;
UPDATE tasks SET
updated_at = @updated_at,
last_touched_at = @last_touched_at
WHERE id=@id;
WHERE uuid=@uuid;
-- name: JobCountTasksInStatus :one
-- Fetch number of tasks in the given status, of the given job.
@ -235,6 +236,13 @@ SELECT sqlc.embed(workers) FROM workers
INNER JOIN task_failures TF on TF.worker_id=workers.id
WHERE TF.task_id=@task_id;
-- name: FetchJobIDFromUUID :one
-- Fetch the job's database ID by its UUID.
--
-- This query is here to keep the SetLastRendered query below simpler,
-- mostly because that query is alread hitting a limitation of sqlc.
SELECT id FROM jobs WHERE uuid=@jobuuid;
-- name: SetLastRendered :exec
-- Set the 'last rendered' job info.
--
@ -309,9 +317,10 @@ AND WTM.worker_tag_id = @worker_tag_id;
-- name: CountTaskFailuresOfWorker :one
SELECT count(TF.task_id) FROM task_failures TF
INNER JOIN tasks T ON TF.task_id = T.id
INNER JOIN jobs J ON T.job_id = J.id
WHERE
TF.worker_id = @worker_id
AND T.job_id = @job_id
AND J.uuid = @jobuuid
AND T.type = @task_type;
@ -326,11 +335,17 @@ SELECT status, count(id) as status_count FROM jobs
GROUP BY status;
-- name: FetchTimedOutTasks :many
SELECT *
SELECT sqlc.embed(tasks),
-- Cast to remove nullability from the generated structs.
CAST(jobs.uuid AS VARCHAR(36)) as jobuuid,
CAST(workers.name AS VARCHAR(64)) as worker_name,
CAST(workers.uuid AS VARCHAR(36)) as workeruuid
FROM tasks
LEFT JOIN jobs ON jobs.id = tasks.job_id
LEFT JOIN workers ON workers.id = tasks.worker_id
WHERE
status = @task_status
AND last_touched_at <= @untouched_since;
tasks.status = @task_status
AND tasks.last_touched_at <= @untouched_since;
-- name: Test_CountJobs :one
-- Count the number of jobs in the database. Only used in unit tests.

View File

@ -89,20 +89,21 @@ func (q *Queries) ClearJobBlocklist(ctx context.Context, jobuuid string) error {
const countTaskFailuresOfWorker = `-- name: CountTaskFailuresOfWorker :one
SELECT count(TF.task_id) FROM task_failures TF
INNER JOIN tasks T ON TF.task_id = T.id
INNER JOIN jobs J ON T.job_id = J.id
WHERE
TF.worker_id = ?1
AND T.job_id = ?2
AND J.uuid = ?2
AND T.type = ?3
`
type CountTaskFailuresOfWorkerParams struct {
WorkerID int64
JobID int64
JobUUID string
TaskType string
}
func (q *Queries) CountTaskFailuresOfWorker(ctx context.Context, arg CountTaskFailuresOfWorkerParams) (int64, error) {
row := q.db.QueryRowContext(ctx, countTaskFailuresOfWorker, arg.WorkerID, arg.JobID, arg.TaskType)
row := q.db.QueryRowContext(ctx, countTaskFailuresOfWorker, arg.WorkerID, arg.JobUUID, arg.TaskType)
var count int64
err := row.Scan(&count)
return count, err
@ -352,6 +353,21 @@ func (q *Queries) FetchJobByID(ctx context.Context, id int64) (Job, error) {
return i, err
}
const fetchJobIDFromUUID = `-- name: FetchJobIDFromUUID :one
SELECT id FROM jobs WHERE uuid=?1
`
// Fetch the job's database ID by its UUID.
//
// This query is here to keep the SetLastRendered query below simpler,
// mostly because that query is alread hitting a limitation of sqlc.
func (q *Queries) FetchJobIDFromUUID(ctx context.Context, jobuuid string) (int64, error) {
row := q.db.QueryRowContext(ctx, fetchJobIDFromUUID, jobuuid)
var id int64
err := row.Scan(&id)
return id, err
}
const fetchJobShamanCheckoutID = `-- name: FetchJobShamanCheckoutID :one
SELECT storage_shaman_checkout_id FROM jobs WHERE uuid=?1
`
@ -736,9 +752,9 @@ func (q *Queries) FetchTasksOfJobInStatus(ctx context.Context, arg FetchTasksOfJ
}
const fetchTasksOfWorkerInStatus = `-- name: FetchTasksOfWorkerInStatus :many
SELECT tasks.id, tasks.created_at, tasks.updated_at, tasks.uuid, tasks.name, tasks.type, tasks.job_id, tasks.index_in_job, tasks.priority, tasks.status, tasks.worker_id, tasks.last_touched_at, tasks.commands, tasks.activity, jobs.UUID as jobUUID
SELECT tasks.id, tasks.created_at, tasks.updated_at, tasks.uuid, tasks.name, tasks.type, tasks.job_id, tasks.index_in_job, tasks.priority, tasks.status, tasks.worker_id, tasks.last_touched_at, tasks.commands, tasks.activity, jobs.uuid as jobuuid
FROM tasks
LEFT JOIN jobs ON (tasks.job_id = jobs.id)
INNER JOIN jobs ON (tasks.job_id = jobs.id)
WHERE tasks.worker_id = ?1
AND tasks.status = ?2
`
@ -750,7 +766,7 @@ type FetchTasksOfWorkerInStatusParams struct {
type FetchTasksOfWorkerInStatusRow struct {
Task Task
JobUUID sql.NullString
JobUUID string
}
func (q *Queries) FetchTasksOfWorkerInStatus(ctx context.Context, arg FetchTasksOfWorkerInStatusParams) ([]FetchTasksOfWorkerInStatusRow, error) {
@ -795,15 +811,16 @@ func (q *Queries) FetchTasksOfWorkerInStatus(ctx context.Context, arg FetchTasks
const fetchTasksOfWorkerInStatusOfJob = `-- name: FetchTasksOfWorkerInStatusOfJob :many
SELECT tasks.id, tasks.created_at, tasks.updated_at, tasks.uuid, tasks.name, tasks.type, tasks.job_id, tasks.index_in_job, tasks.priority, tasks.status, tasks.worker_id, tasks.last_touched_at, tasks.commands, tasks.activity
FROM tasks
LEFT JOIN jobs ON (tasks.job_id = jobs.id)
WHERE tasks.worker_id = ?1
AND tasks.job_id = ?2
AND tasks.status = ?3
AND tasks.status = ?2
AND jobs.uuid = ?3
`
type FetchTasksOfWorkerInStatusOfJobParams struct {
WorkerID sql.NullInt64
JobID int64
TaskStatus api.TaskStatus
JobUUID string
}
type FetchTasksOfWorkerInStatusOfJobRow struct {
@ -811,7 +828,7 @@ type FetchTasksOfWorkerInStatusOfJobRow struct {
}
func (q *Queries) FetchTasksOfWorkerInStatusOfJob(ctx context.Context, arg FetchTasksOfWorkerInStatusOfJobParams) ([]FetchTasksOfWorkerInStatusOfJobRow, error) {
rows, err := q.db.QueryContext(ctx, fetchTasksOfWorkerInStatusOfJob, arg.WorkerID, arg.JobID, arg.TaskStatus)
rows, err := q.db.QueryContext(ctx, fetchTasksOfWorkerInStatusOfJob, arg.WorkerID, arg.TaskStatus, arg.JobUUID)
if err != nil {
return nil, err
}
@ -849,11 +866,17 @@ func (q *Queries) FetchTasksOfWorkerInStatusOfJob(ctx context.Context, arg Fetch
}
const fetchTimedOutTasks = `-- name: FetchTimedOutTasks :many
SELECT id, created_at, updated_at, uuid, name, type, job_id, index_in_job, priority, status, worker_id, last_touched_at, commands, activity
SELECT tasks.id, tasks.created_at, tasks.updated_at, tasks.uuid, tasks.name, tasks.type, tasks.job_id, tasks.index_in_job, tasks.priority, tasks.status, tasks.worker_id, tasks.last_touched_at, tasks.commands, tasks.activity,
-- Cast to remove nullability from the generated structs.
CAST(jobs.uuid AS VARCHAR(36)) as jobuuid,
CAST(workers.name AS VARCHAR(64)) as worker_name,
CAST(workers.uuid AS VARCHAR(36)) as workeruuid
FROM tasks
LEFT JOIN jobs ON jobs.id = tasks.job_id
LEFT JOIN workers ON workers.id = tasks.worker_id
WHERE
status = ?1
AND last_touched_at <= ?2
tasks.status = ?1
AND tasks.last_touched_at <= ?2
`
type FetchTimedOutTasksParams struct {
@ -861,30 +884,40 @@ type FetchTimedOutTasksParams struct {
UntouchedSince sql.NullTime
}
func (q *Queries) FetchTimedOutTasks(ctx context.Context, arg FetchTimedOutTasksParams) ([]Task, error) {
type FetchTimedOutTasksRow struct {
Task Task
JobUUID string
WorkerName string
WorkerUUID string
}
func (q *Queries) FetchTimedOutTasks(ctx context.Context, arg FetchTimedOutTasksParams) ([]FetchTimedOutTasksRow, error) {
rows, err := q.db.QueryContext(ctx, fetchTimedOutTasks, arg.TaskStatus, arg.UntouchedSince)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Task
var items []FetchTimedOutTasksRow
for rows.Next() {
var i Task
var i FetchTimedOutTasksRow
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.UUID,
&i.Name,
&i.Type,
&i.JobID,
&i.IndexInJob,
&i.Priority,
&i.Status,
&i.WorkerID,
&i.LastTouchedAt,
&i.Commands,
&i.Activity,
&i.Task.ID,
&i.Task.CreatedAt,
&i.Task.UpdatedAt,
&i.Task.UUID,
&i.Task.Name,
&i.Task.Type,
&i.Task.JobID,
&i.Task.IndexInJob,
&i.Task.Priority,
&i.Task.Status,
&i.Task.WorkerID,
&i.Task.LastTouchedAt,
&i.Task.Commands,
&i.Task.Activity,
&i.JobUUID,
&i.WorkerName,
&i.WorkerUUID,
); err != nil {
return nil, err
}
@ -1221,17 +1254,17 @@ const taskTouchedByWorker = `-- name: TaskTouchedByWorker :exec
UPDATE tasks SET
updated_at = ?1,
last_touched_at = ?2
WHERE id=?3
WHERE uuid=?3
`
type TaskTouchedByWorkerParams struct {
UpdatedAt sql.NullTime
LastTouchedAt sql.NullTime
ID int64
UUID string
}
func (q *Queries) TaskTouchedByWorker(ctx context.Context, arg TaskTouchedByWorkerParams) error {
_, err := q.db.ExecContext(ctx, taskTouchedByWorker, arg.UpdatedAt, arg.LastTouchedAt, arg.ID)
_, err := q.db.ExecContext(ctx, taskTouchedByWorker, arg.UpdatedAt, arg.LastTouchedAt, arg.UUID)
return err
}

View File

@ -1,7 +1,7 @@
-- name: FetchAssignedAndRunnableTaskOfWorker :one
-- Fetch a task that's assigned to this worker, and is in a runnable state.
SELECT sqlc.embed(tasks)
SELECT sqlc.embed(tasks), jobs.uuid as jobuuid, jobs.priority as job_priority, jobs.job_type as job_type
FROM tasks
INNER JOIN jobs ON tasks.job_id = jobs.id
WHERE tasks.status=@active_task_status
@ -20,7 +20,7 @@ LIMIT 1;
--
-- The order in the WHERE clause is important, slices should come last. See
-- https://github.com/sqlc-dev/sqlc/issues/2452 for more info.
SELECT sqlc.embed(tasks)
SELECT sqlc.embed(tasks), jobs.uuid as jobuuid, jobs.priority as job_priority, jobs.job_type as job_type
FROM tasks
INNER JOIN jobs ON tasks.job_id = jobs.id
LEFT JOIN task_failures TF ON tasks.id = TF.task_id AND TF.worker_id=@worker_id
@ -51,8 +51,8 @@ ORDER BY jobs.priority DESC, tasks.priority DESC;
-- Find the currently-active task assigned to a Worker. If not found, find the last task this Worker worked on.
SELECT
sqlc.embed(tasks),
sqlc.embed(jobs),
(tasks.status = @task_status_active AND jobs.status = @job_status_active) as is_active
jobs.uuid as jobuuid,
CAST(tasks.status = @task_status_active AND jobs.status = @job_status_active AS BOOLEAN) as is_active
FROM tasks
INNER JOIN jobs ON tasks.job_id = jobs.id
WHERE

View File

@ -31,7 +31,7 @@ func (q *Queries) AssignTaskToWorker(ctx context.Context, arg AssignTaskToWorker
}
const fetchAssignedAndRunnableTaskOfWorker = `-- name: FetchAssignedAndRunnableTaskOfWorker :one
SELECT tasks.id, tasks.created_at, tasks.updated_at, tasks.uuid, tasks.name, tasks.type, tasks.job_id, tasks.index_in_job, tasks.priority, tasks.status, tasks.worker_id, tasks.last_touched_at, tasks.commands, tasks.activity
SELECT tasks.id, tasks.created_at, tasks.updated_at, tasks.uuid, tasks.name, tasks.type, tasks.job_id, tasks.index_in_job, tasks.priority, tasks.status, tasks.worker_id, tasks.last_touched_at, tasks.commands, tasks.activity, jobs.uuid as jobuuid, jobs.priority as job_priority, jobs.job_type as job_type
FROM tasks
INNER JOIN jobs ON tasks.job_id = jobs.id
WHERE tasks.status=?1
@ -48,6 +48,9 @@ type FetchAssignedAndRunnableTaskOfWorkerParams struct {
type FetchAssignedAndRunnableTaskOfWorkerRow struct {
Task Task
JobUUID string
JobPriority int64
JobType string
}
// Fetch a task that's assigned to this worker, and is in a runnable state.
@ -81,6 +84,9 @@ func (q *Queries) FetchAssignedAndRunnableTaskOfWorker(ctx context.Context, arg
&i.Task.LastTouchedAt,
&i.Task.Commands,
&i.Task.Activity,
&i.JobUUID,
&i.JobPriority,
&i.JobType,
)
return i, err
}
@ -88,8 +94,8 @@ func (q *Queries) FetchAssignedAndRunnableTaskOfWorker(ctx context.Context, arg
const fetchWorkerTask = `-- name: FetchWorkerTask :one
SELECT
tasks.id, tasks.created_at, tasks.updated_at, tasks.uuid, tasks.name, tasks.type, tasks.job_id, tasks.index_in_job, tasks.priority, tasks.status, tasks.worker_id, tasks.last_touched_at, tasks.commands, tasks.activity,
jobs.id, jobs.created_at, jobs.updated_at, jobs.uuid, jobs.name, jobs.job_type, jobs.priority, jobs.status, jobs.activity, jobs.settings, jobs.metadata, jobs.delete_requested_at, jobs.storage_shaman_checkout_id, jobs.worker_tag_id,
(tasks.status = ?1 AND jobs.status = ?2) as is_active
jobs.uuid as jobuuid,
CAST(tasks.status = ?1 AND jobs.status = ?2 AS BOOLEAN) as is_active
FROM tasks
INNER JOIN jobs ON tasks.job_id = jobs.id
WHERE
@ -108,8 +114,8 @@ type FetchWorkerTaskParams struct {
type FetchWorkerTaskRow struct {
Task Task
Job Job
IsActive interface{}
JobUUID string
IsActive bool
}
// Find the currently-active task assigned to a Worker. If not found, find the last task this Worker worked on.
@ -131,27 +137,14 @@ func (q *Queries) FetchWorkerTask(ctx context.Context, arg FetchWorkerTaskParams
&i.Task.LastTouchedAt,
&i.Task.Commands,
&i.Task.Activity,
&i.Job.ID,
&i.Job.CreatedAt,
&i.Job.UpdatedAt,
&i.Job.UUID,
&i.Job.Name,
&i.Job.JobType,
&i.Job.Priority,
&i.Job.Status,
&i.Job.Activity,
&i.Job.Settings,
&i.Job.Metadata,
&i.Job.DeleteRequestedAt,
&i.Job.StorageShamanCheckoutID,
&i.Job.WorkerTagID,
&i.JobUUID,
&i.IsActive,
)
return i, err
}
const findRunnableTask = `-- name: FindRunnableTask :one
SELECT tasks.id, tasks.created_at, tasks.updated_at, tasks.uuid, tasks.name, tasks.type, tasks.job_id, tasks.index_in_job, tasks.priority, tasks.status, tasks.worker_id, tasks.last_touched_at, tasks.commands, tasks.activity
SELECT tasks.id, tasks.created_at, tasks.updated_at, tasks.uuid, tasks.name, tasks.type, tasks.job_id, tasks.index_in_job, tasks.priority, tasks.status, tasks.worker_id, tasks.last_touched_at, tasks.commands, tasks.activity, jobs.uuid as jobuuid, jobs.priority as job_priority, jobs.job_type as job_type
FROM tasks
INNER JOIN jobs ON tasks.job_id = jobs.id
LEFT JOIN task_failures TF ON tasks.id = TF.task_id AND TF.worker_id=?1
@ -190,6 +183,9 @@ type FindRunnableTaskParams struct {
type FindRunnableTaskRow struct {
Task Task
JobUUID string
JobPriority int64
JobType string
}
// Find a task to be run by a worker. This is the core of the task scheduler.
@ -255,6 +251,9 @@ func (q *Queries) FindRunnableTask(ctx context.Context, arg FindRunnableTaskPara
&i.Task.LastTouchedAt,
&i.Task.Commands,
&i.Task.Activity,
&i.JobUUID,
&i.JobPriority,
&i.JobType,
)
return i, err
}

View File

@ -22,10 +22,22 @@ var (
// completedTaskStatuses = []api.TaskStatus{api.TaskStatusCompleted}
)
// ScheduledTask contains a Task and some info about its job.
//
// This structure is returned from different points in the code below, and
// filled from different sqlc-generated structs. That's why it has to be an
// explicit struct here, rather than an alias for some sqlc struct.
type ScheduledTask struct {
Task Task
JobUUID string
JobPriority int64
JobType string
}
// ScheduleTask finds a task to execute by the given worker.
// If no task is available, (nil, nil) is returned, as this is not an error situation.
// NOTE: this does not also fetch returnedTask.Worker, but returnedTask.WorkerID is set.
func (db *DB) ScheduleTask(ctx context.Context, w *Worker) (*Task, error) {
func (db *DB) ScheduleTask(ctx context.Context, w *Worker) (*ScheduledTask, error) {
logger := log.With().Str("worker", w.UUID).Logger()
logger.Trace().Msg("finding task for worker")
@ -41,32 +53,27 @@ func (db *DB) ScheduleTask(ctx context.Context, w *Worker) (*Task, error) {
defer qtx.rollback()
task, err := db.scheduleTask(ctx, qtx.queries, w, logger)
scheduledTask, err := db.scheduleTask(ctx, qtx.queries, w, logger)
if err != nil {
return nil, err
}
if task == nil {
if scheduledTask == nil {
// No task means no changes to the database.
// It's fine to just roll back the transaction.
return nil, nil
}
gormTask, err := convertSqlTaskWithJobAndWorker(ctx, qtx.queries, *task)
if err != nil {
return nil, err
}
if err := qtx.commit(); err != nil {
return nil, fmt.Errorf(
"could not commit database transaction after scheduling task %s for worker %s: %w",
task.UUID, w.UUID, err)
scheduledTask.Task.UUID, w.UUID, err)
}
return gormTask, nil
return scheduledTask, nil
}
func (db *DB) scheduleTask(ctx context.Context, queries *sqlc.Queries, w *Worker, logger zerolog.Logger) (*sqlc.Task, error) {
func (db *DB) scheduleTask(ctx context.Context, queries *sqlc.Queries, w *Worker, logger zerolog.Logger) (*ScheduledTask, error) {
if w.ID == 0 {
panic("worker should be in database, but has zero ID")
}
@ -76,7 +83,8 @@ func (db *DB) scheduleTask(ctx context.Context, queries *sqlc.Queries, w *Worker
// Note that this task type could be blocklisted or no longer supported by the
// Worker, but since it's active that is unlikely.
{
row, err := queries.FetchAssignedAndRunnableTaskOfWorker(ctx, sqlc.FetchAssignedAndRunnableTaskOfWorkerParams{
row, err := queries.FetchAssignedAndRunnableTaskOfWorker(
ctx, sqlc.FetchAssignedAndRunnableTaskOfWorkerParams{
ActiveTaskStatus: api.TaskStatusActive,
ActiveJobStatuses: schedulableJobStatuses,
WorkerID: workerID,
@ -88,11 +96,18 @@ func (db *DB) scheduleTask(ctx context.Context, queries *sqlc.Queries, w *Worker
case err != nil:
return nil, err
case row.Task.ID > 0:
return &row.Task, nil
// Task was previously assigned, just go for it again.
scheduledTask := ScheduledTask{
Task: row.Task,
JobUUID: row.JobUUID,
JobPriority: row.JobPriority,
JobType: row.JobType,
}
return &scheduledTask, nil
}
}
task, err := findTaskForWorker(ctx, queries, w)
scheduledTask, err := findTaskForWorker(ctx, queries, w)
switch {
case errors.Is(err, sql.ErrNoRows):
@ -107,10 +122,11 @@ func (db *DB) scheduleTask(ctx context.Context, queries *sqlc.Queries, w *Worker
}
// Assign the task to the worker.
assignmentTimestamp := db.nowNullable()
err = queries.AssignTaskToWorker(ctx, sqlc.AssignTaskToWorkerParams{
WorkerID: workerID,
Now: db.nowNullable(),
TaskID: task.ID,
TaskID: scheduledTask.Task.ID,
Now: assignmentTimestamp,
})
switch {
@ -119,32 +135,33 @@ func (db *DB) scheduleTask(ctx context.Context, queries *sqlc.Queries, w *Worker
return nil, errDatabaseBusy
case err != nil:
logger.Warn().
Str("taskID", task.UUID).
Str("taskID", scheduledTask.Task.UUID).
Err(err).
Msg("assigning task to worker")
return nil, fmt.Errorf("assigning task to worker: %w", err)
}
// Make sure the returned task matches the database.
task.WorkerID = workerID
scheduledTask.Task.WorkerID = workerID
scheduledTask.Task.UpdatedAt = assignmentTimestamp
logger.Info().
Str("taskID", task.UUID).
Str("taskID", scheduledTask.Task.UUID).
Msg("assigned task to worker")
return &task, nil
return scheduledTask, nil
}
func findTaskForWorker(
ctx context.Context,
queries *sqlc.Queries,
w *Worker,
) (sqlc.Task, error) {
) (*ScheduledTask, error) {
// Construct the list of worker tag IDs to check.
tags, err := queries.FetchTagsOfWorker(ctx, w.UUID)
if err != nil {
return sqlc.Task{}, err
return nil, err
}
workerTags := make([]sql.NullInt64, len(tags))
for index, tag := range tags {
@ -160,10 +177,17 @@ func findTaskForWorker(
WorkerTags: workerTags,
})
if err != nil {
return sqlc.Task{}, err
return nil, err
}
if row.Task.ID == 0 {
return sqlc.Task{}, nil
return nil, nil
}
return row.Task, nil
scheduledTask := ScheduledTask{
Task: row.Task,
JobUUID: row.JobUUID,
JobPriority: row.JobPriority,
JobType: row.JobType,
}
return &scheduledTask, nil
}

View File

@ -17,7 +17,7 @@ import (
"projects.blender.org/studio/flamenco/pkg/api"
)
const schedulerTestTimeout = 100 * time.Millisecond
const schedulerTestTimeout = 100 * time.Hour
const schedulerTestTimeoutlong = 5000 * time.Millisecond
func TestNoTasks(t *testing.T) {
@ -26,8 +26,8 @@ func TestNoTasks(t *testing.T) {
w := linuxWorker(t, db)
task, err := db.ScheduleTask(ctx, &w)
assert.Nil(t, task)
scheduledTask, err := db.ScheduleTask(ctx, &w)
assert.Nil(t, scheduledTask)
require.NoError(t, err)
}
@ -41,24 +41,25 @@ func TestOneJobOneTask(t *testing.T) {
atj := authorTestJob("b6a1d859-122f-4791-8b78-b943329a9989", "simple-blender-render", authTask)
job := constructTestJob(ctx, t, db, atj)
task, err := db.ScheduleTask(ctx, &w)
scheduledTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
// Check the returned task.
require.NotNil(t, task)
assert.Equal(t, job.ID, task.JobID)
require.NotNil(t, task.WorkerID, "no worker assigned to returned task")
assert.Equal(t, w.ID, int64(*task.WorkerID), "task must be assigned to the requesting worker")
require.NotNil(t, scheduledTask)
assert.Equal(t, job.ID, scheduledTask.Task.JobID)
require.True(t, scheduledTask.Task.WorkerID.Valid, "no worker assigned to returned task")
assert.Equal(t, w.ID, scheduledTask.Task.WorkerID.Int64, "task must be assigned to the requesting worker")
// Check the task in the database.
now := db.now()
dbTask, err := db.FetchTask(context.Background(), authTask.UUID)
taskJobWorker, err := db.FetchTask(context.Background(), authTask.UUID)
require.NoError(t, err)
require.NotNil(t, dbTask)
require.NotNil(t, dbTask.WorkerID, "no worker assigned to task in database")
require.NotNil(t, taskJobWorker)
require.True(t, taskJobWorker.Task.WorkerID.Valid, "no worker assigned to task in database")
require.NotZero(t, taskJobWorker.WorkerUUID, "no worker fetched from database even though assigned assigned to task")
assert.Equal(t, w.ID, int64(*dbTask.WorkerID), "task must be assigned to the requesting worker")
assert.WithinDuration(t, now, dbTask.LastTouchedAt, time.Second, "task must be 'touched' by the worker after scheduling")
assert.Equal(t, w.ID, taskJobWorker.Task.WorkerID.Int64, "task must be assigned to the requesting worker")
assert.WithinDuration(t, now, taskJobWorker.Task.LastTouchedAt.Time, time.Second, "task must be 'touched' by the worker after scheduling")
}
func TestOneJobThreeTasksByPrio(t *testing.T) {
@ -78,14 +79,14 @@ func TestOneJobThreeTasksByPrio(t *testing.T) {
job := constructTestJob(ctx, t, db, atj)
task, err := db.ScheduleTask(ctx, &w)
scheduledTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, task)
require.NotNil(t, scheduledTask)
assert.Equal(t, job.ID, task.JobID)
assert.NotNil(t, task.Job)
assert.Equal(t, job.ID, scheduledTask.Task.JobID)
assert.Equal(t, job.UUID, scheduledTask.JobUUID)
assert.Equal(t, att2.Name, task.Name, "the high-prio task should have been chosen")
assert.Equal(t, att2.Name, scheduledTask.Task.Name, "the high-prio task should have been chosen")
}
func TestOneJobThreeTasksByDependencies(t *testing.T) {
@ -105,11 +106,12 @@ func TestOneJobThreeTasksByDependencies(t *testing.T) {
att1, att2, att3)
job := constructTestJob(ctx, t, db, atj)
task, err := db.ScheduleTask(ctx, &w)
scheduledTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, task)
assert.Equal(t, job.ID, task.JobID)
assert.Equal(t, att1.Name, task.Name, "the first task should have been chosen")
require.NotNil(t, scheduledTask)
assert.Equal(t, job.ID, scheduledTask.Task.JobID)
assert.Equal(t, job.UUID, scheduledTask.JobUUID)
assert.Equal(t, att1.Name, scheduledTask.Task.Name, "the first task should have been chosen")
}
func TestTwoJobsThreeTasks(t *testing.T) {
@ -143,11 +145,11 @@ func TestTwoJobsThreeTasks(t *testing.T) {
constructTestJob(ctx, t, db, atj1)
job2 := constructTestJob(ctx, t, db, atj2)
task, err := db.ScheduleTask(ctx, &w)
scheduledTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, task)
assert.Equal(t, job2.ID, task.JobID)
assert.Equal(t, att2_3.Name, task.Name, "the 3rd task of the 2nd job should have been chosen")
require.NotNil(t, scheduledTask)
assert.Equal(t, job2.ID, scheduledTask.Task.JobID)
assert.Equal(t, att2_3.Name, scheduledTask.Task.Name, "the 3rd task of the 2nd job should have been chosen")
}
// TestFanOutFanIn tests one starting task, then multiple tasks that depend on
@ -190,14 +192,14 @@ func TestFanOutFanIn(t *testing.T) {
// Check the order in which tasks are handed out.
executionOrder := []string{} // Slice of task names.
for index := range 6 {
task, err := db.ScheduleTask(ctx, &w)
scheduledTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, task, "task #%d is nil", index)
executionOrder = append(executionOrder, task.Name)
require.NotNil(t, scheduledTask, "task #%d is nil", index)
executionOrder = append(executionOrder, scheduledTask.Task.Name)
// Fake that the task has been completed by the worker.
task.Status = api.TaskStatusCompleted
require.NoError(t, db.SaveTaskStatus(ctx, task))
scheduledTask.Task.Status = api.TaskStatusCompleted
require.NoError(t, db.SaveTaskStatus(ctx, &scheduledTask.Task))
}
expectedOrder := []string{
@ -230,10 +232,10 @@ func TestSomeButNotAllDependenciesCompleted(t *testing.T) {
setTaskStatus(t, db, att1.UUID, api.TaskStatusCompleted)
w := linuxWorker(t, db)
task, err := db.ScheduleTask(ctx, &w)
scheduledTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
if task != nil {
t.Fatalf("there should not be any task assigned, but received %q", task.Name)
if scheduledTask != nil {
t.Fatalf("there should not be any task assigned, but received %q", scheduledTask.Task.Name)
}
}
@ -259,16 +261,16 @@ func TestAlreadyAssigned(t *testing.T) {
// another, higher-prio task to be done.
dbTask3, err := db.FetchTask(ctx, att3.UUID)
require.NoError(t, err)
dbTask3.WorkerID = ptr(uint(w.ID))
dbTask3.Status = api.TaskStatusActive
err = db.SaveTask(ctx, dbTask3)
dbTask3.Task.WorkerID = sql.NullInt64{Int64: w.ID, Valid: true}
dbTask3.Task.Status = api.TaskStatusActive
err = db.SaveTask(ctx, &dbTask3.Task)
require.NoError(t, err)
task, err := db.ScheduleTask(ctx, &w)
scheduledTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, task)
require.NotNil(t, scheduledTask)
assert.Equal(t, att3.Name, task.Name, "the already-assigned task should have been chosen")
assert.Equal(t, att3.Name, scheduledTask.Task.Name, "the already-assigned task should have been chosen")
}
func TestAssignedToOtherWorker(t *testing.T) {
@ -292,17 +294,17 @@ func TestAssignedToOtherWorker(t *testing.T) {
// it shouldn't matter which worker it's assigned to.
dbTask2, err := db.FetchTask(ctx, att2.UUID)
require.NoError(t, err)
dbTask2.WorkerID = ptr(uint(w2.ID))
dbTask2.Status = api.TaskStatusQueued
err = db.SaveTask(ctx, dbTask2)
dbTask2.Task.WorkerID = sql.NullInt64{Int64: w2.ID, Valid: true}
dbTask2.Task.Status = api.TaskStatusQueued
err = db.SaveTask(ctx, &dbTask2.Task)
require.NoError(t, err)
task, err := db.ScheduleTask(ctx, &w)
scheduledTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, task)
require.NotNil(t, scheduledTask)
assert.Equal(t, att2.Name, task.Name, "the high-prio task should have been chosen")
assert.Equal(t, int64(*task.WorkerID), w.ID, "the task should now be assigned to the worker it was scheduled for")
assert.Equal(t, att2.Name, scheduledTask.Task.Name, "the high-prio task should have been chosen")
assert.Equal(t, w.ID, scheduledTask.Task.WorkerID.Int64, "the task should now be assigned to the worker it was scheduled for")
}
func TestPreviouslyFailed(t *testing.T) {
@ -320,17 +322,17 @@ func TestPreviouslyFailed(t *testing.T) {
job := constructTestJob(ctx, t, db, atj)
// Mimick that this worker already failed the first task.
tasks, err := db.FetchTasksOfJob(ctx, job)
taskJobWorkers, err := db.FetchTasksOfJob(ctx, job)
require.NoError(t, err)
numFailed, err := db.AddWorkerToTaskFailedList(ctx, tasks[0], &w)
numFailed, err := db.AddWorkerToTaskFailedList(ctx, &taskJobWorkers[0].Task, &w)
require.NoError(t, err)
assert.Equal(t, 1, numFailed)
// This should assign the 2nd task.
task, err := db.ScheduleTask(ctx, &w)
// This should assign the 2nd scheduledTask.
scheduledTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, task)
assert.Equal(t, att2.Name, task.Name, "the second task should have been chosen")
require.NotNil(t, scheduledTask)
assert.Equal(t, att2.Name, scheduledTask.Task.Name, "the second task should have been chosen")
}
func TestWorkerTagJobWithTag(t *testing.T) {
@ -369,14 +371,14 @@ func TestWorkerTagJobWithTag(t *testing.T) {
job.WorkerTagUUID = tag1.UUID
constructTestJob(ctx, t, db, job)
task, err := db.ScheduleTask(ctx, &workerC)
scheduledTask, err := db.ScheduleTask(ctx, &workerC)
require.NoError(t, err)
require.NotNil(t, task, "job with matching tag should be scheduled")
assert.Equal(t, authTask.UUID, task.UUID)
require.NotNil(t, scheduledTask, "job with matching tag should be scheduled")
assert.Equal(t, authTask.UUID, scheduledTask.Task.UUID)
task, err = db.ScheduleTask(ctx, &workerNC)
scheduledTask, err = db.ScheduleTask(ctx, &workerNC)
require.NoError(t, err)
assert.Nil(t, task, "job with tag should not be scheduled for worker without tag")
assert.Nil(t, scheduledTask, "job with tag should not be scheduled for worker without tag")
}
}
@ -402,15 +404,15 @@ func TestWorkerTagJobWithoutTag(t *testing.T) {
job := authorTestJob("b6a1d859-122f-4791-8b78-b943329a9989", "simple-blender-render", authTask)
constructTestJob(ctx, t, db, job)
task, err := db.ScheduleTask(ctx, &workerC)
scheduledTask, err := db.ScheduleTask(ctx, &workerC)
require.NoError(t, err)
require.NotNil(t, task, "job without tag should always be scheduled to worker in some tag")
assert.Equal(t, authTask.UUID, task.UUID)
require.NotNil(t, scheduledTask, "job without tag should always be scheduled to worker in some tag")
assert.Equal(t, authTask.UUID, scheduledTask.Task.UUID)
task, err = db.ScheduleTask(ctx, &workerNC)
scheduledTask, err = db.ScheduleTask(ctx, &workerNC)
require.NoError(t, err)
require.NotNil(t, task, "job without tag should always be scheduled to worker without tag")
assert.Equal(t, authTask.UUID, task.UUID)
require.NotNil(t, scheduledTask, "job without tag should always be scheduled to worker without tag")
assert.Equal(t, authTask.UUID, scheduledTask.Task.UUID)
}
func TestBlocklisted(t *testing.T) {
@ -428,14 +430,14 @@ func TestBlocklisted(t *testing.T) {
job := constructTestJob(ctx, t, db, atj)
// Mimick that this worker was already blocked for 'blender' tasks of this job.
err := db.AddWorkerToJobBlocklist(ctx, job, &w, "blender")
err := db.AddWorkerToJobBlocklist(ctx, job.ID, w.ID, "blender")
require.NoError(t, err)
// This should assign the 2nd task.
task, err := db.ScheduleTask(ctx, &w)
// This should assign the 2nd scheduledTask.
scheduledTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, task)
assert.Equal(t, att2.Name, task.Name, "the second task should have been chosen")
require.NotNil(t, scheduledTask)
assert.Equal(t, att2.Name, scheduledTask.Task.Name, "the second task should have been chosen")
}
// To test: blocklists
@ -486,12 +488,12 @@ func authorTestTask(name, taskType string, dependencies ...*job_compilers.Author
func setTaskStatus(t *testing.T, db *DB, taskUUID string, status api.TaskStatus) {
ctx := context.Background()
task, err := db.FetchTask(ctx, taskUUID)
taskJobWorker, err := db.FetchTask(ctx, taskUUID)
require.NoError(t, err)
task.Status = status
taskJobWorker.Task.Status = status
require.NoError(t, db.SaveTask(ctx, task))
require.NoError(t, db.SaveTask(ctx, &taskJobWorker.Task))
}
func linuxWorker(t *testing.T, db *DB, updaters ...func(worker *Worker)) Worker {

View File

@ -21,16 +21,16 @@ var workerStatusNoTimeout = []api.WorkerStatus{
api.WorkerStatusOffline,
}
type TimedOutTaskInfo = sqlc.FetchTimedOutTasksRow
// FetchTimedOutTasks returns a slice of tasks that have timed out.
//
// In order to time out, a task must be in status `active` and not touched by a
// Worker since `untouchedSince`.
//
// The returned tasks also have their `Job` and `Worker` fields set.
func (db *DB) FetchTimedOutTasks(ctx context.Context, untouchedSince time.Time) ([]*Task, error) {
func (db *DB) FetchTimedOutTasks(ctx context.Context, untouchedSince time.Time) ([]TimedOutTaskInfo, error) {
queries := db.queries()
sqlcTasks, err := queries.FetchTimedOutTasks(ctx, sqlc.FetchTimedOutTasksParams{
timedOut, err := queries.FetchTimedOutTasks(ctx, sqlc.FetchTimedOutTasksParams{
TaskStatus: api.TaskStatusActive,
UntouchedSince: sql.NullTime{Time: untouchedSince, Valid: true},
})
@ -38,17 +38,7 @@ func (db *DB) FetchTimedOutTasks(ctx context.Context, untouchedSince time.Time)
if err != nil {
return nil, taskError(err, "finding timed out tasks (untouched since %s)", untouchedSince.String())
}
result := make([]*Task, len(sqlcTasks))
for index, task := range sqlcTasks {
gormTask, err := convertSqlTaskWithJobAndWorker(ctx, queries, task)
if err != nil {
return nil, err
}
result[index] = gormTask
}
return result, nil
return timedOut, nil
}
func (db *DB) FetchTimedOutWorkers(ctx context.Context, lastSeenBefore time.Time) ([]*Worker, error) {

View File

@ -16,15 +16,15 @@ func TestFetchTimedOutTasks(t *testing.T) {
ctx, close, db, job, _ := jobTasksTestFixtures(t)
defer close()
tasks, err := db.FetchTasksOfJob(ctx, job)
tasksOfJob, err := db.FetchTasksOfJob(ctx, job)
require.NoError(t, err)
now := db.now()
deadline := now.Add(-5 * time.Minute)
// Mark the task as last touched before the deadline, i.e. old enough for a timeout.
task := tasks[0]
task.LastTouchedAt = deadline.Add(-1 * time.Minute)
task := &tasksOfJob[0].Task
task.LastTouchedAt = sql.NullTime{Time: deadline.Add(-1 * time.Minute), Valid: true}
require.NoError(t, db.SaveTask(ctx, task))
w := createWorker(ctx, t, db)
@ -45,9 +45,10 @@ func TestFetchTimedOutTasks(t *testing.T) {
if assert.Len(t, timedout, 1) {
// Other fields will be different, like the 'UpdatedAt' field -- this just
// tests that the expected task is returned.
assert.Equal(t, task.UUID, timedout[0].UUID)
assert.Equal(t, job, timedout[0].Job, "the job should be included in the result as well")
assert.Equal(t, w.UUID, timedout[0].Worker.UUID, "the worker should be included in the result as well")
assert.Equal(t, task.UUID, timedout[0].Task.UUID)
assert.Equal(t, job.UUID, timedout[0].JobUUID, "the job should be included in the result as well")
assert.Equal(t, w.UUID, timedout[0].WorkerUUID, "the worker UUID should be included in the result as well")
assert.Equal(t, w.Name, timedout[0].WorkerName, "the worker Name should be included in the result as well")
}
}

View File

@ -16,6 +16,7 @@ import (
// sent to the 'asleep' and 'awake' states.
type SleepSchedule = sqlc.SleepSchedule
// SleepScheduleOwned represents a sleep schedule + info about the worker that's controlled by it.
type SleepScheduleOwned struct {
SleepSchedule SleepSchedule
WorkerName string

View File

@ -54,6 +54,11 @@ func (db *DB) FetchWorkerTag(ctx context.Context, uuid string) (WorkerTag, error
return workerTag, nil
}
func (db *DB) FetchWorkerTagByID(ctx context.Context, id int64) (WorkerTag, error) {
queries := db.queries()
return fetchWorkerTagByID(ctx, queries, id)
}
// fetchWorkerTagByID fetches the worker tag using the given database instance.
func fetchWorkerTagByID(ctx context.Context, queries *sqlc.Queries, id int64) (WorkerTag, error) {
workerTag, err := queries.FetchWorkerTagByID(ctx, id)

View File

@ -99,7 +99,7 @@ func (db *DB) FetchWorkers(ctx context.Context) ([]*Worker, error) {
}
// FetchWorkerTask returns the most recent task assigned to the given Worker.
func (db *DB) FetchWorkerTask(ctx context.Context, worker *Worker) (*Task, error) {
func (db *DB) FetchWorkerTask(ctx context.Context, worker *Worker) (*TaskJob, error) {
queries := db.queries()
// Convert the WorkerID to a NullInt64. As task.worker_id can be NULL, this is
@ -119,27 +119,12 @@ func (db *DB) FetchWorkerTask(ctx context.Context, worker *Worker) (*Task, error
return nil, taskError(err, "fetching task assigned to Worker %s", worker.UUID)
}
// Found a task!
if row.Job.ID == 0 {
panic(fmt.Sprintf("task found but with no job: %#v", row))
taskJob := TaskJob{
Task: row.Task,
JobUUID: row.JobUUID,
IsActive: row.IsActive,
}
if row.Task.ID == 0 {
panic(fmt.Sprintf("task found but with zero ID: %#v", row))
}
// Convert the task & job to gorm data types.
gormTask, err := convertSqlcTask(row.Task, row.Job.UUID, worker.UUID)
if err != nil {
return nil, err
}
gormJob, err := convertSqlcJob(row.Job)
if err != nil {
return nil, err
}
gormTask.Job = &gormJob
gormTask.Worker = worker
return gormTask, nil
return &taskJob, nil
}
func (db *DB) SaveWorkerStatus(ctx context.Context, w *Worker) error {

View File

@ -95,26 +95,29 @@ func TestFetchWorkerTask(t *testing.T) {
assignedTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
require.Equal(t, assignedTask.UUID, authTask1.UUID)
require.Equal(t, authTask1.UUID, assignedTask.Task.UUID)
require.Equal(t, jobUUID, assignedTask.JobUUID)
require.Equal(t, atj.JobType, assignedTask.JobType)
require.Equal(t, atj.Priority, int(assignedTask.JobPriority))
{ // Assigned task should be returned.
foundTask, err := db.FetchWorkerTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, foundTask)
assert.Equal(t, assignedTask.UUID, foundTask.UUID)
assert.Equal(t, jobUUID, foundTask.Job.UUID, "the job UUID should be returned as well")
assert.Equal(t, assignedTask.Task.UUID, foundTask.Task.UUID)
assert.Equal(t, assignedTask.JobUUID, foundTask.JobUUID, "the job UUID should be returned as well")
}
// Set the task to 'completed'.
assignedTask.Status = api.TaskStatusCompleted
require.NoError(t, db.SaveTaskStatus(ctx, assignedTask))
assignedTask.Task.Status = api.TaskStatusCompleted
require.NoError(t, db.SaveTaskStatus(ctx, &assignedTask.Task))
{ // Completed-but-last-assigned task should be returned.
foundTask, err := db.FetchWorkerTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, foundTask)
assert.Equal(t, assignedTask.UUID, foundTask.UUID)
assert.Equal(t, jobUUID, foundTask.Job.UUID, "the job UUID should be returned as well")
assert.Equal(t, assignedTask.Task.UUID, foundTask.Task.UUID)
assert.Equal(t, jobUUID, foundTask.JobUUID, "the job UUID should be returned as well")
}
// Assign another task. Since the remainder of this test depends on the order
@ -125,26 +128,26 @@ func TestFetchWorkerTask(t *testing.T) {
newlyAssignedTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, newlyAssignedTask)
require.Equal(t, newlyAssignedTask.UUID, authTask2.UUID)
require.Equal(t, newlyAssignedTask.Task.UUID, authTask2.UUID)
{ // Newly assigned task should be returned.
foundTask, err := db.FetchWorkerTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, foundTask)
assert.Equal(t, newlyAssignedTask.UUID, foundTask.UUID)
assert.Equal(t, jobUUID, foundTask.Job.UUID, "the job UUID should be returned as well")
assert.Equal(t, newlyAssignedTask.Task.UUID, foundTask.Task.UUID)
assert.Equal(t, jobUUID, foundTask.JobUUID, "the job UUID should be returned as well")
}
// Set the new task to 'completed'.
newlyAssignedTask.Status = api.TaskStatusCompleted
require.NoError(t, db.SaveTaskStatus(ctx, newlyAssignedTask))
newlyAssignedTask.Task.Status = api.TaskStatusCompleted
require.NoError(t, db.SaveTaskStatus(ctx, &newlyAssignedTask.Task))
{ // Completed-but-last-assigned task should be returned.
foundTask, err := db.FetchWorkerTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, foundTask)
assert.Equal(t, newlyAssignedTask.UUID, foundTask.UUID)
assert.Equal(t, jobUUID, foundTask.Job.UUID, "the job UUID should be returned as well")
assert.Equal(t, newlyAssignedTask.Task.UUID, foundTask.Task.UUID)
assert.Equal(t, jobUUID, foundTask.JobUUID, "the job UUID should be returned as well")
}
}
@ -309,10 +312,9 @@ func TestDeleteWorker(t *testing.T) {
persistAuthoredJob(t, ctx, db, authJob)
taskUUID := authJob.Tasks[0].UUID
{
task, err := db.FetchTask(ctx, taskUUID)
taskJobWorker, err := db.FetchTask(ctx, taskUUID)
require.NoError(t, err)
task.Worker = &w1
require.NoError(t, db.SaveTask(ctx, task))
require.NoError(t, db.TaskAssignToWorker(ctx, &taskJobWorker.Task, &w1))
}
// Delete the worker.
@ -320,12 +322,11 @@ func TestDeleteWorker(t *testing.T) {
// Check the task after deletion of the Worker.
{
fetchedTask, err := db.FetchTask(ctx, taskUUID)
taskJobWorker, err := db.FetchTask(ctx, taskUUID)
require.NoError(t, err)
assert.Equal(t, taskUUID, fetchedTask.UUID)
assert.Equal(t, w1.UUID, fetchedTask.Worker.UUID)
assert.NotZero(t, fetchedTask.Worker.DeletedAt.Time)
assert.True(t, fetchedTask.Worker.DeletedAt.Valid)
assert.Equal(t, taskUUID, taskJobWorker.Task.UUID)
assert.Equal(t, w1.UUID, taskJobWorker.WorkerUUID)
assert.Equal(t, authJob.JobID, taskJobWorker.JobUUID)
}
}

View File

@ -37,20 +37,6 @@ func (m *MockPersistenceService) EXPECT() *MockPersistenceServiceMockRecorder {
return m.recorder
}
// CreateWorker mocks base method.
func (m *MockPersistenceService) CreateWorker(arg0 context.Context, arg1 *sqlc.Worker) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateWorker", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// CreateWorker indicates an expected call of CreateWorker.
func (mr *MockPersistenceServiceMockRecorder) CreateWorker(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWorker", reflect.TypeOf((*MockPersistenceService)(nil).CreateWorker), arg0, arg1)
}
// FetchSleepScheduleWorker mocks base method.
func (m *MockPersistenceService) FetchSleepScheduleWorker(arg0 context.Context, arg1 sqlc.SleepSchedule) (*sqlc.Worker, error) {
m.ctrl.T.Helper()

View File

@ -33,9 +33,11 @@ type PersistenceService interface {
UpdateJobsTaskStatusesConditional(ctx context.Context, job *persistence.Job,
statusesToUpdate []api.TaskStatus, taskStatus api.TaskStatus, activity string) error
FetchJob(ctx context.Context, jobUUID string) (*persistence.Job, error)
FetchJobByID(ctx context.Context, jobID int64) (*persistence.Job, error)
FetchJobsInStatus(ctx context.Context, jobStatuses ...api.JobStatus) ([]*persistence.Job, error)
FetchTasksOfWorkerInStatus(context.Context, *persistence.Worker, api.TaskStatus) ([]*persistence.Task, error)
FetchTasksOfWorkerInStatusOfJob(context.Context, *persistence.Worker, api.TaskStatus, *persistence.Job) ([]*persistence.Task, error)
FetchTasksOfWorkerInStatus(context.Context, *persistence.Worker, api.TaskStatus) ([]persistence.TaskJob, error)
FetchTasksOfWorkerInStatusOfJob(ctx context.Context, worker *persistence.Worker, status api.TaskStatus, jobUUID string) ([]*persistence.Task, error)
}
// PersistenceService should be a subset of persistence.DB

View File

@ -39,7 +39,7 @@ func (m *MockPersistenceService) EXPECT() *MockPersistenceServiceMockRecorder {
}
// CountTasksOfJobInStatus mocks base method.
func (m *MockPersistenceService) CountTasksOfJobInStatus(arg0 context.Context, arg1 *persistence.Job, arg2 ...api.TaskStatus) (int, int, error) {
func (m *MockPersistenceService) CountTasksOfJobInStatus(arg0 context.Context, arg1 *sqlc.Job, arg2 ...api.TaskStatus) (int, int, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
@ -59,15 +59,45 @@ func (mr *MockPersistenceServiceMockRecorder) CountTasksOfJobInStatus(arg0, arg1
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountTasksOfJobInStatus", reflect.TypeOf((*MockPersistenceService)(nil).CountTasksOfJobInStatus), varargs...)
}
// FetchJob mocks base method.
func (m *MockPersistenceService) FetchJob(arg0 context.Context, arg1 string) (*sqlc.Job, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchJob", arg0, arg1)
ret0, _ := ret[0].(*sqlc.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FetchJob indicates an expected call of FetchJob.
func (mr *MockPersistenceServiceMockRecorder) FetchJob(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchJob", reflect.TypeOf((*MockPersistenceService)(nil).FetchJob), arg0, arg1)
}
// FetchJobByID mocks base method.
func (m *MockPersistenceService) FetchJobByID(arg0 context.Context, arg1 int64) (*sqlc.Job, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchJobByID", arg0, arg1)
ret0, _ := ret[0].(*sqlc.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FetchJobByID indicates an expected call of FetchJobByID.
func (mr *MockPersistenceServiceMockRecorder) FetchJobByID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchJobByID", reflect.TypeOf((*MockPersistenceService)(nil).FetchJobByID), arg0, arg1)
}
// FetchJobsInStatus mocks base method.
func (m *MockPersistenceService) FetchJobsInStatus(arg0 context.Context, arg1 ...api.JobStatus) ([]*persistence.Job, error) {
func (m *MockPersistenceService) FetchJobsInStatus(arg0 context.Context, arg1 ...api.JobStatus) ([]*sqlc.Job, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "FetchJobsInStatus", varargs...)
ret0, _ := ret[0].([]*persistence.Job)
ret0, _ := ret[0].([]*sqlc.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -80,10 +110,10 @@ func (mr *MockPersistenceServiceMockRecorder) FetchJobsInStatus(arg0 interface{}
}
// FetchTasksOfWorkerInStatus mocks base method.
func (m *MockPersistenceService) FetchTasksOfWorkerInStatus(arg0 context.Context, arg1 *sqlc.Worker, arg2 api.TaskStatus) ([]*persistence.Task, error) {
func (m *MockPersistenceService) FetchTasksOfWorkerInStatus(arg0 context.Context, arg1 *sqlc.Worker, arg2 api.TaskStatus) ([]persistence.TaskJob, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchTasksOfWorkerInStatus", arg0, arg1, arg2)
ret0, _ := ret[0].([]*persistence.Task)
ret0, _ := ret[0].([]persistence.TaskJob)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -95,10 +125,10 @@ func (mr *MockPersistenceServiceMockRecorder) FetchTasksOfWorkerInStatus(arg0, a
}
// FetchTasksOfWorkerInStatusOfJob mocks base method.
func (m *MockPersistenceService) FetchTasksOfWorkerInStatusOfJob(arg0 context.Context, arg1 *sqlc.Worker, arg2 api.TaskStatus, arg3 *persistence.Job) ([]*persistence.Task, error) {
func (m *MockPersistenceService) FetchTasksOfWorkerInStatusOfJob(arg0 context.Context, arg1 *sqlc.Worker, arg2 api.TaskStatus, arg3 string) ([]*sqlc.Task, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchTasksOfWorkerInStatusOfJob", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].([]*persistence.Task)
ret0, _ := ret[0].([]*sqlc.Task)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -110,7 +140,7 @@ func (mr *MockPersistenceServiceMockRecorder) FetchTasksOfWorkerInStatusOfJob(ar
}
// JobHasTasksInStatus mocks base method.
func (m *MockPersistenceService) JobHasTasksInStatus(arg0 context.Context, arg1 *persistence.Job, arg2 api.TaskStatus) (bool, error) {
func (m *MockPersistenceService) JobHasTasksInStatus(arg0 context.Context, arg1 *sqlc.Job, arg2 api.TaskStatus) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JobHasTasksInStatus", arg0, arg1, arg2)
ret0, _ := ret[0].(bool)
@ -125,7 +155,7 @@ func (mr *MockPersistenceServiceMockRecorder) JobHasTasksInStatus(arg0, arg1, ar
}
// SaveJobStatus mocks base method.
func (m *MockPersistenceService) SaveJobStatus(arg0 context.Context, arg1 *persistence.Job) error {
func (m *MockPersistenceService) SaveJobStatus(arg0 context.Context, arg1 *sqlc.Job) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveJobStatus", arg0, arg1)
ret0, _ := ret[0].(error)
@ -139,7 +169,7 @@ func (mr *MockPersistenceServiceMockRecorder) SaveJobStatus(arg0, arg1 interface
}
// SaveTask mocks base method.
func (m *MockPersistenceService) SaveTask(arg0 context.Context, arg1 *persistence.Task) error {
func (m *MockPersistenceService) SaveTask(arg0 context.Context, arg1 *sqlc.Task) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveTask", arg0, arg1)
ret0, _ := ret[0].(error)
@ -153,7 +183,7 @@ func (mr *MockPersistenceServiceMockRecorder) SaveTask(arg0, arg1 interface{}) *
}
// SaveTaskActivity mocks base method.
func (m *MockPersistenceService) SaveTaskActivity(arg0 context.Context, arg1 *persistence.Task) error {
func (m *MockPersistenceService) SaveTaskActivity(arg0 context.Context, arg1 *sqlc.Task) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveTaskActivity", arg0, arg1)
ret0, _ := ret[0].(error)
@ -167,7 +197,7 @@ func (mr *MockPersistenceServiceMockRecorder) SaveTaskActivity(arg0, arg1 interf
}
// SaveTaskStatus mocks base method.
func (m *MockPersistenceService) SaveTaskStatus(arg0 context.Context, arg1 *persistence.Task) error {
func (m *MockPersistenceService) SaveTaskStatus(arg0 context.Context, arg1 *sqlc.Task) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveTaskStatus", arg0, arg1)
ret0, _ := ret[0].(error)
@ -181,7 +211,7 @@ func (mr *MockPersistenceServiceMockRecorder) SaveTaskStatus(arg0, arg1 interfac
}
// UpdateJobsTaskStatuses mocks base method.
func (m *MockPersistenceService) UpdateJobsTaskStatuses(arg0 context.Context, arg1 *persistence.Job, arg2 api.TaskStatus, arg3 string) error {
func (m *MockPersistenceService) UpdateJobsTaskStatuses(arg0 context.Context, arg1 *sqlc.Job, arg2 api.TaskStatus, arg3 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateJobsTaskStatuses", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(error)
@ -195,7 +225,7 @@ func (mr *MockPersistenceServiceMockRecorder) UpdateJobsTaskStatuses(arg0, arg1,
}
// UpdateJobsTaskStatusesConditional mocks base method.
func (m *MockPersistenceService) UpdateJobsTaskStatusesConditional(arg0 context.Context, arg1 *persistence.Job, arg2 []api.TaskStatus, arg3 api.TaskStatus, arg4 string) error {
func (m *MockPersistenceService) UpdateJobsTaskStatusesConditional(arg0 context.Context, arg1 *sqlc.Job, arg2 []api.TaskStatus, arg3 api.TaskStatus, arg4 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateJobsTaskStatusesConditional", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(error)

View File

@ -40,13 +40,23 @@ func (sm *StateMachine) TaskStatusChange(
task *persistence.Task,
newTaskStatus api.TaskStatus,
) error {
if task.JobID == 0 {
log.Panic().Str("task", task.UUID).Msg("task without job ID, cannot handle this")
return nil // Will not run because of the panic.
}
job, err := sm.persist.FetchJobByID(ctx, task.JobID)
if err != nil {
return fmt.Errorf("cannot fetch the job of task %s: %w", task.UUID, err)
}
oldTaskStatus := task.Status
if err := sm.taskStatusChangeOnly(ctx, task, newTaskStatus); err != nil {
if err := sm.taskStatusChangeOnly(ctx, task, job, newTaskStatus); err != nil {
return err
}
if err := sm.updateJobAfterTaskStatusChange(ctx, task, oldTaskStatus); err != nil {
if err := sm.updateJobAfterTaskStatusChange(ctx, task, job, oldTaskStatus); err != nil {
return fmt.Errorf("updating job after task status change: %w", err)
}
return nil
@ -57,19 +67,16 @@ func (sm *StateMachine) TaskStatusChange(
func (sm *StateMachine) taskStatusChangeOnly(
ctx context.Context,
task *persistence.Task,
job *persistence.Job,
newTaskStatus api.TaskStatus,
) error {
if task.JobUUID == "" {
log.Panic().Str("task", task.UUID).Msg("task without job UUID, cannot handle this")
return nil // Will not run because of the panic.
}
oldTaskStatus := task.Status
task.Status = newTaskStatus
logger := log.With().
Str("task", task.UUID).
Str("job", task.JobUUID).
Str("job", job.UUID).
Str("taskStatusOld", string(oldTaskStatus)).
Str("taskStatusNew", string(newTaskStatus)).
Logger()
@ -82,12 +89,12 @@ func (sm *StateMachine) taskStatusChangeOnly(
if oldTaskStatus != newTaskStatus {
// logStorage already logs any error, and an error here shouldn't block the
// rest of the function.
_ = sm.logStorage.WriteTimestamped(logger, task.JobUUID, task.UUID,
_ = sm.logStorage.WriteTimestamped(logger, job.UUID, task.UUID,
fmt.Sprintf("task changed status %s -> %s", oldTaskStatus, newTaskStatus))
}
// Broadcast this change to the SocketIO clients.
taskUpdate := eventbus.NewTaskUpdate(task)
taskUpdate := eventbus.NewTaskUpdate(*task, job.UUID)
taskUpdate.PreviousStatus = &oldTaskStatus
sm.broadcaster.BroadcastTaskUpdate(taskUpdate)
@ -97,16 +104,18 @@ func (sm *StateMachine) taskStatusChangeOnly(
// updateJobAfterTaskStatusChange updates the job status based on the status of
// this task and other tasks in the job.
func (sm *StateMachine) updateJobAfterTaskStatusChange(
ctx context.Context, task *persistence.Task, oldTaskStatus api.TaskStatus,
ctx context.Context,
task *persistence.Task,
job *persistence.Job,
oldTaskStatus api.TaskStatus,
) error {
job := task.Job
if job == nil {
log.Panic().Str("task", task.UUID).Msg("task without job, cannot handle this")
return nil // Will not run because of the panic.
}
logger := log.With().
Str("job", task.JobUUID).
Str("job", job.UUID).
Str("task", task.UUID).
Str("taskStatusOld", string(oldTaskStatus)).
Str("taskStatusNew", string(task.Status)).
@ -136,7 +145,7 @@ func (sm *StateMachine) updateJobAfterTaskStatusChange(
default:
logger.Info().Msg("job became active because one of its task changed status")
reason := fmt.Sprintf("task became %s", task.Status)
return sm.JobStatusChange(ctx, job, api.JobStatusActive, reason)
return sm.jobStatusChange(ctx, job, api.JobStatusActive, reason)
}
case api.TaskStatusCompleted:
@ -163,7 +172,7 @@ func (sm *StateMachine) jobStatusIfAThenB(
Str("jobStatusOld", string(ifStatus)).
Str("jobStatusNew", string(thenStatus)).
Msg("Job will change status because one of its task changed status")
return sm.JobStatusChange(ctx, job, thenStatus, reason)
return sm.jobStatusChange(ctx, job, thenStatus, reason)
}
// isJobPausingComplete returns true when the job status is pause-requested and there are no more active tasks.
@ -189,7 +198,7 @@ func (sm *StateMachine) updateJobOnTaskStatusCanceled(ctx context.Context, logge
if numRunnable == 0 {
// NOTE: this does NOT cancel any non-runnable (paused/failed) tasks. If that's desired, just cancel the job as a whole.
logger.Info().Msg("canceled task was last runnable task of job, canceling job")
return sm.JobStatusChange(ctx, job, api.JobStatusCanceled, "canceled task was last runnable task of job, canceling job")
return sm.jobStatusChange(ctx, job, api.JobStatusCanceled, "canceled task was last runnable task of job, canceling job")
}
// Deal with the special case when the job is in pause-requested status.
@ -198,7 +207,7 @@ func (sm *StateMachine) updateJobOnTaskStatusCanceled(ctx context.Context, logge
return err
}
if toBePaused {
return sm.JobStatusChange(ctx, job, api.JobStatusPaused, "no more active tasks after task cancellation")
return sm.jobStatusChange(ctx, job, api.JobStatusPaused, "no more active tasks after task cancellation")
}
return nil
@ -221,7 +230,7 @@ func (sm *StateMachine) updateJobOnTaskStatusFailed(ctx context.Context, logger
if failedPercentage >= taskFailJobPercentage {
failLogger.Info().Msg("failing job because too many of its tasks failed")
return sm.JobStatusChange(ctx, job, api.JobStatusFailed, "too many tasks failed")
return sm.jobStatusChange(ctx, job, api.JobStatusFailed, "too many tasks failed")
}
// If the job didn't fail, this failure indicates that at least the job is active.
failLogger.Info().Msg("task failed, but not enough to fail the job")
@ -232,7 +241,7 @@ func (sm *StateMachine) updateJobOnTaskStatusFailed(ctx context.Context, logger
return err
}
if toBePaused {
return sm.JobStatusChange(ctx, job, api.JobStatusPaused, "no more active tasks after task failure")
return sm.jobStatusChange(ctx, job, api.JobStatusPaused, "no more active tasks after task failure")
}
return sm.jobStatusIfAThenB(ctx, logger, job, api.JobStatusQueued, api.JobStatusActive,
@ -247,7 +256,7 @@ func (sm *StateMachine) updateJobOnTaskStatusCompleted(ctx context.Context, logg
}
if numComplete == numTotal {
logger.Info().Msg("all tasks of job are completed, job is completed")
return sm.JobStatusChange(ctx, job, api.JobStatusCompleted, "all tasks completed")
return sm.jobStatusChange(ctx, job, api.JobStatusCompleted, "all tasks completed")
}
// Deal with the special case when the job is in pause-requested status.
@ -256,7 +265,7 @@ func (sm *StateMachine) updateJobOnTaskStatusCompleted(ctx context.Context, logg
return err
}
if toBePaused {
return sm.JobStatusChange(ctx, job, api.JobStatusPaused, "no more active tasks after task completion")
return sm.jobStatusChange(ctx, job, api.JobStatusPaused, "no more active tasks after task completion")
}
logger.Info().
@ -268,6 +277,23 @@ func (sm *StateMachine) updateJobOnTaskStatusCompleted(ctx context.Context, logg
// JobStatusChange gives a Job a new status, and handles the resulting status changes on its tasks.
func (sm *StateMachine) JobStatusChange(
ctx context.Context,
jobUUID string,
newJobStatus api.JobStatus,
reason string,
) error {
job, err := sm.persist.FetchJob(ctx, jobUUID)
if err != nil {
return err
}
return sm.jobStatusChange(ctx, job, newJobStatus, reason)
}
// jobStatusChange gives a Job a new status, and handles the resulting status changes on its tasks.
//
// This is the private implementation, which takes the job as an argument. The
// public function (above) takes a job's UUID instead, so that it's easier to call.
func (sm *StateMachine) jobStatusChange(
ctx context.Context,
job *persistence.Job,
newJobStatus api.JobStatus,
@ -369,7 +395,7 @@ func (sm *StateMachine) jobStatusSet(ctx context.Context,
}
// Handle the status change.
result, err := sm.updateTasksAfterJobStatusChange(ctx, logger, job, oldJobStatus)
result, err := sm.updateTasksAfterjobStatusChange(ctx, logger, job, oldJobStatus)
if err != nil {
return "", fmt.Errorf("updating job's tasks after job status change: %w", err)
}
@ -383,7 +409,7 @@ func (sm *StateMachine) jobStatusSet(ctx context.Context,
return result.followingJobStatus, nil
}
// tasksUpdateResult is returned by `updateTasksAfterJobStatusChange`.
// tasksUpdateResult is returned by `updateTasksAfterjobStatusChange`.
type tasksUpdateResult struct {
// FollowingJobStatus is set when the task updates should trigger another job status update.
followingJobStatus api.JobStatus
@ -394,14 +420,14 @@ type tasksUpdateResult struct {
massTaskUpdate bool
}
// updateTasksAfterJobStatusChange updates the status of its tasks based on the
// updateTasksAfterjobStatusChange updates the status of its tasks based on the
// new status of this job.
//
// NOTE: this function assumes that the job already has its new status.
//
// Returns the new state the job should go into after this change, or an empty
// string if there is no subsequent change necessary.
func (sm *StateMachine) updateTasksAfterJobStatusChange(
func (sm *StateMachine) updateTasksAfterjobStatusChange(
ctx context.Context,
logger zerolog.Logger,
job *persistence.Job,

View File

@ -31,13 +31,15 @@ func TestTaskStatusChangeQueuedToActive(t *testing.T) {
defer mockCtrl.Finish()
// T: queued > active --> J: queued > active
task := taskWithStatus(api.JobStatusQueued, api.TaskStatusQueued)
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusActive)
mocks.expectWriteTaskLogTimestamped(t, task, "task changed status queued -> active")
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusActive)
mocks.expectBroadcastJobChange(task.Job, api.JobStatusQueued, api.JobStatusActive)
mocks.expectBroadcastTaskChange(task, api.TaskStatusQueued, api.TaskStatusActive)
task, job := taskWithStatus(api.JobStatusQueued, api.TaskStatusQueued)
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusActive)
mocks.expectWriteTaskLogTimestamped(t, task, job.UUID, "task changed status queued -> active")
mocks.expectSaveJobWithStatus(t, job, api.JobStatusActive)
mocks.expectBroadcastJobChange(job, api.JobStatusQueued, api.JobStatusActive)
mocks.expectBroadcastTaskChange(task, job.UUID, api.TaskStatusQueued, api.TaskStatusActive)
mocks.expectFetchJobOfTask(task, job)
require.NoError(t, sm.TaskStatusChange(ctx, task, api.TaskStatusActive))
}
@ -46,17 +48,18 @@ func TestTaskStatusChangeSaveTaskAfterJobChangeFailure(t *testing.T) {
defer mockCtrl.Finish()
// A task status change should be saved, even when triggering the job change errors somehow.
task := taskWithStatus(api.JobStatusQueued, api.TaskStatusQueued)
task, job := taskWithStatus(api.JobStatusQueued, api.TaskStatusQueued)
jobSaveErr := errors.New("hypothetical job save error")
mocks.persist.EXPECT().
SaveJobStatus(gomock.Any(), task.Job).
SaveJobStatus(gomock.Any(), job).
Return(jobSaveErr)
// Expect a call to save the task in the persistence layer, regardless of the above error.
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusActive)
mocks.expectWriteTaskLogTimestamped(t, task, "task changed status queued -> active")
mocks.expectBroadcastTaskChange(task, api.TaskStatusQueued, api.TaskStatusActive)
mocks.expectWriteTaskLogTimestamped(t, task, job.UUID, "task changed status queued -> active")
mocks.expectBroadcastTaskChange(task, job.UUID, api.TaskStatusQueued, api.TaskStatusActive)
mocks.expectFetchJobOfTask(task, job)
returnedErr := sm.TaskStatusChange(ctx, task, api.TaskStatusActive)
assert.ErrorIs(t, returnedErr, jobSaveErr, "the returned error should wrap the persistence layer error")
@ -67,37 +70,42 @@ func TestTaskStatusChangeActiveToCompleted(t *testing.T) {
defer mockCtrl.Finish()
// Job has three tasks.
task := taskWithStatus(api.JobStatusActive, api.TaskStatusActive)
task2 := taskOfSameJob(task, api.TaskStatusActive)
task3 := taskOfSameJob(task, api.TaskStatusActive)
task1, job := taskWithStatus(api.JobStatusActive, api.TaskStatusActive)
task2 := taskOfSameJob(task1, api.TaskStatusActive)
task3 := taskOfSameJob(task1, api.TaskStatusActive)
// First task completing: T: active > completed --> J: active > active
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusCompleted)
mocks.expectWriteTaskLogTimestamped(t, task, "task changed status active -> completed")
mocks.expectBroadcastTaskChange(task, api.TaskStatusActive, api.TaskStatusCompleted)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, task.Job, api.TaskStatusCompleted).Return(1, 3, nil) // 1 of 3 complete.
require.NoError(t, sm.TaskStatusChange(ctx, task, api.TaskStatusCompleted))
mocks.expectSaveTaskWithStatus(t, task1, api.TaskStatusCompleted)
mocks.expectWriteTaskLogTimestamped(t, task1, job.UUID, "task changed status active -> completed")
mocks.expectBroadcastTaskChange(task1, job.UUID, api.TaskStatusActive, api.TaskStatusCompleted)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, job, api.TaskStatusCompleted).Return(1, 3, nil) // 1 of 3 complete.
mocks.expectFetchJobOfTask(task1, job)
require.NoError(t, sm.TaskStatusChange(ctx, task1, api.TaskStatusCompleted))
// Second task hickup: T: active > soft-failed --> J: active > active
mocks.expectSaveTaskWithStatus(t, task2, api.TaskStatusSoftFailed)
mocks.expectWriteTaskLogTimestamped(t, task2, "task changed status active -> soft-failed")
mocks.expectBroadcastTaskChange(task2, api.TaskStatusActive, api.TaskStatusSoftFailed)
mocks.expectWriteTaskLogTimestamped(t, task2, job.UUID, "task changed status active -> soft-failed")
mocks.expectBroadcastTaskChange(task2, job.UUID, api.TaskStatusActive, api.TaskStatusSoftFailed)
mocks.expectFetchJobOfTask(task2, job)
require.NoError(t, sm.TaskStatusChange(ctx, task2, api.TaskStatusSoftFailed))
// Second task completing: T: soft-failed > completed --> J: active > active
mocks.expectSaveTaskWithStatus(t, task2, api.TaskStatusCompleted)
mocks.expectWriteTaskLogTimestamped(t, task2, "task changed status soft-failed -> completed")
mocks.expectBroadcastTaskChange(task2, api.TaskStatusSoftFailed, api.TaskStatusCompleted)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, task.Job, api.TaskStatusCompleted).Return(2, 3, nil) // 2 of 3 complete.
mocks.expectWriteTaskLogTimestamped(t, task2, job.UUID, "task changed status soft-failed -> completed")
mocks.expectBroadcastTaskChange(task2, job.UUID, api.TaskStatusSoftFailed, api.TaskStatusCompleted)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, job, api.TaskStatusCompleted).Return(2, 3, nil) // 2 of 3 complete.
mocks.expectFetchJobOfTask(task2, job)
require.NoError(t, sm.TaskStatusChange(ctx, task2, api.TaskStatusCompleted))
// Third task completing: T: active > completed --> J: active > completed
mocks.expectSaveTaskWithStatus(t, task3, api.TaskStatusCompleted)
mocks.expectWriteTaskLogTimestamped(t, task3, "task changed status active -> completed")
mocks.expectBroadcastTaskChange(task3, api.TaskStatusActive, api.TaskStatusCompleted)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, task.Job, api.TaskStatusCompleted).Return(3, 3, nil) // 3 of 3 complete.
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusCompleted)
mocks.expectBroadcastJobChange(task.Job, api.JobStatusActive, api.JobStatusCompleted)
mocks.expectWriteTaskLogTimestamped(t, task3, job.UUID, "task changed status active -> completed")
mocks.expectBroadcastTaskChange(task3, job.UUID, api.TaskStatusActive, api.TaskStatusCompleted)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, job, api.TaskStatusCompleted).Return(3, 3, nil) // 3 of 3 complete.
mocks.expectSaveJobWithStatus(t, job, api.JobStatusCompleted)
mocks.expectBroadcastJobChange(job, api.JobStatusActive, api.JobStatusCompleted)
mocks.expectFetchJobOfTask(task3, job)
require.NoError(t, sm.TaskStatusChange(ctx, task3, api.TaskStatusCompleted))
}
@ -107,13 +115,15 @@ func TestTaskStatusChangeQueuedToFailed(t *testing.T) {
defer mockCtrl.Finish()
// T: queued > failed (1% task failure) --> J: queued > active
task := taskWithStatus(api.JobStatusQueued, api.TaskStatusQueued)
task, job := taskWithStatus(api.JobStatusQueued, api.TaskStatusQueued)
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusFailed)
mocks.expectWriteTaskLogTimestamped(t, task, "task changed status queued -> failed")
mocks.expectBroadcastTaskChange(task, api.TaskStatusQueued, api.TaskStatusFailed)
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusActive)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, task.Job, api.TaskStatusFailed).Return(1, 100, nil) // 1 out of 100 failed.
mocks.expectBroadcastJobChange(task.Job, api.JobStatusQueued, api.JobStatusActive)
mocks.expectWriteTaskLogTimestamped(t, task, job.UUID, "task changed status queued -> failed")
mocks.expectBroadcastTaskChange(task, job.UUID, api.TaskStatusQueued, api.TaskStatusFailed)
mocks.expectSaveJobWithStatus(t, job, api.JobStatusActive)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, job, api.TaskStatusFailed).Return(1, 100, nil) // 1 out of 100 failed.
mocks.expectBroadcastJobChange(job, api.JobStatusQueued, api.JobStatusActive)
mocks.expectFetchJobOfTask(task, job)
require.NoError(t, sm.TaskStatusChange(ctx, task, api.TaskStatusFailed))
}
@ -123,16 +133,16 @@ func TestTaskStatusChangeActiveToFailedFailJob(t *testing.T) {
defer mockCtrl.Finish()
// T: active > failed (10% task1 failure) --> J: active > failed + cancellation of any runnable tasks.
task1 := taskWithStatus(api.JobStatusActive, api.TaskStatusActive)
task1, job := taskWithStatus(api.JobStatusActive, api.TaskStatusActive)
mocks.expectSaveTaskWithStatus(t, task1, api.TaskStatusFailed)
mocks.expectWriteTaskLogTimestamped(t, task1, "task changed status active -> failed")
mocks.expectWriteTaskLogTimestamped(t, task1, job.UUID, "task changed status active -> failed")
// The change to the failed task should be broadcast.
mocks.expectBroadcastTaskChange(task1, api.TaskStatusActive, api.TaskStatusFailed)
mocks.expectSaveJobWithStatus(t, task1.Job, api.JobStatusFailed)
mocks.expectBroadcastTaskChange(task1, job.UUID, api.TaskStatusActive, api.TaskStatusFailed)
mocks.expectSaveJobWithStatus(t, job, api.JobStatusFailed)
// The resulting cancellation of the other tasks should be communicated as mass-task-update in the job update broadcast.
mocks.expectBroadcastJobChangeWithTaskRefresh(task1.Job, api.JobStatusActive, api.JobStatusFailed)
mocks.expectBroadcastJobChangeWithTaskRefresh(job, api.JobStatusActive, api.JobStatusFailed)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, task1.Job, api.TaskStatusFailed).Return(10, 100, nil) // 10 out of 100 failed.
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, job, api.TaskStatusFailed).Return(10, 100, nil) // 10 out of 100 failed.
// Expect failure of the job to trigger cancellation of remaining tasks.
taskStatusesToCancel := []api.TaskStatus{
@ -141,9 +151,10 @@ func TestTaskStatusChangeActiveToFailedFailJob(t *testing.T) {
api.TaskStatusSoftFailed,
}
mocks.persist.EXPECT().UpdateJobsTaskStatusesConditional(ctx, task1.Job, taskStatusesToCancel, api.TaskStatusCanceled,
mocks.persist.EXPECT().UpdateJobsTaskStatusesConditional(ctx, job, taskStatusesToCancel, api.TaskStatusCanceled,
"Manager cancelled this task because the job got status \"failed\".",
)
mocks.expectFetchJobOfTask(task1, job)
require.NoError(t, sm.TaskStatusChange(ctx, task1, api.TaskStatusFailed))
}
@ -153,21 +164,23 @@ func TestTaskStatusChangeRequeueOnCompletedJob(t *testing.T) {
defer mockCtrl.Finish()
// T: completed > queued --> J: completed > requeueing > queued
task := taskWithStatus(api.JobStatusCompleted, api.TaskStatusCompleted)
task, job := taskWithStatus(api.JobStatusCompleted, api.TaskStatusCompleted)
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusQueued)
mocks.expectWriteTaskLogTimestamped(t, task, "task changed status completed -> queued")
mocks.expectBroadcastTaskChange(task, api.TaskStatusCompleted, api.TaskStatusQueued)
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusRequeueing)
mocks.expectBroadcastJobChangeWithTaskRefresh(task.Job, api.JobStatusCompleted, api.JobStatusRequeueing)
mocks.expectBroadcastJobChangeWithTaskRefresh(task.Job, api.JobStatusRequeueing, api.JobStatusQueued)
mocks.expectWriteTaskLogTimestamped(t, task, job.UUID, "task changed status completed -> queued")
mocks.expectBroadcastTaskChange(task, job.UUID, api.TaskStatusCompleted, api.TaskStatusQueued)
mocks.expectSaveJobWithStatus(t, job, api.JobStatusRequeueing)
mocks.expectBroadcastJobChangeWithTaskRefresh(job, api.JobStatusCompleted, api.JobStatusRequeueing)
mocks.expectBroadcastJobChangeWithTaskRefresh(job, api.JobStatusRequeueing, api.JobStatusQueued)
// Expect queueing of the job to trigger queueing of all its tasks, if those tasks were all completed before.
// 2 out of 3 completed, because one was just queued.
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, task.Job, api.TaskStatusCompleted).Return(2, 3, nil)
mocks.persist.EXPECT().UpdateJobsTaskStatuses(ctx, task.Job, api.TaskStatusQueued,
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, job, api.TaskStatusCompleted).Return(2, 3, nil)
mocks.persist.EXPECT().UpdateJobsTaskStatuses(ctx, job, api.TaskStatusQueued,
"Queued because job transitioned status from \"completed\" to \"requeueing\"",
)
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusQueued)
mocks.expectSaveJobWithStatus(t, job, api.JobStatusQueued)
mocks.expectFetchJobOfTask(task, job)
require.NoError(t, sm.TaskStatusChange(ctx, task, api.TaskStatusQueued))
}
@ -176,28 +189,29 @@ func TestTaskStatusChangeCancelSingleTask(t *testing.T) {
mockCtrl, ctx, sm, mocks := taskStateMachineTestFixtures(t)
defer mockCtrl.Finish()
task := taskWithStatus(api.JobStatusCancelRequested, api.TaskStatusActive)
task, job := taskWithStatus(api.JobStatusCancelRequested, api.TaskStatusActive)
task2 := taskOfSameJob(task, api.TaskStatusQueued)
job := task.Job
// T1: active > cancelled --> J: cancel-requested > cancel-requested
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusCanceled)
mocks.expectWriteTaskLogTimestamped(t, task, "task changed status active -> canceled")
mocks.expectBroadcastTaskChange(task, api.TaskStatusActive, api.TaskStatusCanceled)
mocks.expectWriteTaskLogTimestamped(t, task, job.UUID, "task changed status active -> canceled")
mocks.expectBroadcastTaskChange(task, job.UUID, api.TaskStatusActive, api.TaskStatusCanceled)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, job,
api.TaskStatusActive, api.TaskStatusQueued, api.TaskStatusSoftFailed).
Return(1, 2, nil)
mocks.expectFetchJobOfTask(task, job)
require.NoError(t, sm.TaskStatusChange(ctx, task, api.TaskStatusCanceled))
// T2: queued > cancelled --> J: cancel-requested > canceled
mocks.expectSaveTaskWithStatus(t, task2, api.TaskStatusCanceled)
mocks.expectWriteTaskLogTimestamped(t, task2, "task changed status queued -> canceled")
mocks.expectBroadcastTaskChange(task2, api.TaskStatusQueued, api.TaskStatusCanceled)
mocks.expectWriteTaskLogTimestamped(t, task2, job.UUID, "task changed status queued -> canceled")
mocks.expectBroadcastTaskChange(task2, job.UUID, api.TaskStatusQueued, api.TaskStatusCanceled)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, job,
api.TaskStatusActive, api.TaskStatusQueued, api.TaskStatusSoftFailed).
Return(0, 2, nil)
mocks.expectSaveJobWithStatus(t, job, api.JobStatusCanceled)
mocks.expectBroadcastJobChange(task.Job, api.JobStatusCancelRequested, api.JobStatusCanceled)
mocks.expectBroadcastJobChange(job, api.JobStatusCancelRequested, api.JobStatusCanceled)
mocks.expectFetchJobOfTask(task2, job)
require.NoError(t, sm.TaskStatusChange(ctx, task2, api.TaskStatusCanceled))
}
@ -206,20 +220,20 @@ func TestTaskStatusChangeCancelSingleTaskWithOtherFailed(t *testing.T) {
mockCtrl, ctx, sm, mocks := taskStateMachineTestFixtures(t)
defer mockCtrl.Finish()
task1 := taskWithStatus(api.JobStatusCancelRequested, api.TaskStatusActive)
task1, job := taskWithStatus(api.JobStatusCancelRequested, api.TaskStatusActive)
task2 := taskOfSameJob(task1, api.TaskStatusFailed)
taskOfSameJob(task2, api.TaskStatusPaused)
job := task1.Job
// T1: active > cancelled --> J: cancel-requested > canceled because T2 already failed and cannot run anyway.
mocks.expectSaveTaskWithStatus(t, task1, api.TaskStatusCanceled)
mocks.expectWriteTaskLogTimestamped(t, task1, "task changed status active -> canceled")
mocks.expectBroadcastTaskChange(task1, api.TaskStatusActive, api.TaskStatusCanceled)
mocks.expectWriteTaskLogTimestamped(t, task1, job.UUID, "task changed status active -> canceled")
mocks.expectBroadcastTaskChange(task1, job.UUID, api.TaskStatusActive, api.TaskStatusCanceled)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, job,
api.TaskStatusActive, api.TaskStatusQueued, api.TaskStatusSoftFailed).
Return(0, 3, nil)
mocks.expectSaveJobWithStatus(t, job, api.JobStatusCanceled)
mocks.expectBroadcastJobChange(task1.Job, api.JobStatusCancelRequested, api.JobStatusCanceled)
mocks.expectBroadcastJobChange(job, api.JobStatusCancelRequested, api.JobStatusCanceled)
mocks.expectFetchJobOfTask(task1, job)
// The canceled task just stays canceled, so don't expectBroadcastTaskChange(task3).
@ -231,10 +245,12 @@ func TestTaskStatusChangeUnknownStatus(t *testing.T) {
defer mockCtrl.Finish()
// T: queued > borked --> saved to DB but otherwise ignored w.r.t. job status changes.
task := taskWithStatus(api.JobStatusQueued, api.TaskStatusQueued)
task, job := taskWithStatus(api.JobStatusQueued, api.TaskStatusQueued)
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatus("borked"))
mocks.expectWriteTaskLogTimestamped(t, task, "task changed status queued -> borked")
mocks.expectBroadcastTaskChange(task, api.TaskStatusQueued, api.TaskStatus("borked"))
mocks.expectWriteTaskLogTimestamped(t, task, job.UUID, "task changed status queued -> borked")
mocks.expectBroadcastTaskChange(task, job.UUID, api.TaskStatusQueued, api.TaskStatus("borked"))
mocks.expectFetchJobOfTask(task, job)
require.NoError(t, sm.TaskStatusChange(ctx, task, api.TaskStatus("borked")))
}
@ -243,12 +259,11 @@ func TestJobRequeueWithSomeCompletedTasks(t *testing.T) {
mockCtrl, ctx, sm, mocks := taskStateMachineTestFixtures(t)
defer mockCtrl.Finish()
task1 := taskWithStatus(api.JobStatusActive, api.TaskStatusCompleted)
_, job := taskWithStatus(api.JobStatusActive, api.TaskStatusCompleted)
// These are not necessary to create for this test, but just imagine these tasks are there too.
// This is mimicked by returning (1, 3, nil) when counting the tasks (1 of 3 completed).
// task2 := taskOfSameJob(task1, api.TaskStatusFailed)
// task3 := taskOfSameJob(task2, api.TaskStatusSoftFailed)
job := task1.Job
mocks.expectSaveJobWithStatus(t, job, api.JobStatusRequeueing)
@ -270,19 +285,18 @@ func TestJobRequeueWithSomeCompletedTasks(t *testing.T) {
mocks.expectBroadcastJobChangeWithTaskRefresh(job, api.JobStatusActive, api.JobStatusRequeueing)
mocks.expectBroadcastJobChangeWithTaskRefresh(job, api.JobStatusRequeueing, api.JobStatusQueued)
require.NoError(t, sm.JobStatusChange(ctx, job, api.JobStatusRequeueing, "someone wrote a unittest"))
require.NoError(t, sm.jobStatusChange(ctx, job, api.JobStatusRequeueing, "someone wrote a unittest"))
}
func TestJobRequeueWithAllCompletedTasks(t *testing.T) {
mockCtrl, ctx, sm, mocks := taskStateMachineTestFixtures(t)
defer mockCtrl.Finish()
task1 := taskWithStatus(api.JobStatusCompleted, api.TaskStatusCompleted)
_, job := taskWithStatus(api.JobStatusCompleted, api.TaskStatusCompleted)
// These are not necessary to create for this test, but just imagine these tasks are there too.
// This is mimicked by returning (3, 3, nil) when counting the tasks (3 of 3 completed).
// task2 := taskOfSameJob(task1, api.TaskStatusCompleted)
// task3 := taskOfSameJob(task2, api.TaskStatusCompleted)
job := task1.Job
call1 := mocks.expectSaveJobWithStatus(t, job, api.JobStatusRequeueing)
@ -302,17 +316,16 @@ func TestJobRequeueWithAllCompletedTasks(t *testing.T) {
mocks.expectBroadcastJobChangeWithTaskRefresh(job, api.JobStatusCompleted, api.JobStatusRequeueing)
mocks.expectBroadcastJobChangeWithTaskRefresh(job, api.JobStatusRequeueing, api.JobStatusQueued)
require.NoError(t, sm.JobStatusChange(ctx, job, api.JobStatusRequeueing, "someone wrote a unit test"))
require.NoError(t, sm.jobStatusChange(ctx, job, api.JobStatusRequeueing, "someone wrote a unit test"))
}
func TestJobCancelWithSomeCompletedTasks(t *testing.T) {
mockCtrl, ctx, sm, mocks := taskStateMachineTestFixtures(t)
defer mockCtrl.Finish()
task1 := taskWithStatus(api.JobStatusActive, api.TaskStatusCompleted)
_, job := taskWithStatus(api.JobStatusActive, api.TaskStatusCompleted)
// task2 := taskOfSameJob(task1, api.TaskStatusFailed)
// task3 := taskOfSameJob(task2, api.TaskStatusSoftFailed)
job := task1.Job
mocks.expectSaveJobWithStatus(t, job, api.JobStatusCancelRequested)
@ -333,17 +346,17 @@ func TestJobCancelWithSomeCompletedTasks(t *testing.T) {
mocks.expectBroadcastJobChangeWithTaskRefresh(job, api.JobStatusActive, api.JobStatusCancelRequested)
mocks.expectBroadcastJobChange(job, api.JobStatusCancelRequested, api.JobStatusCanceled)
require.NoError(t, sm.JobStatusChange(ctx, job, api.JobStatusCancelRequested, "someone wrote a unittest"))
require.NoError(t, sm.jobStatusChange(ctx, job, api.JobStatusCancelRequested, "someone wrote a unittest"))
}
func TestJobPauseWithAllQueuedTasks(t *testing.T) {
mockCtrl, ctx, sm, mocks := taskStateMachineTestFixtures(t)
defer mockCtrl.Finish()
task1 := taskWithStatus(api.JobStatusQueued, api.TaskStatusQueued)
task1, job := taskWithStatus(api.JobStatusQueued, api.TaskStatusQueued)
task2 := taskOfSameJob(task1, api.TaskStatusQueued)
task3 := taskOfSameJob(task2, api.TaskStatusQueued)
job := task3.Job
_ = task3
mocks.expectSaveJobWithStatus(t, job, api.JobStatusPauseRequested)
@ -363,17 +376,17 @@ func TestJobPauseWithAllQueuedTasks(t *testing.T) {
mocks.expectBroadcastJobChangeWithTaskRefresh(job, api.JobStatusQueued, api.JobStatusPauseRequested)
mocks.expectBroadcastJobChange(job, api.JobStatusPauseRequested, api.JobStatusPaused)
require.NoError(t, sm.JobStatusChange(ctx, job, api.JobStatusPauseRequested, "someone wrote a unittest"))
require.NoError(t, sm.jobStatusChange(ctx, job, api.JobStatusPauseRequested, "someone wrote a unittest"))
}
func TestJobPauseWithSomeCompletedTasks(t *testing.T) {
mockCtrl, ctx, sm, mocks := taskStateMachineTestFixtures(t)
defer mockCtrl.Finish()
task1 := taskWithStatus(api.JobStatusQueued, api.TaskStatusCompleted)
task1, job := taskWithStatus(api.JobStatusQueued, api.TaskStatusCompleted)
task2 := taskOfSameJob(task1, api.TaskStatusQueued)
task3 := taskOfSameJob(task2, api.TaskStatusQueued)
job := task3.Job
_ = task3
mocks.expectSaveJobWithStatus(t, job, api.JobStatusPauseRequested)
@ -393,17 +406,17 @@ func TestJobPauseWithSomeCompletedTasks(t *testing.T) {
mocks.expectBroadcastJobChangeWithTaskRefresh(job, api.JobStatusQueued, api.JobStatusPauseRequested)
mocks.expectBroadcastJobChange(job, api.JobStatusPauseRequested, api.JobStatusPaused)
require.NoError(t, sm.JobStatusChange(ctx, job, api.JobStatusPauseRequested, "someone wrote a unittest"))
require.NoError(t, sm.jobStatusChange(ctx, job, api.JobStatusPauseRequested, "someone wrote a unittest"))
}
func TestJobPauseWithSomeActiveTasks(t *testing.T) {
mockCtrl, ctx, sm, mocks := taskStateMachineTestFixtures(t)
defer mockCtrl.Finish()
task1 := taskWithStatus(api.JobStatusActive, api.TaskStatusActive)
task1, job := taskWithStatus(api.JobStatusActive, api.TaskStatusActive)
task2 := taskOfSameJob(task1, api.TaskStatusCompleted)
task3 := taskOfSameJob(task2, api.TaskStatusQueued)
job := task3.Job
_ = task3
mocks.expectSaveJobWithStatus(t, job, api.JobStatusPauseRequested)
@ -421,17 +434,17 @@ func TestJobPauseWithSomeActiveTasks(t *testing.T) {
Return(1, 3, nil)
mocks.expectBroadcastJobChangeWithTaskRefresh(job, api.JobStatusActive, api.JobStatusPauseRequested)
require.NoError(t, sm.JobStatusChange(ctx, job, api.JobStatusPauseRequested, "someone wrote a unittest"))
require.NoError(t, sm.jobStatusChange(ctx, job, api.JobStatusPauseRequested, "someone wrote a unittest"))
}
func TestCheckStuck(t *testing.T) {
mockCtrl, ctx, sm, mocks := taskStateMachineTestFixtures(t)
defer mockCtrl.Finish()
task1 := taskWithStatus(api.JobStatusActive, api.TaskStatusCompleted)
task1, job := taskWithStatus(api.JobStatusActive, api.TaskStatusCompleted)
_ = task1
// task2 := taskOfSameJob(task1, api.TaskStatusFailed)
// task3 := taskOfSameJob(task2, api.TaskStatusSoftFailed)
job := task1.Job
job.Status = api.JobStatusRequeueing
mocks.persist.EXPECT().FetchJobsInStatus(ctx, api.JobStatusCancelRequested, api.JobStatusRequeueing).
@ -469,6 +482,13 @@ func mockedTaskStateMachine(mockCtrl *gomock.Controller) (*StateMachine, *StateM
return sm, &mocks
}
func (m *StateMachineMocks) expectFetchJobOfTask(
task *persistence.Task,
jobToReturn *persistence.Job,
) *gomock.Call {
return m.persist.EXPECT().FetchJobByID(gomock.Any(), task.JobID).Return(jobToReturn, nil)
}
func (m *StateMachineMocks) expectSaveTaskWithStatus(
t *testing.T,
task *persistence.Task,
@ -484,10 +504,11 @@ func (m *StateMachineMocks) expectSaveTaskWithStatus(
func (m *StateMachineMocks) expectWriteTaskLogTimestamped(
t *testing.T,
task *persistence.Task,
jobUUID string,
logtext string,
) *gomock.Call {
return m.logStorage.EXPECT().WriteTimestamped(
gomock.Any(), task.Job.UUID, task.UUID, logtext,
gomock.Any(), jobUUID, task.UUID, logtext,
)
}
@ -514,7 +535,7 @@ func (m *StateMachineMocks) expectBroadcastJobChange(
PreviousStatus: &fromStatus,
RefreshTasks: false,
Status: toStatus,
Updated: job.UpdatedAt,
Updated: job.UpdatedAt.Time,
}
return m.broadcaster.EXPECT().BroadcastJobUpdate(expectUpdate)
}
@ -529,20 +550,21 @@ func (m *StateMachineMocks) expectBroadcastJobChangeWithTaskRefresh(
PreviousStatus: &fromStatus,
RefreshTasks: true,
Status: toStatus,
Updated: job.UpdatedAt,
Updated: job.UpdatedAt.Time,
}
return m.broadcaster.EXPECT().BroadcastJobUpdate(expectUpdate)
}
func (m *StateMachineMocks) expectBroadcastTaskChange(
task *persistence.Task,
jobUUID string,
fromStatus, toStatus api.TaskStatus,
) *gomock.Call {
expectUpdate := api.EventTaskUpdate{
Id: task.UUID,
JobId: task.Job.UUID,
JobId: jobUUID,
Name: task.Name,
Updated: task.UpdatedAt,
Updated: task.UpdatedAt.Time,
PreviousStatus: &fromStatus,
Status: toStatus,
}
@ -550,36 +572,30 @@ func (m *StateMachineMocks) expectBroadcastTaskChange(
}
/* taskWithStatus() creates a task of a certain status, with a job of a certain status. */
func taskWithStatus(jobStatus api.JobStatus, taskStatus api.TaskStatus) *persistence.Task {
func taskWithStatus(jobStatus api.JobStatus, taskStatus api.TaskStatus) (*persistence.Task, *persistence.Job) {
job := persistence.Job{
Model: persistence.Model{ID: 47},
ID: 47,
UUID: "test-job-f3f5-4cef-9cd7-e67eb28eaf3e",
Status: jobStatus,
}
task := persistence.Task{
Model: persistence.Model{ID: 327},
UUID: "testtask-0001-4e28-aeea-8cbaf2fc96a5",
JobUUID: job.UUID,
ID: 327,
JobID: job.ID,
Job: &job,
UUID: "testtask-0001-4e28-aeea-8cbaf2fc96a5",
Status: taskStatus,
}
return &task
return &task, &job
}
/* taskOfSameJob() creates a task of a certain status, on the same job as the given task. */
func taskOfSameJob(task *persistence.Task, taskStatus api.TaskStatus) *persistence.Task {
newTaskID := task.ID + 1
return &persistence.Task{
Model: persistence.Model{ID: newTaskID},
ID: newTaskID,
UUID: fmt.Sprintf("testtask-%04d-4e28-aeea-8cbaf2fc96a5", newTaskID),
JobUUID: task.JobUUID,
JobID: task.JobID,
Job: task.Job,
Status: taskStatus,
}
}

View File

@ -19,13 +19,19 @@ func (sm *StateMachine) RequeueActiveTasksOfWorker(
reason string,
) error {
// Fetch the tasks to update.
tasks, err := sm.persist.FetchTasksOfWorkerInStatus(
tasksJobs, err := sm.persist.FetchTasksOfWorkerInStatus(
ctx, worker, api.TaskStatusActive)
if err != nil {
return err
}
return sm.requeueTasksOfWorker(ctx, tasks, worker, reason)
// Run each task change through the task state machine.
var lastErr error
for _, taskJobWorker := range tasksJobs {
lastErr = sm.requeueTaskOfWorker(ctx, &taskJobWorker.Task, taskJobWorker.JobUUID, worker, reason)
}
return lastErr
}
// RequeueFailedTasksOfWorkerOfJob re-queues all failed tasks of this worker on this job.
@ -34,22 +40,29 @@ func (sm *StateMachine) RequeueActiveTasksOfWorker(
func (sm *StateMachine) RequeueFailedTasksOfWorkerOfJob(
ctx context.Context,
worker *persistence.Worker,
job *persistence.Job,
jobUUID string,
reason string,
) error {
// Fetch the tasks to update.
tasks, err := sm.persist.FetchTasksOfWorkerInStatusOfJob(
ctx, worker, api.TaskStatusFailed, job)
ctx, worker, api.TaskStatusFailed, jobUUID)
if err != nil {
return err
}
return sm.requeueTasksOfWorker(ctx, tasks, worker, reason)
// Run each task change through the task state machine.
var lastErr error
for _, task := range tasks {
lastErr = sm.requeueTaskOfWorker(ctx, task, jobUUID, worker, reason)
}
return lastErr
}
func (sm *StateMachine) requeueTasksOfWorker(
func (sm *StateMachine) requeueTaskOfWorker(
ctx context.Context,
tasks []*persistence.Task,
task *persistence.Task,
jobUUID string,
worker *persistence.Worker,
reason string,
) error {
@ -58,9 +71,6 @@ func (sm *StateMachine) requeueTasksOfWorker(
Str("reason", reason).
Logger()
// Run each task change through the task state machine.
var lastErr error
for _, task := range tasks {
logger.Info().
Str("task", task.UUID).
Msg("re-queueing task")
@ -73,20 +83,18 @@ func (sm *StateMachine) requeueTasksOfWorker(
Str("reason", reason).
Str("activity", task.Activity).
Msg("error saving task activity to database")
lastErr = err
}
if err := sm.TaskStatusChange(ctx, task, api.TaskStatusQueued); err != nil {
err := sm.TaskStatusChange(ctx, task, api.TaskStatusQueued)
if err != nil {
logger.Warn().Err(err).
Str("task", task.UUID).
Str("reason", reason).
Msg("error queueing task")
lastErr = err
}
// The error is already logged by the log storage.
_ = sm.logStorage.WriteTimestamped(logger, task.Job.UUID, task.UUID, task.Activity)
}
_ = sm.logStorage.WriteTimestamped(logger, jobUUID, task.UUID, task.Activity)
return lastErr
return err
}

View File

@ -22,9 +22,12 @@ func TestRequeueActiveTasksOfWorker(t *testing.T) {
// Mock that the worker has two active tasks. It shouldn't happen, but even
// when it does, both should be requeued when the worker signs off.
task1 := taskWithStatus(api.JobStatusActive, api.TaskStatusActive)
task1, job := taskWithStatus(api.JobStatusActive, api.TaskStatusActive)
task2 := taskOfSameJob(task1, api.TaskStatusActive)
workerTasks := []*persistence.Task{task1, task2}
workerTasks := []persistence.TaskJob{
{Task: *task1, JobUUID: job.UUID},
{Task: *task2, JobUUID: job.UUID},
}
task1PrevStatus := task1.Status
task2PrevStatus := task2.Status
@ -32,39 +35,50 @@ func TestRequeueActiveTasksOfWorker(t *testing.T) {
mocks.persist.EXPECT().FetchTasksOfWorkerInStatus(ctx, &worker, api.TaskStatusActive).Return(workerTasks, nil)
// Expect this re-queueing to end up in the task's log and activity.
mocks.persist.EXPECT().SaveTaskActivity(ctx, task1) // TODO: test saved activity value
mocks.persist.EXPECT().SaveTaskActivity(ctx, task2) // TODO: test saved activity value
mocks.persist.EXPECT().SaveTaskStatus(ctx, task1) // TODO: test saved task status
mocks.persist.EXPECT().SaveTaskStatus(ctx, task2) // TODO: test saved task status
logMsg1 := "task changed status active -> queued"
mocks.logStorage.EXPECT().WriteTimestamped(gomock.Any(), task1.Job.UUID, task1.UUID, logMsg1)
mocks.logStorage.EXPECT().WriteTimestamped(gomock.Any(), task2.Job.UUID, task2.UUID, logMsg1)
logMsg2 := "Task was requeued by Manager because worker had to test"
mocks.logStorage.EXPECT().WriteTimestamped(gomock.Any(), task1.Job.UUID, task1.UUID, logMsg2)
mocks.logStorage.EXPECT().WriteTimestamped(gomock.Any(), task2.Job.UUID, task2.UUID, logMsg2)
task1WithActivity := *task1
task1WithActivity.Activity = logMsg2
task2WithActivity := *task2
task2WithActivity.Activity = logMsg2
task1WithActivityAndStatus := task1WithActivity
task1WithActivityAndStatus.Status = api.TaskStatusQueued
task2WithActivityAndStatus := task2WithActivity
task2WithActivityAndStatus.Status = api.TaskStatusQueued
mocks.persist.EXPECT().SaveTaskActivity(ctx, &task1WithActivity)
mocks.persist.EXPECT().SaveTaskActivity(ctx, &task2WithActivity)
mocks.persist.EXPECT().SaveTaskStatus(ctx, &task1WithActivityAndStatus)
mocks.persist.EXPECT().SaveTaskStatus(ctx, &task2WithActivityAndStatus)
mocks.logStorage.EXPECT().WriteTimestamped(gomock.Any(), job.UUID, task1.UUID, logMsg1)
mocks.logStorage.EXPECT().WriteTimestamped(gomock.Any(), job.UUID, task2.UUID, logMsg1)
mocks.logStorage.EXPECT().WriteTimestamped(gomock.Any(), job.UUID, task1.UUID, logMsg2)
mocks.logStorage.EXPECT().WriteTimestamped(gomock.Any(), job.UUID, task2.UUID, logMsg2)
mocks.broadcaster.EXPECT().BroadcastTaskUpdate(api.EventTaskUpdate{
Activity: logMsg2,
Id: task1.UUID,
JobId: task1.Job.UUID,
JobId: job.UUID,
Name: task1.Name,
PreviousStatus: &task1PrevStatus,
Status: api.TaskStatusQueued,
Updated: task1.UpdatedAt,
Updated: task1.UpdatedAt.Time,
})
mocks.broadcaster.EXPECT().BroadcastTaskUpdate(api.EventTaskUpdate{
Activity: logMsg2,
Id: task2.UUID,
JobId: task2.Job.UUID,
JobId: job.UUID,
Name: task2.Name,
PreviousStatus: &task2PrevStatus,
Status: api.TaskStatusQueued,
Updated: task2.UpdatedAt,
Updated: task2.UpdatedAt.Time,
})
mocks.expectFetchJobOfTask(task1, job)
mocks.expectFetchJobOfTask(task2, job)
err := sm.RequeueActiveTasksOfWorker(ctx, &worker, "worker had to test")
require.NoError(t, err)
}

View File

@ -9,7 +9,6 @@ import (
"github.com/rs/zerolog"
"projects.blender.org/studio/flamenco/internal/manager/eventbus"
"projects.blender.org/studio/flamenco/internal/manager/persistence"
"projects.blender.org/studio/flamenco/internal/manager/persistence/sqlc"
"projects.blender.org/studio/flamenco/internal/manager/task_state_machine"
"projects.blender.org/studio/flamenco/pkg/api"
)
@ -18,9 +17,11 @@ import (
//go:generate go run github.com/golang/mock/mockgen -destination mocks/interfaces_mock.gen.go -package mocks projects.blender.org/studio/flamenco/internal/manager/timeout_checker PersistenceService,TaskStateMachine,LogStorage,ChangeBroadcaster
type PersistenceService interface {
FetchTimedOutTasks(ctx context.Context, untouchedSince time.Time) ([]*persistence.Task, error)
FetchTimedOutTasks(ctx context.Context, untouchedSince time.Time) ([]persistence.TimedOutTaskInfo, error)
FetchTimedOutWorkers(ctx context.Context, lastSeenBefore time.Time) ([]*persistence.Worker, error)
SaveWorker(ctx context.Context, w *sqlc.Worker) error
FetchWorker(ctx context.Context, workerUUID string) (*persistence.Worker, error)
SaveWorker(ctx context.Context, w *persistence.Worker) error
FetchJob(ctx context.Context, jobUUID string) (*persistence.Job, error)
}
var _ PersistenceService = (*persistence.DB)(nil)

View File

@ -11,7 +11,6 @@ import (
gomock "github.com/golang/mock/gomock"
zerolog "github.com/rs/zerolog"
persistence "projects.blender.org/studio/flamenco/internal/manager/persistence"
sqlc "projects.blender.org/studio/flamenco/internal/manager/persistence/sqlc"
api "projects.blender.org/studio/flamenco/pkg/api"
)
@ -39,11 +38,26 @@ func (m *MockPersistenceService) EXPECT() *MockPersistenceServiceMockRecorder {
return m.recorder
}
// FetchJob mocks base method.
func (m *MockPersistenceService) FetchJob(arg0 context.Context, arg1 string) (*sqlc.Job, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchJob", arg0, arg1)
ret0, _ := ret[0].(*sqlc.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FetchJob indicates an expected call of FetchJob.
func (mr *MockPersistenceServiceMockRecorder) FetchJob(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchJob", reflect.TypeOf((*MockPersistenceService)(nil).FetchJob), arg0, arg1)
}
// FetchTimedOutTasks mocks base method.
func (m *MockPersistenceService) FetchTimedOutTasks(arg0 context.Context, arg1 time.Time) ([]*persistence.Task, error) {
func (m *MockPersistenceService) FetchTimedOutTasks(arg0 context.Context, arg1 time.Time) ([]sqlc.FetchTimedOutTasksRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchTimedOutTasks", arg0, arg1)
ret0, _ := ret[0].([]*persistence.Task)
ret0, _ := ret[0].([]sqlc.FetchTimedOutTasksRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -69,6 +83,21 @@ func (mr *MockPersistenceServiceMockRecorder) FetchTimedOutWorkers(arg0, arg1 in
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchTimedOutWorkers", reflect.TypeOf((*MockPersistenceService)(nil).FetchTimedOutWorkers), arg0, arg1)
}
// FetchWorker mocks base method.
func (m *MockPersistenceService) FetchWorker(arg0 context.Context, arg1 string) (*sqlc.Worker, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchWorker", arg0, arg1)
ret0, _ := ret[0].(*sqlc.Worker)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FetchWorker indicates an expected call of FetchWorker.
func (mr *MockPersistenceServiceMockRecorder) FetchWorker(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchWorker", reflect.TypeOf((*MockPersistenceService)(nil).FetchWorker), arg0, arg1)
}
// SaveWorker mocks base method.
func (m *MockPersistenceService) SaveWorker(arg0 context.Context, arg1 *sqlc.Worker) error {
m.ctrl.T.Helper()
@ -121,7 +150,7 @@ func (mr *MockTaskStateMachineMockRecorder) RequeueActiveTasksOfWorker(arg0, arg
}
// TaskStatusChange mocks base method.
func (m *MockTaskStateMachine) TaskStatusChange(arg0 context.Context, arg1 *persistence.Task, arg2 api.TaskStatus) error {
func (m *MockTaskStateMachine) TaskStatusChange(arg0 context.Context, arg1 *sqlc.Task, arg2 api.TaskStatus) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TaskStatusChange", arg0, arg1, arg2)
ret0, _ := ret[0].(error)

View File

@ -21,38 +21,44 @@ func (ttc *TimeoutChecker) checkTasks(ctx context.Context) {
Logger()
logger.Trace().Msg("TimeoutChecker: finding active tasks that have not been touched since threshold")
tasks, err := ttc.persist.FetchTimedOutTasks(ctx, timeoutThreshold)
timeoutTaskInfo, err := ttc.persist.FetchTimedOutTasks(ctx, timeoutThreshold)
if err != nil {
log.Error().Err(err).Msg("TimeoutChecker: error fetching timed-out tasks from database")
return
}
if len(tasks) == 0 {
if len(timeoutTaskInfo) == 0 {
logger.Trace().Msg("TimeoutChecker: no timed-out tasks")
return
}
logger.Debug().
Int("numTasks", len(tasks)).
Int("numTasks", len(timeoutTaskInfo)).
Msg("TimeoutChecker: failing all active tasks that have not been touched since threshold")
for _, task := range tasks {
ttc.timeoutTask(ctx, task)
for _, taskInfo := range timeoutTaskInfo {
ttc.timeoutTask(ctx, taskInfo)
}
}
// timeoutTask marks a task as 'failed' due to a timeout.
func (ttc *TimeoutChecker) timeoutTask(ctx context.Context, task *persistence.Task) {
workerIdent, logger := ttc.assignedWorker(task)
func (ttc *TimeoutChecker) timeoutTask(ctx context.Context, taskInfo persistence.TimedOutTaskInfo) {
task := taskInfo.Task
workerIdent, logger := ttc.assignedWorker(taskInfo)
task.Activity = fmt.Sprintf("Task timed out on worker %s", workerIdent)
err := ttc.taskStateMachine.TaskStatusChange(ctx, task, api.TaskStatusFailed)
err := ttc.taskStateMachine.TaskStatusChange(ctx, &task, api.TaskStatusFailed)
if err != nil {
logger.Error().Err(err).Msg("TimeoutChecker: error saving timed-out task to database")
}
err = ttc.logStorage.WriteTimestamped(logger, task.Job.UUID, task.UUID,
lastTouchedAt := "forever"
if task.LastTouchedAt.Valid {
lastTouchedAt = task.LastTouchedAt.Time.Format(time.RFC3339)
}
err = ttc.logStorage.WriteTimestamped(logger, taskInfo.JobUUID, task.UUID,
fmt.Sprintf("Task timed out. It was assigned to worker %s, but untouched since %s",
workerIdent, task.LastTouchedAt.Format(time.RFC3339)))
workerIdent, lastTouchedAt))
if err != nil {
logger.Error().Err(err).Msg("TimeoutChecker: error writing timeout info to the task log")
}
@ -60,27 +66,20 @@ func (ttc *TimeoutChecker) timeoutTask(ctx context.Context, task *persistence.Ta
// assignedWorker returns a description of the worker assigned to this task,
// and a logger configured for it.
func (ttc *TimeoutChecker) assignedWorker(task *persistence.Task) (string, zerolog.Logger) {
logCtx := log.With().Str("task", task.UUID)
func (ttc *TimeoutChecker) assignedWorker(taskInfo persistence.TimedOutTaskInfo) (string, zerolog.Logger) {
logCtx := log.With().Str("task", taskInfo.Task.UUID)
if task.WorkerID == nil {
if taskInfo.WorkerUUID == "" {
logger := logCtx.Logger()
logger.Warn().Msg("TimeoutChecker: task timed out, but was not assigned to any worker")
return "-unassigned-", logger
}
if task.Worker == nil {
logger := logCtx.Logger()
logger.Warn().Uint("workerDBID", *task.WorkerID).
Msg("TimeoutChecker: task is assigned to worker that no longer exists")
return "-unknown-", logger
}
logCtx = logCtx.
Str("worker", task.Worker.UUID).
Str("workerName", task.Worker.Name)
Str("worker", taskInfo.WorkerUUID).
Str("workerName", taskInfo.WorkerName)
logger := logCtx.Logger()
logger.Warn().Msg("TimeoutChecker: task timed out")
return task.Worker.Identifier(), logger
return fmt.Sprintf("%s (%s)", taskInfo.WorkerName, taskInfo.WorkerUUID), logger
}

View File

@ -4,6 +4,7 @@ package timeout_checker
import (
"context"
"database/sql"
"errors"
"testing"
"time"
@ -109,7 +110,7 @@ func TestTaskTimeout(t *testing.T) {
lastTime := mocks.clock.Now().UTC().Add(-1 * time.Hour)
job := persistence.Job{UUID: "JOB-UUID"}
job := persistence.Job{ID: 327, UUID: "JOB-UUID"}
worker := persistence.Worker{
UUID: "WORKER-UUID",
Name: "Tester",
@ -117,36 +118,35 @@ func TestTaskTimeout(t *testing.T) {
}
taskUnassigned := persistence.Task{
UUID: "TASK-UUID-UNASSIGNED",
Job: &job,
LastTouchedAt: lastTime,
}
taskUnknownWorker := persistence.Task{
UUID: "TASK-UUID-UNKNOWN",
Job: &job,
LastTouchedAt: lastTime,
WorkerID: ptr(uint(worker.ID)),
JobID: job.ID,
LastTouchedAt: sql.NullTime{Time: lastTime, Valid: true},
}
taskAssigned := persistence.Task{
UUID: "TASK-UUID-ASSIGNED",
Job: &job,
LastTouchedAt: lastTime,
WorkerID: ptr(uint(worker.ID)),
Worker: &worker,
JobID: job.ID,
LastTouchedAt: sql.NullTime{Time: lastTime, Valid: true},
WorkerID: sql.NullInt64{Int64: worker.ID, Valid: true},
}
mocks.persist.EXPECT().FetchTimedOutWorkers(mocks.ctx, gomock.Any()).AnyTimes().Return(nil, nil)
timedoutTaskInfo := []persistence.TimedOutTaskInfo{
{Task: taskUnassigned, JobUUID: job.UUID, WorkerName: "", WorkerUUID: ""},
{Task: taskAssigned, JobUUID: job.UUID, WorkerName: worker.Name, WorkerUUID: worker.UUID},
}
mocks.persist.EXPECT().FetchTimedOutTasks(mocks.ctx, gomock.Any()).
Return([]*persistence.Task{&taskUnassigned, &taskUnknownWorker, &taskAssigned}, nil)
Return(timedoutTaskInfo, nil)
mocks.taskStateMachine.EXPECT().TaskStatusChange(mocks.ctx, &taskUnassigned, api.TaskStatusFailed)
mocks.taskStateMachine.EXPECT().TaskStatusChange(mocks.ctx, &taskUnknownWorker, api.TaskStatusFailed)
mocks.taskStateMachine.EXPECT().TaskStatusChange(mocks.ctx, &taskAssigned, api.TaskStatusFailed)
taskUnassignedWithActivity := taskUnassigned
taskUnassignedWithActivity.Activity = "Task timed out on worker -unassigned-"
taskAssignedWithActivity := taskAssigned
taskAssignedWithActivity.Activity = "Task timed out on worker Tester (WORKER-UUID)"
mocks.taskStateMachine.EXPECT().TaskStatusChange(mocks.ctx, &taskUnassignedWithActivity, api.TaskStatusFailed)
mocks.taskStateMachine.EXPECT().TaskStatusChange(mocks.ctx, &taskAssignedWithActivity, api.TaskStatusFailed)
mocks.logStorage.EXPECT().WriteTimestamped(gomock.Any(), job.UUID, taskUnassigned.UUID,
"Task timed out. It was assigned to worker -unassigned-, but untouched since 2022-06-09T11:00:00Z")
mocks.logStorage.EXPECT().WriteTimestamped(gomock.Any(), job.UUID, taskUnknownWorker.UUID,
"Task timed out. It was assigned to worker -unknown-, but untouched since 2022-06-09T11:00:00Z")
mocks.logStorage.EXPECT().WriteTimestamped(gomock.Any(), job.UUID, taskAssigned.UUID,
"Task timed out. It was assigned to worker Tester (WORKER-UUID), but untouched since 2022-06-09T11:00:00Z")

View File

@ -38,7 +38,7 @@ func TestWorkerTimeout(t *testing.T) {
}
// No tasks are timing out in this test.
mocks.persist.EXPECT().FetchTimedOutTasks(mocks.ctx, gomock.Any()).Return([]*persistence.Task{}, nil)
mocks.persist.EXPECT().FetchTimedOutTasks(mocks.ctx, gomock.Any()).Return([]persistence.TimedOutTaskInfo{}, nil)
mocks.persist.EXPECT().FetchTimedOutWorkers(mocks.ctx, gomock.Any()).
Return([]*persistence.Worker{&worker}, nil)