
Add a new API operation to get the overall farm status. This is based on the jobs and workers, and their status. The statuses are: - `active`: Actively working on jobs. - `idle`: Farm could be active, but has no work to do. - `waiting`: Work has been queued, but all workers are asleep. - `asleep`: Farm is idle, and all workers are asleep. - `inoperative`: Cannot work: no workers, or all are offline/error. - `starting`: Farm is starting up. - `unknown`: Unexpected configuration of worker and job statuses.
202 lines
6.1 KiB
Go
202 lines
6.1 KiB
Go
// Package persistence provides the database interface for Flamenco Manager.
|
|
package persistence
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"projects.blender.org/studio/flamenco/internal/manager/job_compilers"
|
|
"projects.blender.org/studio/flamenco/internal/uuid"
|
|
"projects.blender.org/studio/flamenco/pkg/api"
|
|
)
|
|
|
|
func TestSimpleQuery(t *testing.T) {
|
|
ctx, close, db, job, _ := jobTasksTestFixtures(t)
|
|
defer close()
|
|
|
|
// Sanity check.
|
|
if !assert.Equal(t, api.JobStatusUnderConstruction, job.Status, "check job status is as expected") {
|
|
t.FailNow()
|
|
}
|
|
|
|
// Check empty result when querying for other status.
|
|
result, err := db.QueryJobs(ctx, api.JobsQuery{
|
|
StatusIn: &[]api.JobStatus{api.JobStatusActive, api.JobStatusCanceled},
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.Len(t, result, 0)
|
|
|
|
// Check job was returned properly on correct status.
|
|
result, err = db.QueryJobs(ctx, api.JobsQuery{
|
|
StatusIn: &[]api.JobStatus{api.JobStatusUnderConstruction, api.JobStatusCanceled},
|
|
})
|
|
assert.NoError(t, err)
|
|
if !assert.Len(t, result, 1) {
|
|
t.FailNow()
|
|
}
|
|
assert.Equal(t, job.ID, result[0].ID)
|
|
|
|
}
|
|
|
|
func TestQueryMetadata(t *testing.T) {
|
|
ctx, close, db := persistenceTestFixtures(t, 0)
|
|
defer close()
|
|
|
|
testJob := persistAuthoredJob(t, ctx, db, createTestAuthoredJobWithTasks())
|
|
|
|
otherAuthoredJob := createTestAuthoredJobWithTasks()
|
|
otherAuthoredJob.Status = api.JobStatusActive
|
|
otherAuthoredJob.Tasks = []job_compilers.AuthoredTask{}
|
|
otherAuthoredJob.JobID = "138678c8-efd0-452b-ac05-397ff4c02b26"
|
|
otherAuthoredJob.Metadata["project"] = "Other Project"
|
|
otherJob := persistAuthoredJob(t, ctx, db, otherAuthoredJob)
|
|
|
|
var (
|
|
result []*Job
|
|
err error
|
|
)
|
|
|
|
// Check empty result when querying for specific metadata:
|
|
result, err = db.QueryJobs(ctx, api.JobsQuery{
|
|
Metadata: &api.JobsQuery_Metadata{
|
|
AdditionalProperties: map[string]string{
|
|
"project": "Secret Future Project",
|
|
}}})
|
|
assert.NoError(t, err)
|
|
assert.Len(t, result, 0)
|
|
|
|
// Check job was returned properly when querying for the right project.
|
|
result, err = db.QueryJobs(ctx, api.JobsQuery{
|
|
Metadata: &api.JobsQuery_Metadata{
|
|
AdditionalProperties: map[string]string{
|
|
"project": testJob.Metadata["project"],
|
|
}}})
|
|
assert.NoError(t, err)
|
|
if !assert.Len(t, result, 1) {
|
|
t.FailNow()
|
|
}
|
|
assert.Equal(t, testJob.ID, result[0].ID)
|
|
|
|
// Check for the other job
|
|
result, err = db.QueryJobs(ctx, api.JobsQuery{
|
|
Metadata: &api.JobsQuery_Metadata{
|
|
AdditionalProperties: map[string]string{
|
|
"project": otherJob.Metadata["project"],
|
|
}}})
|
|
assert.NoError(t, err)
|
|
if !assert.Len(t, result, 1) {
|
|
t.FailNow()
|
|
}
|
|
assert.Equal(t, otherJob.ID, result[0].ID)
|
|
|
|
// Check job was returned properly when querying for empty metadata.
|
|
result, err = db.QueryJobs(ctx, api.JobsQuery{
|
|
OrderBy: &[]string{"status"},
|
|
Metadata: &api.JobsQuery_Metadata{AdditionalProperties: map[string]string{}},
|
|
})
|
|
assert.NoError(t, err)
|
|
if !assert.Len(t, result, 2) {
|
|
t.FailNow()
|
|
}
|
|
// 'active' should come before 'under-construction':
|
|
assert.Equal(t, otherJob.ID, result[0].ID, "status is %s", result[0].Status)
|
|
assert.Equal(t, testJob.ID, result[1].ID, "status is %s", result[1].Status)
|
|
}
|
|
|
|
func TestQueryJobTaskSummaries(t *testing.T) {
|
|
ctx, close, db, job, authoredJob := jobTasksTestFixtures(t)
|
|
defer close()
|
|
|
|
expectTaskUUIDs := map[string]bool{}
|
|
for _, task := range authoredJob.Tasks {
|
|
expectTaskUUIDs[task.UUID] = true
|
|
}
|
|
|
|
// Create another test job, just to check we get the right tasks back.
|
|
otherAuthoredJob := createTestAuthoredJobWithTasks()
|
|
otherAuthoredJob.Status = api.JobStatusActive
|
|
for i := range otherAuthoredJob.Tasks {
|
|
otherAuthoredJob.Tasks[i].UUID = uuid.New()
|
|
otherAuthoredJob.Tasks[i].Dependencies = []*job_compilers.AuthoredTask{}
|
|
}
|
|
otherAuthoredJob.JobID = "138678c8-efd0-452b-ac05-397ff4c02b26"
|
|
otherAuthoredJob.Metadata["project"] = "Other Project"
|
|
persistAuthoredJob(t, ctx, db, otherAuthoredJob)
|
|
|
|
// Sanity check for the above code, there should be 6 tasks overall, 3 per job.
|
|
var numTasks int64
|
|
tx := db.gormDB.Model(&Task{}).Count(&numTasks)
|
|
assert.NoError(t, tx.Error)
|
|
assert.Equal(t, int64(6), numTasks)
|
|
|
|
// Get the task summaries of a particular job.
|
|
summaries, err := db.QueryJobTaskSummaries(ctx, job.UUID)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, summaries, len(expectTaskUUIDs))
|
|
for _, summary := range summaries {
|
|
assert.True(t, expectTaskUUIDs[summary.UUID], "%q should be in %v", summary.UUID, expectTaskUUIDs)
|
|
}
|
|
}
|
|
|
|
func TestSummarizeJobStatuses(t *testing.T) {
|
|
ctx, close, db, job1, authoredJob1 := jobTasksTestFixtures(t)
|
|
defer close()
|
|
|
|
// Create another job
|
|
authoredJob2 := duplicateJobAndTasks(authoredJob1)
|
|
job2 := persistAuthoredJob(t, ctx, db, authoredJob2)
|
|
|
|
// Test the summary.
|
|
summary, err := db.SummarizeJobStatuses(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, JobStatusCount{api.JobStatusUnderConstruction: 2}, summary)
|
|
|
|
// Change the jobs so that each has a unique status.
|
|
job1.Status = api.JobStatusQueued
|
|
require.NoError(t, db.SaveJobStatus(ctx, job1))
|
|
job2.Status = api.JobStatusFailed
|
|
require.NoError(t, db.SaveJobStatus(ctx, job2))
|
|
|
|
// Test the summary.
|
|
summary, err = db.SummarizeJobStatuses(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, JobStatusCount{
|
|
api.JobStatusQueued: 1,
|
|
api.JobStatusFailed: 1,
|
|
}, summary)
|
|
|
|
// Delete all jobs.
|
|
require.NoError(t, db.DeleteJob(ctx, job1.UUID))
|
|
require.NoError(t, db.DeleteJob(ctx, job2.UUID))
|
|
|
|
// Test the summary.
|
|
summary, err = db.SummarizeJobStatuses(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, JobStatusCount{}, summary)
|
|
}
|
|
|
|
// Check that a context timeout can be detected by inspecting the
|
|
// returned error.
|
|
func TestSummarizeJobStatusesTimeout(t *testing.T) {
|
|
ctx, close, db, _, _ := jobTasksTestFixtures(t)
|
|
defer close()
|
|
|
|
subCtx, subCtxCancel := context.WithTimeout(ctx, 1*time.Nanosecond)
|
|
defer subCtxCancel()
|
|
|
|
// Force a timeout of the context. And yes, even when a nanosecond is quite
|
|
// short, it is still necessary to wait.
|
|
time.Sleep(2 * time.Nanosecond)
|
|
|
|
summary, err := db.SummarizeJobStatuses(subCtx)
|
|
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
|
assert.Nil(t, summary)
|
|
}
|