Files
fleet/server/datastore/mysql/jobs_test.go
T
Scott Gress 1ae5167a06 Allow worker to filter queue by job type (#31556)
for #31518 

# Details

This PR updates the jobs package with a new `GetFilteredQueuedJobs`
method which accepts a list of job names to return from the `jobs`
table, rather than returning all jobs matching the other criteria. It
also updates the `worker` package to use this new method instead of
`GetQueuedJobs`.

The purpose of this update is to allow us to add multiple workers which
process different kinds of jobs, while sharing the same `jobs` table and
framework.

I chose to add the new `GetFilteredQueuedJobs` method rather than
updating `GetQueuedJobs` because the latter is used in a bunch of tests
that would need to be updated. `GetQueuedJobs` now calls
`GetFilteredQueuedJobs` with an empty list, which is interpreted as
"return all jobs".

# Checklist for submitter

## Testing

- [X] Added/updated automated tests

- [ ] QA'd all new/changed functionality manually
There shouldn't be any functional change here, but if someone has a Jira
or Zendesk integration set up to easily test with, that'd be good.
2025-08-06 17:22:48 -05:00

208 lines
6.1 KiB
Go

package mysql
import (
"context"
"fmt"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/require"
)
func TestJobs(t *testing.T) {
ds := CreateMySQLDS(t)
// call TruncateTables before the first test, because a DB migation may have
// created job entries.
TruncateTables(t, ds)
cases := []struct {
name string
fn func(t *testing.T, ds *Datastore)
}{
{"QueueAndProcessJobs", testQueueAndProcessJobs},
{"QueueAndProcessJobs", testQueueAndProcessFilteredJobs},
{"CleanupWorkerJobs", testCleanupWorkerJobs},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer TruncateTables(t, ds)
c.fn(t, ds)
})
}
}
func testQueueAndProcessJobs(t *testing.T, ds *Datastore) {
ctx := context.Background()
// no jobs yet
jobs, err := ds.GetQueuedJobs(ctx, 10, time.Time{})
require.NoError(t, err)
require.Empty(t, jobs)
// add a couple of jobs, one immediate, one delayed
j1 := &fleet.Job{Name: "j1", State: fleet.JobStateQueued}
j2 := &fleet.Job{Name: "j2", State: fleet.JobStateQueued, NotBefore: time.Now().Add(time.Hour)}
j1, err = ds.NewJob(ctx, j1)
require.NoError(t, err)
require.NotZero(t, j1.ID)
j2, err = ds.NewJob(ctx, j2)
require.NoError(t, err)
require.NotZero(t, j2.ID)
// only j1 is returned
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Time{})
require.NoError(t, err)
require.Len(t, jobs, 1)
require.Equal(t, j1.ID, jobs[0].ID)
require.NotZero(t, jobs[0].NotBefore)
require.False(t, jobs[0].NotBefore.After(time.Now())) // before or equal
// update j1 as successful
j1.State = fleet.JobStateSuccess
_, err = ds.UpdateJob(ctx, j1.ID, j1)
require.NoError(t, err)
// no jobs queued for now
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Time{})
require.NoError(t, err)
require.Empty(t, jobs)
// update j2 not before timestamp to now (-1s just to be safe)
j2.NotBefore = time.Now().Add(-time.Second)
_, err = ds.UpdateJob(ctx, j2.ID, j2)
require.NoError(t, err)
// j2 is returned
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Time{})
require.NoError(t, err)
require.Len(t, jobs, 1)
require.Equal(t, j2.ID, jobs[0].ID)
require.NotZero(t, jobs[0].NotBefore)
require.False(t, jobs[0].NotBefore.After(time.Now())) // before or equal
}
func testQueueAndProcessFilteredJobs(t *testing.T, ds *Datastore) {
ctx := context.Background()
// no jobs yet
jobs, err := ds.GetQueuedJobs(ctx, 10, time.Time{})
require.NoError(t, err)
require.Empty(t, jobs)
// add a couple of jobs
j1 := &fleet.Job{Name: "j1", State: fleet.JobStateQueued}
j2 := &fleet.Job{Name: "j2", State: fleet.JobStateQueued}
j1, err = ds.NewJob(ctx, j1)
require.NoError(t, err)
require.NotZero(t, j1.ID)
j2, err = ds.NewJob(ctx, j2)
require.NoError(t, err)
require.NotZero(t, j2.ID)
// only j1 is returned
jobs, err = ds.GetFilteredQueuedJobs(ctx, 10, time.Time{}, []string{"j1"})
require.NoError(t, err)
require.Len(t, jobs, 1)
require.Equal(t, j1.ID, jobs[0].ID)
require.NotZero(t, jobs[0].NotBefore)
require.False(t, jobs[0].NotBefore.After(time.Now())) // before or equal
// both returned
jobs, err = ds.GetFilteredQueuedJobs(ctx, 10, time.Time{}, []string{"j1", "j2"})
require.NoError(t, err)
require.Len(t, jobs, 2)
require.Equal(t, j1.ID, jobs[0].ID)
require.NotZero(t, jobs[0].NotBefore)
require.False(t, jobs[0].NotBefore.After(time.Now()))
require.Equal(t, j2.ID, jobs[1].ID)
require.NotZero(t, jobs[1].NotBefore)
require.False(t, jobs[1].NotBefore.After(time.Now()))
}
func testCleanupWorkerJobs(t *testing.T, ds *Datastore) {
ctx := t.Context()
// no job yet
n, err := ds.CleanupWorkerJobs(ctx, time.Second, time.Second)
require.NoError(t, err)
require.EqualValues(t, 0, n)
setJobTimestamp := func(j *fleet.Job, subtract time.Duration) {
j.NotBefore = time.Now().UTC().Add(-subtract)
_, err = ds.UpdateJob(ctx, j.ID, j)
require.NoError(t, err)
}
setJobStatus := func(j *fleet.Job, state fleet.JobState) {
j.State = state
_, err = ds.UpdateJob(ctx, j.ID, j)
require.NoError(t, err)
}
// enqueue a job
j1 := &fleet.Job{Name: "j1", State: fleet.JobStateQueued}
j1, err = ds.NewJob(ctx, j1)
require.NoError(t, err)
setJobTimestamp(j1, 2*time.Second)
// still clears nothing as it is not in a final state
n, err = ds.CleanupWorkerJobs(ctx, time.Second, time.Second)
require.NoError(t, err)
require.EqualValues(t, 0, n)
// job is still returned as a queued job
jobs, err := ds.GetQueuedJobs(ctx, 10, time.Time{})
require.NoError(t, err)
require.Len(t, jobs, 1)
require.Equal(t, j1.ID, jobs[0].ID)
// mark it as done
setJobStatus(j1, fleet.JobStateSuccess)
// does not clear it if the completed duration is not far enough in the past
n, err = ds.CleanupWorkerJobs(ctx, time.Second, time.Minute)
require.NoError(t, err)
require.EqualValues(t, 0, n)
// does clear it if the completed duration is far enough in the past
n, err = ds.CleanupWorkerJobs(ctx, time.Second, time.Second)
require.NoError(t, err)
require.EqualValues(t, 1, n)
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Time{})
require.NoError(t, err)
require.Len(t, jobs, 0)
// enqueue a few more jobs
queuedJobs := make([]*fleet.Job, 0, 4)
for i := 0; i < 4; i++ {
j := &fleet.Job{Name: "j" + fmt.Sprint(i+1), State: fleet.JobStateQueued}
j, err = ds.NewJob(ctx, j)
require.NoError(t, err)
setJobTimestamp(j, time.Duration(i+1)*time.Minute)
queuedJobs = append(queuedJobs, j)
}
// make jobs[1] and jobs[3] failed, jobs[2] successful, jobs[0] queued
setJobStatus(queuedJobs[1], fleet.JobStateFailure) // failed 2m ago
setJobStatus(queuedJobs[3], fleet.JobStateFailure) // failed 4m ago
setJobStatus(queuedJobs[2], fleet.JobStateSuccess) // successful 3m ago
// cleanup failed > 3m, success > 1m, should delete jobs[3] and jobs[2]
n, err = ds.CleanupWorkerJobs(ctx, 3*time.Minute, time.Minute)
require.NoError(t, err)
require.EqualValues(t, 2, n)
// cleanup failed > 1m, success > 10m, should delete jobs[1]
n, err = ds.CleanupWorkerJobs(ctx, 1*time.Minute, 10*time.Minute)
require.NoError(t, err)
require.EqualValues(t, 1, n)
// jobs[0] is still queued
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Time{})
require.NoError(t, err)
require.Len(t, jobs, 1)
require.Equal(t, queuedJobs[0].ID, jobs[0].ID)
}