influxdb/task/backend/storetest/storetest.go

1036 lines
29 KiB
Go

package storetest
import (
"context"
"fmt"
"math"
"os"
"testing"
"time"
"github.com/google/go-cmp/cmp"
platform "github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/snowflake"
"github.com/influxdata/influxdb/task/backend"
"github.com/influxdata/influxdb/task/options"
)
var idGen = snowflake.NewIDGenerator()
type CreateStoreFunc func(*testing.T) backend.Store
type DestroyStoreFunc func(*testing.T, backend.Store)
type TestFunc func(*testing.T, CreateStoreFunc, DestroyStoreFunc)
// NewStoreTest creates a test function for a given store.
func NewStoreTest(name string, cf CreateStoreFunc, df DestroyStoreFunc, funcNames ...string) func(t *testing.T) {
if len(funcNames) == 0 {
funcNames = []string{
"CreateTask",
"UpdateTask",
"ListTasks",
"FindTask",
"FindMeta",
"FindTaskByIDWithMeta",
"DeleteTask",
"CreateNextRun",
"FinishRun",
"ManuallyRunTimeRange",
}
}
availableFuncs := map[string]TestFunc{
"CreateTask": testStoreCreate,
"UpdateTask": testStoreUpdate,
"ListTasks": testStoreListTasks,
"FindTask": testStoreFindTask,
"FindMeta": testStoreFindMeta,
"FindTaskByIDWithMeta": testStoreFindByIDWithMeta,
"DeleteTask": testStoreDelete,
"CreateNextRun": testStoreCreateNextRun,
"FinishRun": testStoreFinishRun,
"ManuallyRunTimeRange": testStoreManuallyRunTimeRange,
"DeleteOrg": testStoreDeleteOrg,
}
return func(t *testing.T) {
fn := func(funcName string, tf TestFunc) {
t.Run(funcName, func(t *testing.T) {
tf(t, cf, df)
})
}
t.Run(name, func(t *testing.T) {
for _, funcName := range funcNames {
fn(funcName, availableFuncs[funcName])
}
})
}
}
func testStoreCreate(t *testing.T, create CreateStoreFunc, destroy DestroyStoreFunc) {
const script = `option task = {
name: "a task",
cron: "* * * * *",
}
from(bucket:"test") |> range(start:-1h)`
const scriptNoName = `option task = {
cron: "* * * * *",
}
from(bucket:"test") |> range(start:-1h)`
s := create(t)
defer destroy(t, s)
for _, args := range []struct {
caseName string
org platform.ID
script string
status backend.TaskStatus
omitAuthz bool
noerr bool
}{
{caseName: "happy path", org: platform.ID(1), script: script, noerr: true},
{caseName: "missing org", org: platform.ID(0), script: script},
{caseName: "missing name", org: platform.ID(1), script: scriptNoName},
{caseName: "missing authz", org: platform.ID(1), script: script, omitAuthz: true},
{caseName: "missing script", org: platform.ID(1), script: ""},
{caseName: "repeated name and org", org: platform.ID(1), script: script, noerr: true},
{caseName: "explicitly active", org: 1, script: script, status: backend.TaskActive, noerr: true},
{caseName: "explicitly inactive", org: 1, script: script, status: backend.TaskInactive, noerr: true},
{caseName: "invalid status", org: 1, script: script, status: backend.TaskStatus("this is not a valid status")},
} {
t.Run(args.caseName, func(t *testing.T) {
req := backend.CreateTaskRequest{
Org: args.org,
Script: args.script,
Status: args.status,
}
if !args.omitAuthz {
req.AuthorizationID = 54321
}
_, err := s.CreateTask(context.Background(), req)
if args.noerr && err != nil {
t.Fatalf("expected no err but got %v", err)
} else if !args.noerr && err == nil {
t.Fatal("expected error but got nil")
}
})
}
}
func testStoreUpdate(t *testing.T, create CreateStoreFunc, destroy DestroyStoreFunc) {
const script = `option task = {
name: "a task",
cron: "* * * * *",
}
from(bucket:"x") |> range(start:-1h)`
const script2 = `option task = {
name: "a task2",
cron: "* * * * *",
}
from(bucket:"y") |> range(start:-1h)`
const scriptNoName = `option task = {
cron: "* * * * *",
}
from(bucket:"y") |> range(start:-1h)`
t.Run("happy path", func(t *testing.T) {
s := create(t)
defer destroy(t, s)
const origAuthzID = 3
id, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: 1, AuthorizationID: origAuthzID, Script: script})
if err != nil {
t.Fatal(err)
}
// Modify just the script.
res, err := s.UpdateTask(context.Background(), backend.UpdateTaskRequest{ID: id, Script: script2})
if err != nil {
t.Fatal(err)
}
task := res.NewTask
meta := res.NewMeta
if task.Script != script2 {
t.Fatalf("Task didnt update: %s", task.Script)
}
if task.Name != "a task2" {
t.Fatalf("Task didn't update name, expected 'a task2' but got '%s' for task %v", task.Name, task)
}
if meta.Status != string(backend.TaskActive) {
// Other tests explicitly check the initial status against DefaultTaskStatus,
// but in this case we need to be certain of the initial status so we can toggle it correctly.
t.Fatalf("expected task to be created active, got %q", meta.Status)
}
if meta.AuthorizationID != origAuthzID {
t.Fatalf("expected same authorization ID, got %v", meta.AuthorizationID)
}
// Modify just the status.
res, err = s.UpdateTask(context.Background(), backend.UpdateTaskRequest{ID: id, Status: backend.TaskInactive})
if err != nil {
t.Fatal(err)
}
task = res.NewTask
meta = res.NewMeta
if task.Script != script2 {
t.Fatalf("Task script unexpectedly updated: newtask:\n%s\n, oldtask:\n%s, \ndiff:\n %s", task.Script, script2, cmp.Diff(task.Script, script2))
}
if task.Name != "a task2" {
t.Fatalf("Task name unexpectedly updated: %q", task.Name)
}
if meta.Status != string(backend.TaskInactive) {
t.Fatalf("expected task status to be inactive, got %q", meta.Status)
}
// Modify the status to active, and change the script.
res, err = s.UpdateTask(context.Background(), backend.UpdateTaskRequest{ID: id, Status: backend.TaskActive, Script: script})
if err != nil {
t.Fatal(err)
}
task = res.NewTask
meta = res.NewMeta
if task.Script != script {
t.Fatalf("Task script did not update: %s", task.Script)
}
if task.Name != "a task" {
t.Fatalf("Task name did not update: %s", task.Name)
}
if meta.Status != string(backend.TaskActive) {
t.Fatalf("expected task status to be active, got %q", meta.Status)
}
// Modify the status to inactive, and change the script again.
res, err = s.UpdateTask(context.Background(), backend.UpdateTaskRequest{ID: id, Status: backend.TaskInactive, Script: script2})
if err != nil {
t.Fatal(err)
}
task = res.NewTask
meta = res.NewMeta
if task.Script != script2 {
t.Fatalf("Task script did not update: %s", task.Script)
}
if task.Name != "a task2" {
t.Fatalf("Task name did not update: %s", task.Name)
}
if meta.Status != string(backend.TaskInactive) {
t.Fatalf("expected task status to be inactive, got %q", meta.Status)
}
// Modify only the authorization ID.
const newAuthzID = 987654
res, err = s.UpdateTask(context.Background(), backend.UpdateTaskRequest{ID: id, AuthorizationID: newAuthzID})
if err != nil {
t.Fatal(err)
}
meta = res.NewMeta
if meta.AuthorizationID != newAuthzID {
t.Fatalf("expected authz ID to update to %v; got %v", newAuthzID, meta.AuthorizationID)
}
})
for _, args := range []struct {
caseName string
req backend.UpdateTaskRequest
}{
{caseName: "missing id", req: backend.UpdateTaskRequest{Script: script}},
{caseName: "not found", req: backend.UpdateTaskRequest{ID: platform.ID(7123), Script: script}},
{caseName: "missing script and status", req: backend.UpdateTaskRequest{ID: platform.ID(1)}},
{caseName: "missing name", req: backend.UpdateTaskRequest{ID: platform.ID(1), Script: scriptNoName}},
} {
t.Run(args.caseName, func(t *testing.T) {
s := create(t)
defer destroy(t, s)
if _, err := s.UpdateTask(context.Background(), args.req); err == nil {
t.Fatal("expected error but did not receive one")
}
})
}
t.Run("name repetition", func(t *testing.T) {
s := create(t)
defer destroy(t, s)
id1, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: 1, AuthorizationID: 3, Script: script})
if err != nil {
t.Fatal(err)
}
_, err = s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: 1, AuthorizationID: 3, Script: script2})
if err != nil {
t.Fatal(err)
}
if _, err := s.UpdateTask(context.Background(), backend.UpdateTaskRequest{ID: id1, Script: script2}); err != nil {
t.Fatalf("expected to be allowed to reuse name when modifying task, but got %v", err)
}
})
}
func testStoreListTasks(t *testing.T, create CreateStoreFunc, destroy DestroyStoreFunc) {
const scriptFmt = `option task = {
name: "testStoreListTasks %d",
cron: "* * * * *",
}
from(bucket:"test") |> range(start:-1h)`
t.Run("happy path", func(t *testing.T) {
s := create(t)
defer destroy(t, s)
orgID := platform.ID(1)
authzID := platform.ID(3)
id, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: orgID, AuthorizationID: authzID, Script: fmt.Sprintf(scriptFmt, 0)})
if err != nil {
t.Fatal(err)
}
ts, err := s.ListTasks(context.Background(), backend.TaskSearchParams{Org: orgID})
if err != nil {
t.Fatal(err)
}
if len(ts) != 1 {
t.Fatalf("expected 1 result, got %d", len(ts))
}
if ts[0].Task.ID != id {
t.Fatalf("got task ID %v, exp %v", ts[0].Task.ID, id)
}
meta, err := s.FindTaskMetaByID(context.Background(), id)
if err != nil {
t.Fatal(err)
}
if !ts[0].Meta.Equal(*meta) {
t.Fatalf("exp meta %v, got meta %v", *meta, ts[0].Meta)
}
meta, err = s.FindTaskMetaByID(context.Background(), id)
if err != nil {
t.Fatal(err)
}
if !ts[0].Meta.Equal(*meta) {
t.Fatalf("exp meta %v, got meta %v", *meta, ts[0].Meta)
}
ts, err = s.ListTasks(context.Background(), backend.TaskSearchParams{Org: platform.ID(123)})
if err != nil {
t.Fatal(err)
}
if len(ts) > 0 {
t.Fatalf("expected no results for bad org ID, got %d result(s)", len(ts))
}
newID, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: orgID, AuthorizationID: authzID, Script: fmt.Sprintf(scriptFmt, 1), Status: backend.TaskInactive})
if err != nil {
t.Fatal(err)
}
ts, err = s.ListTasks(context.Background(), backend.TaskSearchParams{After: id})
if err != nil {
t.Fatal(err)
}
if len(ts) != 1 {
t.Fatalf("expected 1 result, got %d", len(ts))
}
if ts[0].Task.ID != newID {
t.Fatalf("got task ID %v, exp %v", ts[0].Task.ID, newID)
}
meta, err = s.FindTaskMetaByID(context.Background(), newID)
if err != nil {
t.Fatal(err)
}
if !ts[0].Meta.Equal(*meta) {
t.Fatalf("exp meta %v, got meta %v", *meta, ts[0].Meta)
}
})
t.Run("multiple, large pages", func(t *testing.T) {
if os.Getenv("JENKINS_URL") != "" {
t.Skip("Skipping test that parses a lot of Flux on Jenkins. Unskip when https://github.com/influxdata/platform/issues/484 is fixed.")
}
if testing.Short() {
t.Skip("Skipping test in short mode.")
}
s := create(t)
defer destroy(t, s)
orgID := platform.ID(1)
authzID := platform.ID(3)
type createdTask struct {
id platform.ID
name, script string
}
tasks := make([]createdTask, 150)
const script = `option task = {
name: "my_bucket_%d",
cron: "* * * * *",
}
from(bucket:"my_bucket_%d") |> range(start:-1h)`
for i := range tasks {
tasks[i].name = fmt.Sprintf("my_bucket_%d", i)
tasks[i].script = fmt.Sprintf(script, i, i)
id, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: orgID, AuthorizationID: authzID, Script: tasks[i].script})
if err != nil {
t.Fatalf("failed to create task %d: %v", i, err)
}
tasks[i].id = id
}
for _, p := range []backend.TaskSearchParams{
{Org: orgID, PageSize: 100},
} {
got, err := s.ListTasks(context.Background(), p)
if err != nil {
t.Fatalf("failed to list tasks with search param %v: %v", p, err)
}
if len(got) != 100 {
t.Fatalf("expected 100 returned tasks, got %d", len(got))
}
for i, g := range got {
if tasks[i].id != g.Task.ID {
t.Fatalf("task ID mismatch at index %d: got %x, expected %x", i, g.Task.ID, tasks[i].id)
}
if orgID != g.Task.Org {
t.Fatalf("task org mismatch at index %d: got %x, expected %x", i, g.Task.Org, orgID)
}
if tasks[i].name != g.Task.Name {
t.Fatalf("task name mismatch at index %d: got %q, expected %q", i, g.Task.Name, tasks[i].name)
}
if tasks[i].script != g.Task.Script {
t.Fatalf("task script mismatch at index %d: got %q, expected %q", i, g.Task.Script, tasks[i].script)
}
}
}
})
t.Run("invalid params", func(t *testing.T) {
s := create(t)
defer destroy(t, s)
if _, err := s.ListTasks(context.Background(), backend.TaskSearchParams{PageSize: -1}); err == nil {
t.Fatal("expected error for negative page size but got nil")
}
if _, err := s.ListTasks(context.Background(), backend.TaskSearchParams{PageSize: math.MaxInt32}); err == nil {
t.Fatal("expected error for huge page size but got nil")
}
})
}
func testStoreFindTask(t *testing.T, create CreateStoreFunc, destroy DestroyStoreFunc) {
const script = `option task = {
name: "a task",
cron: "* * * * *",
}
from(bucket:"test") |> range(start:-1h)`
t.Run("happy path", func(t *testing.T) {
s := create(t)
defer destroy(t, s)
org := platform.ID(1)
authz := platform.ID(3)
id, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: org, AuthorizationID: authz, Script: script})
if err != nil {
t.Fatal(err)
}
task, meta, err := s.FindTaskByIDWithMeta(context.Background(), id)
if err != nil {
t.Fatal(err)
}
if task.ID != id {
t.Fatalf("unexpected ID: got %v, exp %v", task.ID, id)
}
if task.Org != org {
t.Fatalf("unexpected org: got %v, exp %v", task.Org, org)
}
if task.Name != "a task" {
t.Fatalf("unexpected name %q", task.Name)
}
if task.Script != script {
t.Fatalf("unexpected script %q", task.Script)
}
if meta.Status != string(backend.DefaultTaskStatus) {
t.Fatalf("unexpected default status: got %q, exp %q", meta.Status, backend.DefaultTaskStatus)
}
badID := id + 1
task, err = s.FindTaskByID(context.Background(), badID)
if err != backend.ErrTaskNotFound {
t.Fatalf("expected %v when finding nonexistent ID, got %v", backend.ErrTaskNotFound, err)
}
if task != nil {
t.Fatalf("expected nil task when finding nonexistent ID, got %#v", task)
}
})
}
func testStoreFindMeta(t *testing.T, create CreateStoreFunc, destroy DestroyStoreFunc) {
const script = `option task = {
name: "a task",
cron: "* * * * *",
concurrency: 3,
offset: 5s,
}
from(bucket:"test") |> range(start:-1h)`
t.Run("happy path", func(t *testing.T) {
s := create(t)
defer destroy(t, s)
org := platform.ID(1)
authz := platform.ID(3)
id, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: org, AuthorizationID: authz, Script: script, ScheduleAfter: 6000})
if err != nil {
t.Fatal(err)
}
meta, err := s.FindTaskMetaByID(context.Background(), id)
if err != nil {
t.Fatal(err)
}
if meta.MaxConcurrency != 3 {
t.Fatal("failed to set max concurrency")
}
if meta.LatestCompleted != 6000 {
t.Fatalf("LatestCompleted should have been set to 6000, got %d", meta.LatestCompleted)
}
if meta.EffectiveCron != "* * * * *" {
t.Fatalf("unexpected cron stored in meta: %q", meta.EffectiveCron)
}
duration := options.Duration{}
if err := duration.Parse(meta.Offset); err != nil {
t.Fatal(err)
}
dur, err := duration.DurationFrom(time.Now()) // is time.Now() the best option here
if err != nil {
t.Fatal(err)
}
if dur != 5*time.Second {
t.Fatalf("unexpected delay stored in meta: %v", meta.Offset)
}
if meta.Status != string(backend.DefaultTaskStatus) {
t.Fatalf("unexpected status: got %v, exp %v", meta.Status, backend.DefaultTaskStatus)
}
badID := platform.ID(0)
meta, err = s.FindTaskMetaByID(context.Background(), badID)
if err == nil {
t.Fatalf("failed to error on bad taskID")
}
if meta != nil {
t.Fatalf("expected nil meta when finding nonexistent ID, got %#v", meta)
}
rc, err := s.CreateNextRun(context.Background(), id, 6065)
if err != nil {
t.Fatal(err)
}
_, err = s.CreateNextRun(context.Background(), id, 6125)
if err != nil {
t.Fatal(err)
}
err = s.FinishRun(context.Background(), id, rc.Created.RunID)
if err != nil {
t.Fatal(err)
}
meta, err = s.FindTaskMetaByID(context.Background(), id)
if err != nil {
t.Fatal(err)
}
if len(meta.CurrentlyRunning) != 1 {
t.Fatal("creating and finishing runs doesn't work")
}
if meta.LatestCompleted != 6060 {
t.Fatalf("expected LatestCompleted to be updated by finished run, but it wasn't; LatestCompleted=%d", meta.LatestCompleted)
}
})
t.Run("explicit status", func(t *testing.T) {
s := create(t)
defer destroy(t, s)
for _, st := range []backend.TaskStatus{backend.TaskActive, backend.TaskInactive} {
id, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: 1, AuthorizationID: 3, Script: script, Status: st})
if err != nil {
t.Fatal(err)
}
meta, err := s.FindTaskMetaByID(context.Background(), id)
if err != nil {
t.Fatal(err)
}
if meta.Status != string(st) {
t.Fatalf("got status %v, exp %v", meta.Status, st)
}
}
})
t.Run("schedule alignment with 'every' option", func(t *testing.T) {
s := create(t)
defer destroy(t, s)
const secondsPerDay = 60 * 60 * 24
const scriptFmt = `
import "http"
option task = {
name: "task with every",
every: %s,
}
from(bucket: "b") |> range(start:-1h) |> http.to(url: "http://example.com")`
for _, tc := range []struct {
e string // every option in task
sa int64 // ScheduleAfter when creating
lc int64 // expected meta.LatestCompleted
}{
{e: "1m", sa: 65, lc: 60},
{e: "1m", sa: 60, lc: 120},
{e: "10s", sa: 27, lc: 20},
{e: "2d", sa: (2 * secondsPerDay) + 1, lc: 2 * secondsPerDay},
} {
script := fmt.Sprintf(scriptFmt, tc.e)
id, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: 1, AuthorizationID: 3, Script: script, ScheduleAfter: tc.sa})
if err != nil {
t.Fatal(err)
}
meta, err := s.FindTaskMetaByID(context.Background(), id)
if err != nil {
t.Fatal(err)
}
if meta.LatestCompleted != tc.lc {
t.Errorf("with every = %q, Create.ScheduleAfter = %d: got LatestCompleted %d, expected %d", tc.e, tc.sa, meta.LatestCompleted, tc.lc)
}
}
})
}
func testStoreFindByIDWithMeta(t *testing.T, create CreateStoreFunc, destroy DestroyStoreFunc) {
const script = `option task = {
name: "a task",
cron: "* * * * *",
concurrency: 3,
offset: 5s,
}
from(bucket:"test") |> range(start:-1h)`
s := create(t)
defer destroy(t, s)
org := platform.ID(1)
authz := platform.ID(3)
id, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: org, AuthorizationID: authz, Script: script, ScheduleAfter: 6000})
if err != nil {
t.Fatal(err)
}
task, meta, err := s.FindTaskByIDWithMeta(context.Background(), id)
if err != nil {
t.Fatal(err)
}
if task.ID != id {
t.Fatalf("unexpected ID: got %v, exp %v", task.ID, id)
}
if task.Org != org {
t.Fatalf("unexpected org: got %v, exp %v", task.Org, org)
}
if task.Name != "a task" {
t.Fatalf("unexpected name %q", task.Name)
}
if task.Script != script {
t.Fatalf("unexpected script %q", task.Script)
}
if meta.MaxConcurrency != 3 {
t.Fatal("failed to set max concurrency")
}
if meta.LatestCompleted != 6000 {
t.Fatalf("LatestCompleted should have been set to 6000, got %d", meta.LatestCompleted)
}
if meta.EffectiveCron != "* * * * *" {
t.Fatalf("unexpected cron stored in meta: %q", meta.EffectiveCron)
}
duration := options.Duration{}
if err := duration.Parse(meta.Offset); err != nil {
t.Fatal(err)
}
if duration.String() != "5s" {
t.Fatalf("unexpected delay stored in meta: %v", meta.Offset)
}
badID := task.ID + 1
task, meta, err = s.FindTaskByIDWithMeta(context.Background(), badID)
if err != backend.ErrTaskNotFound {
t.Fatalf("expected %v when task not found, got %v", backend.ErrTaskNotFound, err)
}
if meta != nil || task != nil {
t.Fatalf("expected nil meta and task when finding nonexistent ID, got meta: %#v, task: %#v", meta, task)
}
}
func testStoreDelete(t *testing.T, create CreateStoreFunc, destroy DestroyStoreFunc) {
const script = `option task = {
name: "a task",
cron: "* * * * *",
}
from(bucket:"test") |> range(start:-1h)`
t.Run("happy path", func(t *testing.T) {
s := create(t)
defer destroy(t, s)
org, authz := idGen.ID(), idGen.ID()
id, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: org, AuthorizationID: authz, Script: script})
if err != nil {
t.Fatal(err)
}
deleted, err := s.DeleteTask(context.Background(), id)
if err != nil {
t.Fatal(err)
}
if !deleted {
t.Fatal("stored task not deleted")
}
// Deleting a nonexistent ID should return false, nil.
deleted, err = s.DeleteTask(context.Background(), id)
if err != nil {
t.Fatal(err)
}
if deleted {
t.Fatal("previously deleted task reported as deleted")
}
// Neither the deleted task nor its meta should not be found.
if _, err := s.FindTaskByID(context.Background(), id); err != backend.ErrTaskNotFound {
t.Fatalf("expected task not to be found, got %v", err)
}
if _, err := s.FindTaskMetaByID(context.Background(), id); err != backend.ErrTaskNotFound {
t.Fatalf("expected task meta not to be found, got %v", err)
}
// It's safe to reuse the same name, for the same org with a new user, after deleting the original.
id, err = s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: org, AuthorizationID: idGen.ID(), Script: script})
if err != nil {
t.Fatalf("Error when reusing task name that was previously deleted: %v", err)
}
if _, err := s.DeleteTask(context.Background(), id); err != nil {
t.Fatal(err)
}
// Reuse the same name, for the original user but a new org.
if _, err = s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: idGen.ID(), AuthorizationID: idGen.ID(), Script: script}); err != nil {
t.Fatalf("Error when reusing task name that was previously deleted: %v", err)
}
})
}
func testStoreCreateNextRun(t *testing.T, create CreateStoreFunc, destroy DestroyStoreFunc) {
const script = `option task = {
name: "a task",
cron: "* * * * *",
offset: 5s,
concurrency: 2,
}
from(bucket:"test") |> range(start:-1h)`
s := create(t)
defer destroy(t, s)
t.Run("no queue", func(t *testing.T) {
taskID, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: 1, AuthorizationID: 3, Script: script, ScheduleAfter: 30})
if err != nil {
t.Fatal(err)
}
badID := uint64(taskID)
badID++
if _, err := s.CreateNextRun(context.Background(), platform.ID(badID), 999); err == nil {
t.Fatal("expected error for CreateNextRun with bad ID, got none")
}
_, err = s.CreateNextRun(context.Background(), taskID, 64)
if e, ok := err.(backend.RunNotYetDueError); !ok {
t.Fatalf("expected RunNotYetDueError, got %v (%T)", err, err)
} else if e.DueAt != 65 {
t.Fatalf("expected run due at 65, got %d", e.DueAt)
}
rc, err := s.CreateNextRun(context.Background(), taskID, 65)
if err != nil {
t.Fatal(err)
}
if rc.Created.TaskID != taskID {
t.Fatalf("bad created task ID; exp %s got %s", taskID, rc.Created.TaskID)
}
if rc.Created.Now != 60 {
t.Fatalf("unexpected time for created run: %d", rc.Created.Now)
}
if rc.NextDue != 125 {
t.Fatalf("unexpected next due time: %d", rc.NextDue)
}
rc, err = s.CreateNextRun(context.Background(), taskID, 125)
if err != nil {
t.Fatal(err)
}
if rc.Created.TaskID != taskID {
t.Fatalf("bad created task ID; exp %x got %x", taskID, rc.Created.TaskID)
}
if rc.Created.Now != 120 {
t.Fatalf("unexpected time for created run: %d", rc.Created.Now)
}
if rc.NextDue != 185 {
t.Fatalf("unexpected next due time: %d", rc.NextDue)
}
})
t.Run("with a queue", func(t *testing.T) {
const script = `option task = {
name: "a task",
cron: "* * * * *",
offset: 5s,
concurrency: 9,
}
from(bucket:"test") |> range(start:-1h)`
taskID, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: 5, AuthorizationID: 7, Script: script, ScheduleAfter: 2999})
if err != nil {
t.Fatal(err)
}
// Task is set to every minute. Should schedule once on 0 and once on 60.
if _, err := s.ManuallyRunTimeRange(context.Background(), taskID, 0, 60, 3000); err != nil {
t.Fatal(err)
}
// Should schedule once exactly on 180.
if _, err := s.ManuallyRunTimeRange(context.Background(), taskID, 180, 180, 3001); err != nil {
t.Fatal(err)
}
rc, err := s.CreateNextRun(context.Background(), taskID, 3005)
if err != nil {
t.Fatal(err)
}
if rc.Created.Now != 3000 {
t.Fatalf("expected run to be created with time 3000, got %d", rc.Created.Now)
}
if rc.NextDue != 3065 {
t.Fatalf("expected next due run to be 3065, got %d", rc.NextDue)
}
if !rc.HasQueue {
t.Fatal("expected run to have queue but it didn't")
}
// Queue: 0
rc, err = s.CreateNextRun(context.Background(), taskID, 3005)
if err != nil {
t.Fatal(err)
}
if rc.Created.Now != 0 {
t.Fatalf("expected run to be scheduled for 0, got %d", rc.Created.Now)
}
if rc.NextDue != 3065 {
// NextDue doesn't change with queue.
t.Fatalf("expected next due run to be 3065, got %d", rc.NextDue)
}
if !rc.HasQueue {
t.Fatal("expected run to have queue but it didn't")
}
// Queue: 60
rc, err = s.CreateNextRun(context.Background(), taskID, 3005)
if err != nil {
t.Fatal(err)
}
if rc.Created.Now != 60 {
t.Fatalf("expected run to be scheduled for 0, got %d", rc.Created.Now)
}
if rc.NextDue != 3065 {
// NextDue doesn't change with queue.
t.Fatalf("expected next due run to be 3065, got %d", rc.NextDue)
}
if !rc.HasQueue {
t.Fatal("expected run to have queue but it didn't")
}
// Queue: 180
rc, err = s.CreateNextRun(context.Background(), taskID, 3005)
if err != nil {
t.Fatal(err)
}
if rc.Created.Now != 180 {
t.Fatalf("expected run to be scheduled for 0, got %d", rc.Created.Now)
}
if rc.NextDue != 3065 {
// NextDue doesn't change with queue.
t.Fatalf("expected next due run to be 3065, got %d", rc.NextDue)
}
if rc.HasQueue {
t.Fatal("expected run to have empty queue but it didn't")
}
})
}
func testStoreFinishRun(t *testing.T, create CreateStoreFunc, destroy DestroyStoreFunc) {
const script = `option task = {
name: "a task",
cron: "* * * * *",
}
from(bucket:"test") |> range(start:-1h)`
s := create(t)
defer destroy(t, s)
task, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: 1, AuthorizationID: 3, Script: script})
if err != nil {
t.Fatal(err)
}
rc, err := s.CreateNextRun(context.Background(), task, 60)
if err != nil {
t.Fatal(err)
}
if err := s.FinishRun(context.Background(), task, rc.Created.RunID); err != nil {
t.Fatal(err)
}
if err := s.FinishRun(context.Background(), task, rc.Created.RunID); err == nil {
t.Fatal("expected failure when removing run that doesnt exist")
}
}
func testStoreManuallyRunTimeRange(t *testing.T, create CreateStoreFunc, destroy DestroyStoreFunc) {
const script = `option task = {
name: "a task",
cron: "* * * * *",
}
from(bucket:"test") |> range(start:-1h)`
s := create(t)
defer destroy(t, s)
taskID, err := s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: 1, AuthorizationID: 3, Script: script})
if err != nil {
t.Fatal(err)
}
if _, err := s.ManuallyRunTimeRange(context.Background(), taskID, 1, 10, 0); err != nil {
t.Fatal(err)
}
meta, err := s.FindTaskMetaByID(context.Background(), taskID)
if err != nil {
t.Fatal(err)
}
if len(meta.ManualRuns) != 1 {
t.Fatalf("expected 1 manual run to be created, got %d", len(meta.ManualRuns))
}
rc, err := s.CreateNextRun(context.Background(), taskID, 9999)
if err != nil {
t.Fatal(err)
}
if !rc.HasQueue {
t.Fatal("CreateNextRun should have reported that there is a queue")
}
}
func testStoreDeleteOrg(t *testing.T, create CreateStoreFunc, destroy DestroyStoreFunc) {
s := create(t)
defer destroy(t, s)
ids := createABunchOFTasks(t, s,
func(o uint64) bool {
return o == 1
},
)
org := platform.ID(1)
err := s.DeleteOrg(context.Background(), org)
if err != nil {
t.Fatal(err)
}
for i := range ids {
task, err := s.FindTaskByID(context.Background(), ids[i])
if err != nil {
t.Fatal(err)
}
if task != nil {
t.Fatal("expected task to be deleted but it was not")
}
}
}
func createABunchOFTasks(t *testing.T, s backend.Store, filter func(org uint64) bool) []platform.ID {
const script = `option task = {
name: "a task",
cron: "* * * * *",
}
from(bucket:"test") |> range(start:-1h)`
var id platform.ID
var ids []platform.ID
var err error
for i := 0; i < 15; i++ {
for orgInt := uint64(1); orgInt < 4; orgInt++ {
org := platform.ID(orgInt)
if id, err = s.CreateTask(context.Background(), backend.CreateTaskRequest{Org: org, Script: script}); err != nil {
t.Fatal(err)
}
if filter(orgInt) {
ids = append(ids, id)
}
}
}
return ids
}