Before this change, upon the completion of a job run, the scheduler would update a job_run entry with the completed status as well as the number of objects affected by the run. Additionally, we then had a cleaner job whose scope was deleting these completed entries from job_run. This commit changes this design to drop the cleaner job the and rolls its functionality directly into the scheduler. Now, when any given job run is complete, the scheduler will automatically delete it from the job_run table itself. In addition, the completed status has been removed as a valid status as it is not set anymore. This change follows a decision that given the data in these job_run entries is already being deleted by the cleaner job, updating its state when a job completes just to have it deleted is not efficient. Additionally, no use-case for this data was surfaced.
213 lines
6.5 KiB
Go
213 lines
6.5 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package job
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"github.com/hashicorp/boundary/internal/iam"
|
|
"github.com/hashicorp/boundary/internal/kms"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestJobWorkflow(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
assert, require := assert.New(t), require.New(t)
|
|
|
|
conn, _ := db.TestSetup(t, "postgres")
|
|
rw := db.New(conn)
|
|
wrapper := db.TestWrapper(t)
|
|
kms := kms.TestKms(t, conn, wrapper)
|
|
iam.TestRepo(t, conn, wrapper)
|
|
|
|
server := testController(t, conn, wrapper)
|
|
|
|
repo, err := NewRepository(ctx, rw, rw, kms)
|
|
require.NoError(err)
|
|
|
|
job, err := repo.UpsertJob(ctx, "job1", "description")
|
|
require.NoError(err)
|
|
require.NotNil(job)
|
|
assert.Equal(defaultPluginId, job.PluginId)
|
|
|
|
runs, err := repo.RunJobs(ctx, server.PrivateId)
|
|
require.NoError(err)
|
|
assert.Len(runs, 1)
|
|
run := runs[0]
|
|
assert.NotEmpty(run.PrivateId)
|
|
assert.Equal(job.Name, run.JobName)
|
|
|
|
run, err = repo.UpdateProgress(ctx, run.PrivateId, 100, 110, 0)
|
|
require.NoError(err)
|
|
assert.Equal(uint32(100), run.CompletedCount)
|
|
assert.Equal(uint32(110), run.TotalCount)
|
|
|
|
// The only available job is already running, a request for work should return nil
|
|
newRuns, err := repo.RunJobs(ctx, server.PrivateId)
|
|
require.NoError(err)
|
|
assert.Nil(newRuns)
|
|
|
|
err = repo.CompleteRun(ctx, run.PrivateId, time.Hour)
|
|
require.NoError(err)
|
|
|
|
job, err = repo.LookupJob(ctx, job.Name)
|
|
require.NoError(err)
|
|
assert.NotNil(job)
|
|
|
|
// The only available job has a next run in the future, a request for work should return nil
|
|
newRuns, err = repo.RunJobs(ctx, server.PrivateId)
|
|
require.NoError(err)
|
|
assert.Nil(newRuns)
|
|
|
|
// Update job next run to time in past
|
|
job, err = repo.UpdateJobNextRunInAtLeast(ctx, job.Name, 0)
|
|
require.NoError(err)
|
|
|
|
// Now that next scheduled time is in past, a request for work should return a Run
|
|
newRuns, err = repo.RunJobs(ctx, server.PrivateId)
|
|
require.NoError(err)
|
|
require.Len(newRuns, 1)
|
|
newRun := newRuns[0]
|
|
require.NotEmpty(newRun.PrivateId)
|
|
assert.Equal(job.Name, newRun.JobName)
|
|
assert.NotEqual(run.PrivateId, newRun.PrivateId)
|
|
|
|
// The only available job is already running, a request for work should return nil
|
|
newRuns, err = repo.RunJobs(ctx, server.PrivateId)
|
|
require.NoError(err)
|
|
assert.Nil(newRuns)
|
|
|
|
newRun, err = repo.FailRun(ctx, newRun.PrivateId, 0, 0, 0)
|
|
require.NoError(err)
|
|
assert.Equal(Failed.string(), newRun.Status)
|
|
|
|
// Run failed so the job should be available for work immediately
|
|
newRuns, err = repo.RunJobs(ctx, server.PrivateId)
|
|
require.NoError(err)
|
|
assert.Len(newRuns, 1)
|
|
}
|
|
|
|
// TODO (LCR): This test can be dropped once plugin support is plumbed through job repo and scheduler
|
|
func TestPlugin(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("plugin-unique", func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
|
|
conn, _ := db.TestSetup(t, "postgres")
|
|
rw := db.New(conn)
|
|
|
|
rows, err := rw.Query(context.Background(), upsertJobQuery, []any{
|
|
sql.Named("plugin_id", defaultPluginId),
|
|
sql.Named("name", "same-job-name"),
|
|
sql.Named("description", "description"),
|
|
sql.Named("next_scheduled_run", 0),
|
|
})
|
|
require.NoError(err)
|
|
_ = rows.Close()
|
|
|
|
rows, err = rw.Query(context.Background(), "select * from job;", nil)
|
|
require.NoError(err)
|
|
var numRows int
|
|
for rows.Next() {
|
|
numRows++
|
|
}
|
|
require.NoError(rows.Err())
|
|
_ = rows.Close()
|
|
require.Equal(1, numRows)
|
|
|
|
// Calling upsertJob with same name and pluginId should not insert a new job
|
|
rows, err = rw.Query(context.Background(), upsertJobQuery, []any{
|
|
sql.Named("plugin_id", defaultPluginId),
|
|
sql.Named("name", "same-job-name"),
|
|
sql.Named("description", "description"),
|
|
sql.Named("next_scheduled_run", 0),
|
|
})
|
|
require.NoError(err)
|
|
_ = rows.Close()
|
|
|
|
// Validate there is still only 1 job in the database
|
|
rows, err = rw.Query(context.Background(), "select * from job;", nil)
|
|
require.NoError(err)
|
|
numRows = 0
|
|
for rows.Next() {
|
|
numRows++
|
|
}
|
|
require.NoError(rows.Err())
|
|
_ = rows.Close()
|
|
require.Equal(1, numRows)
|
|
|
|
// Create test plugin id
|
|
testPluginId := "pi_test1234"
|
|
numRows, err = rw.Exec(context.Background(), "insert into plugin(public_id, scope_id) values (?, 'global')", []any{testPluginId})
|
|
require.NoError(err)
|
|
assert.Equal(1, numRows)
|
|
|
|
// Calling upsertJob with the same name and different pluginId should create a new job
|
|
rows, err = rw.Query(context.Background(), upsertJobQuery, []any{
|
|
sql.Named("plugin_id", testPluginId),
|
|
sql.Named("name", "same-job-name"),
|
|
sql.Named("description", "description"),
|
|
sql.Named("next_scheduled_run", 0),
|
|
})
|
|
assert.NoError(err)
|
|
_ = rows.Close()
|
|
|
|
// Validate there are 2 jobs in the database
|
|
rows, err = rw.Query(context.Background(), "select * from job;", nil)
|
|
require.NoError(err)
|
|
numRows = 0
|
|
for rows.Next() {
|
|
numRows++
|
|
}
|
|
require.NoError(rows.Err())
|
|
_ = rows.Close()
|
|
require.Equal(2, numRows)
|
|
})
|
|
t.Run("plugin-id-immutable", func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
|
|
conn, _ := db.TestSetup(t, "postgres")
|
|
rw := db.New(conn)
|
|
|
|
// Create test plugin id
|
|
testPluginId := "pi_test1234"
|
|
numRows, err := rw.Exec(context.Background(), "insert into plugin(public_id, scope_id) values (?, 'global')", []any{testPluginId})
|
|
assert.NoError(err)
|
|
assert.Equal(1, numRows)
|
|
|
|
newPluginId := "pi_newtest1234"
|
|
numUpdated, err := rw.Exec(context.Background(), "update plugin set public_id = ? where public_id = ?", []any{newPluginId, testPluginId})
|
|
require.Error(err)
|
|
assert.Equal("db.Exec: immutable column: plugin.public_id: integrity violation: error #1003", err.Error())
|
|
assert.Equal(0, numUpdated)
|
|
})
|
|
t.Run("default-plugin-deletion-disallowed", func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
|
|
conn, _ := db.TestSetup(t, "postgres")
|
|
rw := db.New(conn)
|
|
|
|
numDeleted, err := rw.Exec(context.Background(), "delete from plugin where public_id = ?", []any{defaultPluginId})
|
|
require.Error(err)
|
|
assert.Equal("db.Exec: deletion of system plugin not allowed: integrity violation: error #1104", err.Error())
|
|
assert.Equal(0, numDeleted)
|
|
|
|
// Create test plugin id
|
|
testPluginId := "pi_test1234"
|
|
numRows, err := rw.Exec(context.Background(), "insert into plugin(public_id, scope_id) values (?, 'global')", []any{testPluginId})
|
|
assert.NoError(err)
|
|
assert.Equal(1, numRows)
|
|
|
|
numDeleted, err = rw.Exec(context.Background(), "delete from plugin where public_id = ?", []any{testPluginId})
|
|
assert.NoError(err)
|
|
assert.Equal(1, numDeleted)
|
|
})
|
|
}
|