Compare commits

...

10 Commits

Author SHA1 Message Date
David Zhang
00848a3755 Manager: backend for submitting jobs in paused status
With a new `initial_status` field, the user could choose whether to submit a job in `paused` status.

Pull request: https://projects.blender.org/studio/flamenco/pulls/104322
2024-07-09 22:57:01 -04:00
David Zhang
376112b392 OAPI: regenerate code 2024-07-09 22:57:00 -04:00
David Zhang
6f3ba146de OAPI: add initial_status field in SubmittedJob
Pull request: https://projects.blender.org/studio/flamenco/pulls/104322
2024-07-09 22:56:59 -04:00
3b2b5c47f3 Manager: convert FetchWorkerTask() from gorm to sqlc
Ref: #104305

No functional changes
2024-07-02 10:16:41 +02:00
7db91294c2 Manager: fix unit test TestFetchWorkerTask reliability
The unit test was using the real clock, and depends on ordering by
last-updated timestamps. This means that sometimes two updates both fell
within the granularity of the timestamp in sqlite, which made the
ordering unreliable.

Switching to a mocked clock fixes this, as it moves forward with a hard-
coded pace, regardless of the execution speed of the test.

No functional changes to Flamenco itself.
2024-07-02 10:00:08 +02:00
David Zhang
aac55e7e3c Manager: Support pausing jobs
A job first goes to `pause-requested` status, during which any `active` task
gets a chance to be completed. Once there are no more active tasks, the job
goes to `paused` state (or `failed`, if that is applicable).

Pull request: https://projects.blender.org/studio/flamenco/pulls/104313
2024-07-01 10:59:37 -04:00
David Zhang
1330487078 OAPI: regenerate code 2024-07-01 10:46:57 -04:00
David Zhang
8a82b1ee0a OAPI: add pause-requested job status
Pull request: https://projects.blender.org/studio/flamenco/pulls/104313
2024-07-01 10:44:46 -04:00
46d56a7b49 Mark go.sum as 'generated'
The `go.sum` file is not directly managed, but just the result of running
`go get`, `go mod tidy`, etc.
2024-07-01 11:59:52 +02:00
7d8afb5be7 Worker: reduce log level of last-rendered-image processing
Reduce the log level from 'info' to 'debug' for the "output uploader:
processing file before uploading to Manager" message.
2024-07-01 11:34:22 +02:00
26 changed files with 617 additions and 276 deletions

1
.gitattributes vendored

@ -3,6 +3,7 @@
/addon/flamenco/manager_README.md linguist-generated=true
/web/app/src/manager-api/** linguist-generated=true
**/*.gen.go linguist-generated=true
/go.sum linguist-generated=true
# In your Git config, set:
# git config core.eol native

@ -18,6 +18,7 @@ Name | Type | Description | Notes
**metadata** | [**JobMetadata**](JobMetadata.md) | | [optional]
**storage** | [**JobStorageInfo**](JobStorageInfo.md) | | [optional]
**worker_tag** | **str** | Worker tag that should execute this job. When a tag ID is given, only Workers in that tag will be scheduled to work on it. If empty or ommitted, all workers can work on this job. | [optional]
**initial_status** | [**JobStatus**](JobStatus.md) | | [optional]
**delete_requested_at** | **datetime** | If job deletion was requested, this is the timestamp at which that request was stored on Flamenco Manager. | [optional]
**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]

@ -4,7 +4,7 @@
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**value** | **str** | | must be one of ["active", "canceled", "completed", "failed", "paused", "queued", "cancel-requested", "requeueing", "under-construction", ]
**value** | **str** | | must be one of ["active", "canceled", "completed", "failed", "paused", "pause-requested", "queued", "cancel-requested", "requeueing", "under-construction", ]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

@ -1296,6 +1296,7 @@ with flamenco.manager.ApiClient() as api_client:
shaman_checkout_id="shaman_checkout_id_example",
),
worker_tag="worker_tag_example",
initial_status=JobStatus("active"),
) # SubmittedJob | Job to submit
# example passing only required values which don't have defaults set
@ -1378,6 +1379,7 @@ with flamenco.manager.ApiClient() as api_client:
shaman_checkout_id="shaman_checkout_id_example",
),
worker_tag="worker_tag_example",
initial_status=JobStatus("active"),
) # SubmittedJob | Job to check
# example passing only required values which don't have defaults set

@ -14,6 +14,7 @@ Name | Type | Description | Notes
**metadata** | [**JobMetadata**](JobMetadata.md) | | [optional]
**storage** | [**JobStorageInfo**](JobStorageInfo.md) | | [optional]
**worker_tag** | **str** | Worker tag that should execute this job. When a tag ID is given, only Workers in that tag will be scheduled to work on it. If empty or ommitted, all workers can work on this job. | [optional]
**initial_status** | [**JobStatus**](JobStatus.md) | | [optional]
**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

@ -111,6 +111,7 @@ class Job(ModelComposed):
'metadata': (JobMetadata,), # noqa: E501
'storage': (JobStorageInfo,), # noqa: E501
'worker_tag': (str,), # noqa: E501
'initial_status': (JobStatus,), # noqa: E501
'delete_requested_at': (datetime,), # noqa: E501
}
@ -134,6 +135,7 @@ class Job(ModelComposed):
'metadata': 'metadata', # noqa: E501
'storage': 'storage', # noqa: E501
'worker_tag': 'worker_tag', # noqa: E501
'initial_status': 'initial_status', # noqa: E501
'delete_requested_at': 'delete_requested_at', # noqa: E501
}
@ -190,6 +192,7 @@ class Job(ModelComposed):
metadata (JobMetadata): [optional] # noqa: E501
storage (JobStorageInfo): [optional] # noqa: E501
worker_tag (str): Worker tag that should execute this job. When a tag ID is given, only Workers in that tag will be scheduled to work on it. If empty or ommitted, all workers can work on this job. . [optional] # noqa: E501
initial_status (JobStatus): [optional] # noqa: E501
delete_requested_at (datetime): If job deletion was requested, this is the timestamp at which that request was stored on Flamenco Manager. . [optional] # noqa: E501
"""
@ -305,6 +308,7 @@ class Job(ModelComposed):
metadata (JobMetadata): [optional] # noqa: E501
storage (JobStorageInfo): [optional] # noqa: E501
worker_tag (str): Worker tag that should execute this job. When a tag ID is given, only Workers in that tag will be scheduled to work on it. If empty or ommitted, all workers can work on this job. . [optional] # noqa: E501
initial_status (JobStatus): [optional] # noqa: E501
delete_requested_at (datetime): If job deletion was requested, this is the timestamp at which that request was stored on Flamenco Manager. . [optional] # noqa: E501
"""

@ -57,6 +57,7 @@ class JobStatus(ModelSimple):
'COMPLETED': "completed",
'FAILED': "failed",
'PAUSED': "paused",
'PAUSE-REQUESTED': "pause-requested",
'QUEUED': "queued",
'CANCEL-REQUESTED': "cancel-requested",
'REQUEUEING': "requeueing",
@ -112,10 +113,10 @@ class JobStatus(ModelSimple):
Note that value can be passed either in args or in kwargs, but not in both.
Args:
args[0] (str):, must be one of ["active", "canceled", "completed", "failed", "paused", "queued", "cancel-requested", "requeueing", "under-construction", ] # noqa: E501
args[0] (str):, must be one of ["active", "canceled", "completed", "failed", "paused", "pause-requested", "queued", "cancel-requested", "requeueing", "under-construction", ] # noqa: E501
Keyword Args:
value (str):, must be one of ["active", "canceled", "completed", "failed", "paused", "queued", "cancel-requested", "requeueing", "under-construction", ] # noqa: E501
value (str):, must be one of ["active", "canceled", "completed", "failed", "paused", "pause-requested", "queued", "cancel-requested", "requeueing", "under-construction", ] # noqa: E501
_check_type (bool): if True, values for parameters in openapi_types
will be type checked and a TypeError will be
raised if the wrong type is input.
@ -202,10 +203,10 @@ class JobStatus(ModelSimple):
Note that value can be passed either in args or in kwargs, but not in both.
Args:
args[0] (str):, must be one of ["active", "canceled", "completed", "failed", "paused", "queued", "cancel-requested", "requeueing", "under-construction", ] # noqa: E501
args[0] (str):, must be one of ["active", "canceled", "completed", "failed", "paused", "pause-requested", "queued", "cancel-requested", "requeueing", "under-construction", ] # noqa: E501
Keyword Args:
value (str):, must be one of ["active", "canceled", "completed", "failed", "paused", "queued", "cancel-requested", "requeueing", "under-construction", ] # noqa: E501
value (str):, must be one of ["active", "canceled", "completed", "failed", "paused", "pause-requested", "queued", "cancel-requested", "requeueing", "under-construction", ] # noqa: E501
_check_type (bool): if True, values for parameters in openapi_types
will be type checked and a TypeError will be
raised if the wrong type is input.

@ -32,9 +32,11 @@ from flamenco.manager.exceptions import ApiAttributeError
def lazy_import():
from flamenco.manager.model.job_metadata import JobMetadata
from flamenco.manager.model.job_settings import JobSettings
from flamenco.manager.model.job_status import JobStatus
from flamenco.manager.model.job_storage_info import JobStorageInfo
globals()['JobMetadata'] = JobMetadata
globals()['JobSettings'] = JobSettings
globals()['JobStatus'] = JobStatus
globals()['JobStorageInfo'] = JobStorageInfo
@ -100,6 +102,7 @@ class SubmittedJob(ModelNormal):
'metadata': (JobMetadata,), # noqa: E501
'storage': (JobStorageInfo,), # noqa: E501
'worker_tag': (str,), # noqa: E501
'initial_status': (JobStatus,), # noqa: E501
}
@cached_property
@ -117,6 +120,7 @@ class SubmittedJob(ModelNormal):
'metadata': 'metadata', # noqa: E501
'storage': 'storage', # noqa: E501
'worker_tag': 'worker_tag', # noqa: E501
'initial_status': 'initial_status', # noqa: E501
}
read_only_vars = {
@ -171,6 +175,7 @@ class SubmittedJob(ModelNormal):
metadata (JobMetadata): [optional] # noqa: E501
storage (JobStorageInfo): [optional] # noqa: E501
worker_tag (str): Worker tag that should execute this job. When a tag ID is given, only Workers in that tag will be scheduled to work on it. If empty or ommitted, all workers can work on this job. . [optional] # noqa: E501
initial_status (JobStatus): [optional] # noqa: E501
"""
priority = kwargs.get('priority', 50)
@ -268,6 +273,7 @@ class SubmittedJob(ModelNormal):
metadata (JobMetadata): [optional] # noqa: E501
storage (JobStorageInfo): [optional] # noqa: E501
worker_tag (str): Worker tag that should execute this job. When a tag ID is given, only Workers in that tag will be scheduled to work on it. If empty or ommitted, all workers can work on this job. . [optional] # noqa: E501
initial_status (JobStatus): [optional] # noqa: E501
"""
priority = kwargs.get('priority', 50)

@ -91,8 +91,12 @@ func (f *Flamenco) SubmitJob(e echo.Context) error {
logger = logger.With().Str("job_id", authoredJob.JobID).Logger()
// TODO: check whether this job should be queued immediately or start paused.
authoredJob.Status = api.JobStatusQueued
submittedJob := api.SubmittedJob(job)
initialStatus := api.JobStatusQueued
if submittedJob.InitialStatus != nil {
initialStatus = *submittedJob.InitialStatus
}
authoredJob.Status = initialStatus
if err := f.persist.StoreAuthoredJob(ctx, *authoredJob); err != nil {
logger.Error().Err(err).Msg("error persisting job in database")

@ -47,6 +47,21 @@ WHERE TF.worker_id IS NULL -- Not failed by this worker before.
AND tasks.type IN (sqlc.slice('supported_task_types'))
ORDER BY jobs.priority DESC, tasks.priority DESC;
-- name: FetchWorkerTask :one
-- 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
FROM tasks
INNER JOIN jobs ON tasks.job_id = jobs.id
WHERE
tasks.worker_id = @worker_id
ORDER BY
is_active DESC,
tasks.updated_at DESC
LIMIT 1;
-- name: AssignTaskToWorker :exec
UPDATE tasks
SET worker_id=@worker_id, last_touched_at=@now, updated_at=@now

@ -82,6 +82,70 @@ func (q *Queries) FetchAssignedAndRunnableTaskOfWorker(ctx context.Context, arg
return i, err
}
const fetchWorkerTask = `-- name: FetchWorkerTask :one
SELECT
tasks.id, tasks.created_at, tasks.updated_at, tasks.uuid, tasks.name, tasks.type, tasks.job_id, 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
FROM tasks
INNER JOIN jobs ON tasks.job_id = jobs.id
WHERE
tasks.worker_id = ?3
ORDER BY
is_active DESC,
tasks.updated_at DESC
LIMIT 1
`
type FetchWorkerTaskParams struct {
TaskStatusActive string
JobStatusActive string
WorkerID sql.NullInt64
}
type FetchWorkerTaskRow struct {
Task Task
Job Job
IsActive interface{}
}
// Find the currently-active task assigned to a Worker. If not found, find the last task this Worker worked on.
func (q *Queries) FetchWorkerTask(ctx context.Context, arg FetchWorkerTaskParams) (FetchWorkerTaskRow, error) {
row := q.db.QueryRowContext(ctx, fetchWorkerTask, arg.TaskStatusActive, arg.JobStatusActive, arg.WorkerID)
var i FetchWorkerTaskRow
err := row.Scan(
&i.Task.ID,
&i.Task.CreatedAt,
&i.Task.UpdatedAt,
&i.Task.UUID,
&i.Task.Name,
&i.Task.Type,
&i.Task.JobID,
&i.Task.Priority,
&i.Task.Status,
&i.Task.WorkerID,
&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.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.priority, tasks.status, tasks.worker_id, tasks.last_touched_at, tasks.commands, tasks.activity
FROM tasks

@ -10,7 +10,6 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"projects.blender.org/studio/flamenco/internal/manager/persistence/sqlc"
"projects.blender.org/studio/flamenco/pkg/api"
@ -164,14 +163,3 @@ func findTaskForWorker(
}
return row.Task, nil
}
// taskAssignedAndRunnableQuery appends some GORM clauses to query for a task
// that's already assigned to this worker, and is in a runnable state.
func taskAssignedAndRunnableQuery(tx *gorm.DB, w *Worker) *gorm.DB {
return tx.
Joins("left join jobs on tasks.job_id = jobs.id").
Where("tasks.status = ?", api.TaskStatusActive).
Where("jobs.status in ?", schedulableJobStatuses).
Where("tasks.worker_id = ?", w.ID). // assigned to this worker
Limit(1)
}

@ -5,6 +5,7 @@ package persistence
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
@ -191,38 +192,49 @@ 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) {
task := Task{}
// See if there is a task assigned to this worker in the same way that the
// task scheduler does.
query := db.gormDB.WithContext(ctx)
query = taskAssignedAndRunnableQuery(query, worker)
tx := query.
Order("tasks.updated_at").
Preload("Job").
Find(&task)
if tx.Error != nil {
return nil, taskError(tx.Error, "fetching task assigned to Worker %s", worker.UUID)
}
if task.ID != 0 {
// Found a task!
return &task, nil
queries, err := db.queries()
if err != nil {
return nil, err
}
// If not found, just find the last-modified task associated with this Worker.
tx = db.gormDB.WithContext(ctx).
Where("worker_id = ?", worker.ID).
Order("tasks.updated_at DESC").
Preload("Job").
Find(&task)
if tx.Error != nil {
return nil, taskError(tx.Error, "fetching task assigned to Worker %s", worker.UUID)
}
if task.ID == 0 {
// Convert the WorkerID to a NullInt64. As task.worker_id can be NULL, this is
// what sqlc expects us to pass in.
workerID := sql.NullInt64{Int64: int64(worker.ID), Valid: true}
row, err := queries.FetchWorkerTask(ctx, sqlc.FetchWorkerTaskParams{
TaskStatusActive: string(api.TaskStatusActive),
JobStatusActive: string(api.JobStatusActive),
WorkerID: workerID,
})
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, nil
case err != nil:
return nil, taskError(err, "fetching task assigned to Worker %s", worker.UUID)
}
return &task, nil
// Found a task!
if row.Job.ID == 0 {
panic(fmt.Sprintf("task found but with no job: %#v", row))
}
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
}
func (db *DB) SaveWorkerStatus(ctx context.Context, w *Worker) error {

@ -60,6 +60,10 @@ func TestFetchWorkerTask(t *testing.T) {
ctx, cancel, db := persistenceTestFixtures(10 * time.Second)
defer cancel()
startTime := time.Date(2024, time.July, 2, 7, 56, 0, 0, time.UTC)
mockNow := startTime
db.gormDB.NowFunc = func() time.Time { return mockNow }
// Worker without task.
w := Worker{
UUID: uuid.New(),
@ -82,13 +86,16 @@ func TestFetchWorkerTask(t *testing.T) {
// Create a job with tasks.
authTask1 := authorTestTask("the task", "blender")
authTask1.UUID = "11111111-1111-4111-1111-111111111111"
authTask2 := authorTestTask("the other task", "blender")
authTask2.UUID = "22222222-2222-4222-2222-222222222222"
jobUUID := "b6a1d859-122f-4791-8b78-b943329a9989"
atj := authorTestJob(jobUUID, "simple-blender-render", authTask1, authTask2)
constructTestJob(ctx, t, db, atj)
assignedTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
require.Equal(t, assignedTask.UUID, authTask1.UUID)
{ // Assigned task should be returned.
foundTask, err := db.FetchWorkerTask(ctx, &w)
@ -110,10 +117,15 @@ func TestFetchWorkerTask(t *testing.T) {
assert.Equal(t, jobUUID, foundTask.Job.UUID, "the job UUID should be returned as well")
}
// Assign another task.
// Assign another task. Since the remainder of this test depends on the order
// of assignment, it is important to forward the mocked clock to keep things
// predictable.
mockNow = mockNow.Add(1 * time.Second)
newlyAssignedTask, err := db.ScheduleTask(ctx, &w)
require.NoError(t, err)
require.NotNil(t, newlyAssignedTask)
require.Equal(t, newlyAssignedTask.UUID, authTask2.UUID)
{ // Newly assigned task should be returned.
foundTask, err := db.FetchWorkerTask(ctx, &w)

@ -166,6 +166,18 @@ func (sm *StateMachine) jobStatusIfAThenB(
return sm.JobStatusChange(ctx, job, thenStatus, reason)
}
// isJobPausingComplete returns true when the job status is pause-requested and there are no more active tasks.
func (sm *StateMachine) isJobPausingComplete(ctx context.Context, job *persistence.Job) (bool, error) {
if job.Status != api.JobStatusPauseRequested {
return false, nil
}
numActive, _, err := sm.persist.CountTasksOfJobInStatus(ctx, job, api.TaskStatusActive)
if err != nil {
return false, err
}
return numActive == 0, nil
}
// updateJobOnTaskStatusCanceled conditionally escalates the cancellation of a task to cancel the job.
func (sm *StateMachine) updateJobOnTaskStatusCanceled(ctx context.Context, logger zerolog.Logger, job *persistence.Job) error {
// If no more tasks can run, cancel the job.
@ -180,6 +192,15 @@ func (sm *StateMachine) updateJobOnTaskStatusCanceled(ctx context.Context, logge
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.
toBePaused, err := sm.isJobPausingComplete(ctx, job)
if err != nil {
return err
}
if toBePaused {
return sm.JobStatusChange(ctx, job, api.JobStatusPaused, "no more active tasks after task cancellation")
}
return nil
}
@ -204,6 +225,16 @@ func (sm *StateMachine) updateJobOnTaskStatusFailed(ctx context.Context, logger
}
// 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")
// Deal with the special case when the job is in pause-requested status.
toBePaused, err := sm.isJobPausingComplete(ctx, job)
if err != nil {
return err
}
if toBePaused {
return sm.JobStatusChange(ctx, job, api.JobStatusPaused, "no more active tasks after task failure")
}
return sm.jobStatusIfAThenB(ctx, logger, job, api.JobStatusQueued, api.JobStatusActive,
"task failed, but not enough to fail the job")
}
@ -218,6 +249,16 @@ func (sm *StateMachine) updateJobOnTaskStatusCompleted(ctx context.Context, logg
logger.Info().Msg("all tasks of job are completed, job is completed")
return sm.JobStatusChange(ctx, job, api.JobStatusCompleted, "all tasks completed")
}
// Deal with the special case when the job is in pause-requested status.
toBePaused, err := sm.isJobPausingComplete(ctx, job)
if err != nil {
return err
}
if toBePaused {
return sm.JobStatusChange(ctx, job, api.JobStatusPaused, "no more active tasks after task completion")
}
logger.Info().
Int("taskNumTotal", numTotal).
Int("taskNumComplete", numComplete).
@ -369,7 +410,7 @@ func (sm *StateMachine) updateTasksAfterJobStatusChange(
// Every case in this switch MUST return, for sanity sake.
switch job.Status {
case api.JobStatusCompleted, api.JobStatusCanceled:
case api.JobStatusCompleted, api.JobStatusCanceled, api.JobStatusPaused:
// Nothing to do; this will happen as a response to all tasks receiving this status.
return tasksUpdateResult{}, nil
@ -385,6 +426,13 @@ func (sm *StateMachine) updateTasksAfterJobStatusChange(
massTaskUpdate: true,
}, err
case api.JobStatusPauseRequested:
jobStatus, err := sm.pauseTasks(ctx, logger, job)
return tasksUpdateResult{
followingJobStatus: jobStatus,
massTaskUpdate: true,
}, err
case api.JobStatusRequeueing:
jobStatus, err := sm.requeueTasks(ctx, logger, job, oldJobStatus)
return tasksUpdateResult{
@ -438,6 +486,38 @@ func (sm *StateMachine) cancelTasks(
return "", nil
}
func (sm *StateMachine) pauseTasks(
ctx context.Context, logger zerolog.Logger, job *persistence.Job,
) (api.JobStatus, error) {
logger.Info().Msg("pausing tasks of job")
// Any task that might run in the future should get paused.
// Active tasks should remain active until finished.
taskStatusesToPause := []api.TaskStatus{
api.TaskStatusQueued,
api.TaskStatusSoftFailed,
}
err := sm.persist.UpdateJobsTaskStatusesConditional(
ctx, job, taskStatusesToPause, api.TaskStatusPaused,
fmt.Sprintf("Manager paused this task because the job got status %q.", job.Status),
)
if err != nil {
return "", fmt.Errorf("pausing tasks of job %s: %w", job.UUID, err)
}
// If pausing was requested, it has now happened, so the job can transition.
toBePaused, err := sm.isJobPausingComplete(ctx, job)
if err != nil {
return "", err
}
if toBePaused {
logger.Info().Msg("all tasks of job paused, job can go to 'paused' status")
return api.JobStatusPaused, nil
}
return api.JobStatusPauseRequested, nil
}
// requeueTasks re-queues all tasks of the job.
//
// This function assumes that the current job status is "requeueing".

@ -336,6 +336,94 @@ func TestJobCancelWithSomeCompletedTasks(t *testing.T) {
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)
task2 := taskOfSameJob(task1, api.TaskStatusQueued)
task3 := taskOfSameJob(task2, api.TaskStatusQueued)
job := task3.Job
mocks.expectSaveJobWithStatus(t, job, api.JobStatusPauseRequested)
// Expect pausing of the job to trigger pausing of all its queued tasks.
mocks.persist.EXPECT().UpdateJobsTaskStatusesConditional(ctx, job,
[]api.TaskStatus{
api.TaskStatusQueued,
api.TaskStatusSoftFailed,
},
api.TaskStatusPaused,
"Manager paused this task because the job got status \"pause-requested\".",
)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, job,
api.TaskStatusActive).
Return(0, 3, nil)
mocks.expectSaveJobWithStatus(t, job, api.JobStatusPaused)
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"))
}
func TestJobPauseWithSomeCompletedTasks(t *testing.T) {
mockCtrl, ctx, sm, mocks := taskStateMachineTestFixtures(t)
defer mockCtrl.Finish()
task1 := taskWithStatus(api.JobStatusQueued, api.TaskStatusCompleted)
task2 := taskOfSameJob(task1, api.TaskStatusQueued)
task3 := taskOfSameJob(task2, api.TaskStatusQueued)
job := task3.Job
mocks.expectSaveJobWithStatus(t, job, api.JobStatusPauseRequested)
// Expect pausing of the job to trigger pausing of all its queued tasks.
mocks.persist.EXPECT().UpdateJobsTaskStatusesConditional(ctx, job,
[]api.TaskStatus{
api.TaskStatusQueued,
api.TaskStatusSoftFailed,
},
api.TaskStatusPaused,
"Manager paused this task because the job got status \"pause-requested\".",
)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, job,
api.TaskStatusActive).
Return(0, 3, nil)
mocks.expectSaveJobWithStatus(t, job, api.JobStatusPaused)
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"))
}
func TestJobPauseWithSomeActiveTasks(t *testing.T) {
mockCtrl, ctx, sm, mocks := taskStateMachineTestFixtures(t)
defer mockCtrl.Finish()
task1 := taskWithStatus(api.JobStatusActive, api.TaskStatusActive)
task2 := taskOfSameJob(task1, api.TaskStatusCompleted)
task3 := taskOfSameJob(task2, api.TaskStatusQueued)
job := task3.Job
mocks.expectSaveJobWithStatus(t, job, api.JobStatusPauseRequested)
// Expect pausing of the job to trigger pausing of all its queued tasks.
mocks.persist.EXPECT().UpdateJobsTaskStatusesConditional(ctx, job,
[]api.TaskStatus{
api.TaskStatusQueued,
api.TaskStatusSoftFailed,
},
api.TaskStatusPaused,
"Manager paused this task because the job got status \"pause-requested\".",
)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, job,
api.TaskStatusActive).
Return(1, 3, nil)
mocks.expectBroadcastJobChangeWithTaskRefresh(job, api.JobStatusActive, api.JobStatusPauseRequested)
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()

@ -82,7 +82,7 @@ func (ou *OutputUploader) process(ctx context.Context, item TaskOutput) {
Str("image", item.Filename).
Str("task", item.TaskID).
Logger()
logger.Info().Msg("output uploader: processing file before uploading to Manager")
logger.Debug().Msg("output uploader: processing file before uploading to Manager")
jpegBytes := loadAsJPEG(item.Filename)
if len(jpegBytes) == 0 {

@ -1680,6 +1680,7 @@ components:
- completed
- failed
- paused
- pause-requested
- queued
- cancel-requested
- requeueing
@ -1866,6 +1867,7 @@ components:
Worker tag that should execute this job. When a tag ID is
given, only Workers in that tag will be scheduled to work on it.
If empty or ommitted, all workers can work on this job.
"initial_status": { $ref: "#/components/schemas/JobStatus" }
required: [name, type, priority, submitter_platform]
example:
type: "simple-blender-render"

@ -19,233 +19,233 @@ import (
var swaggerSpec = []string{
"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",
}
// GetSwagger returns the content of the embedded swagger specification file

@ -84,6 +84,8 @@ const (
JobStatusFailed JobStatus = "failed"
JobStatusPauseRequested JobStatus = "pause-requested"
JobStatusPaused JobStatus = "paused"
JobStatusQueued JobStatus = "queued"
@ -700,6 +702,8 @@ type SocketIOSubscriptionType string
// Job definition submitted to Flamenco.
type SubmittedJob struct {
InitialStatus *JobStatus `json:"initial_status,omitempty"`
// Arbitrary metadata strings. More complex structures can be modeled by using `a.b.c` notation for the key.
Metadata *JobMetadata `json:"metadata,omitempty"`
Name string `json:"name"`

@ -8,6 +8,9 @@
<button class="btn delete dangerous" v-on:click="onButtonDeleteConfirmed">Delete</button>
</div>
</div>
<button class="btn pause" :disabled="!jobs.canPause" v-on:click="onButtonPause">
Pause Job
</button>
<button class="btn cancel" :disabled="!jobs.canCancel" v-on:click="onButtonCancel">
Cancel Job
</button>
@ -69,6 +72,9 @@ export default {
onButtonRequeue() {
return this._handleJobActionPromise(this.jobs.requeueJobs(), 'requeueing');
},
onButtonPause() {
return this._handleJobActionPromise(this.jobs.pauseJobs(), 'marked for pausing');
},
_handleJobActionPromise(promise, description) {
return promise.then(() => {

@ -100,6 +100,9 @@ class Job {
if (data.hasOwnProperty('worker_tag')) {
obj['worker_tag'] = ApiClient.convertToType(data['worker_tag'], 'String');
}
if (data.hasOwnProperty('initial_status')) {
obj['initial_status'] = JobStatus.constructFromObject(data['initial_status']);
}
if (data.hasOwnProperty('id')) {
obj['id'] = ApiClient.convertToType(data['id'], 'String');
}
@ -175,6 +178,11 @@ Job.prototype['storage'] = undefined;
*/
Job.prototype['worker_tag'] = undefined;
/**
* @member {module:model/JobStatus} initial_status
*/
Job.prototype['initial_status'] = undefined;
/**
* UUID of the Job
* @member {String} id
@ -253,6 +261,10 @@ SubmittedJob.prototype['storage'] = undefined;
* @member {String} worker_tag
*/
SubmittedJob.prototype['worker_tag'] = undefined;
/**
* @member {module:model/JobStatus} initial_status
*/
SubmittedJob.prototype['initial_status'] = undefined;
// Implement JobAllOf interface:
/**
* UUID of the Job

@ -54,6 +54,13 @@ export default class JobStatus {
"paused" = "paused";
/**
* value: "pause-requested"
* @const
*/
"pause-requested" = "pause-requested";
/**
* value: "queued"
* @const

@ -12,6 +12,7 @@
*/
import ApiClient from '../ApiClient';
import JobStatus from './JobStatus';
import JobStorageInfo from './JobStorageInfo';
/**
@ -84,6 +85,9 @@ class SubmittedJob {
if (data.hasOwnProperty('worker_tag')) {
obj['worker_tag'] = ApiClient.convertToType(data['worker_tag'], 'String');
}
if (data.hasOwnProperty('initial_status')) {
obj['initial_status'] = JobStatus.constructFromObject(data['initial_status']);
}
}
return obj;
}
@ -141,6 +145,11 @@ SubmittedJob.prototype['storage'] = undefined;
*/
SubmittedJob.prototype['worker_tag'] = undefined;
/**
* @member {module:model/JobStatus} initial_status
*/
SubmittedJob.prototype['initial_status'] = undefined;

@ -33,6 +33,9 @@ export const useJobs = defineStore('jobs', {
canRequeue() {
return this._anyJobWithStatus(['canceled', 'completed', 'failed', 'paused']);
},
canPause() {
return this._anyJobWithStatus(['active', 'queued', 'canceled']);
},
},
actions: {
setIsJobless(isJobless) {
@ -74,6 +77,9 @@ export const useJobs = defineStore('jobs', {
cancelJobs() {
return this._setJobStatus('cancel-requested');
},
pauseJobs() {
return this._setJobStatus('pause-requested');
},
requeueJobs() {
return this._setJobStatus('requeueing');
},

@ -2,6 +2,7 @@ import { defineStore } from 'pinia';
import * as API from '@/manager-api';
import { getAPIClient } from '@/api-client';
import { useJobs } from '@/stores/jobs';
const jobsAPI = new API.JobsApi(getAPIClient());
@ -19,6 +20,21 @@ export const useTasks = defineStore('tasks', {
}),
getters: {
canCancel() {
const jobs = useJobs();
const activeJob = jobs.activeJob;
if (!activeJob) {
console.warn('no active job, unable to determine whether the active task is cancellable');
return false;
}
if (activeJob.status == 'pause-requested') {
// Cancelling a task should not be possible while the job is being paused.
// In the future this might be supported, see issue #104315.
return false;
}
// Allow cancellation for specified task statuses.
return this._anyTaskWithStatus(['queued', 'active', 'soft-failed']);
},
canRequeue() {