package orbit import ( "context" "encoding/json" "errors" "testing" "time" ) func TestIntegrationScheduleLifecycle(t *testing.T) { api, queue, _ := integrationWorkerClient(t) ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second) defer cancel() name := "scheduled." + workerTestID(t) w, err := NewWorker(api, WorkerOptions{QueueID: queue, Capacity: 1, Handlers: []WorkerHandler{runtimeHandler(name, func(ctx context.Context, e WorkerExecution) (json.RawMessage, error) { return e.Task.Payload, nil })}}) if err != nil { t.Fatal(err) } run, stop := context.WithCancel(ctx) defer stop() done := make(chan error, 1) go func() { done <- w.Run(run) }() ended := false defer func() { stop() if !ended { <-done } }() start := time.Now().UTC().Truncate(time.Microsecond).Add(1500 * time.Millisecond) in := CreateScheduleRequest{Name: name, Paused: true, Definition: ScheduleDefinition{Rule: ScheduleRule{Kind: "FIXED_RATE", Timezone: "UTC", StartAt: start, IntervalMS: 1000}, Policy: SchedulePolicy{Misfire: "COALESCE", Overlap: "SERIAL", CatchupLimit: 2, CatchupWindowMS: 60000, GraceMS: 1000}, Task: SubmitTaskRequest{QueueID: queue, Handler: name, HandlerVersion: "v1", PayloadVersion: "v1", Payload: json.RawMessage(`{"n":9007199254740993}`)}}} key := "schedule-" + workerTestID(t) accepted, err := api.CreateSchedule(ctx, key, in) if err != nil { t.Fatal(err) } duplicate, err := api.CreateSchedule(ctx, key, in) if err != nil || !duplicate.Deduplicated || duplicate.Schedule.ID != accepted.Schedule.ID { t.Fatal("schedule submit replay", err) } resumed, err := api.ResumeSchedule(ctx, accepted.Schedule.ID, workerTestID(t), ControlScheduleRequest{ExpectedRevision: accepted.Schedule.Revision, Reason: "run integration schedule"}) if err != nil { t.Fatal(err) } var occurrence ScheduleOccurrence for occurrence.TaskID == nil { page, err := api.ListScheduleOccurrences(ctx, resumed.Schedule.ID, SchedulePageRequest{}) if err != nil { t.Fatal(err) } for _, item := range page.Items { if item.TaskID != nil { occurrence = item break } } if occurrence.TaskID != nil { break } select { case <-ctx.Done(): t.Fatal(ctx.Err()) case <-time.After(80 * time.Millisecond): } } task := waitRuntimeState(t, ctx, api, *occurrence.TaskID, TaskSucceeded) if string(task.Result) != `{"n":9007199254740993}` || task.AttemptCount != 1 { t.Fatal("scheduled Worker result corrupted") } var paused ScheduleResult for { current, err := api.GetSchedule(ctx, resumed.Schedule.ID) if err != nil { t.Fatal(err) } paused, err = api.PauseSchedule(ctx, current.ID, workerTestID(t), ControlScheduleRequest{ExpectedRevision: current.Revision, Reason: "publish next definition"}) if err == nil { break } var apiErr *APIError if !errors.As(err, &apiErr) || apiErr.Code != "STATE_CONFLICT" { t.Fatal(err) } } in.Definition.Rule = ScheduleRule{Kind: "CRON", Timezone: "Australia/Lord_Howe", StartAt: start, Cron: "*/5 * * * *"} publish := PublishScheduleRequest{ExpectedRevision: paused.Schedule.Revision, Reason: "test immutable cron version", Definition: in.Definition} publishKey := workerTestID(t) updated, err := api.PublishSchedule(ctx, paused.Schedule.ID, publishKey, publish) if err != nil || updated.Schedule.CurrentVersion != 2 || !updated.Schedule.Paused { t.Fatal("publish schedule", err) } source, err := api.GetTaskSource(ctx, task.ID) if err != nil || source.Kind != "SCHEDULE" || source.Schedule == nil || source.Schedule.ScheduleID != paused.Schedule.ID || source.Schedule.OccurrenceID != occurrence.ID || source.Schedule.ScheduleVersion != 1 || !source.Schedule.ScheduledAt.Equal(occurrence.ScheduledAt) { t.Fatal("published version changed task source", source, err) } old, err := api.GetScheduleVersion(ctx, paused.Schedule.ID, 1) if err != nil || old.Definition.Rule.Kind != "FIXED_RATE" { t.Fatal("old version lost", err) } current, err := api.GetScheduleVersion(ctx, paused.Schedule.ID, 2) if err != nil || current.Definition.Rule.Kind != "CRON" { t.Fatal("new cron version lost", err) } duplicate, err = api.PublishSchedule(ctx, paused.Schedule.ID, publishKey, publish) if err != nil || !duplicate.Deduplicated || duplicate.Schedule.Revision != updated.Schedule.Revision { t.Fatal("publish replay changed revision") } list, err := api.ListSchedules(ctx, queue, SchedulePageRequest{Limit: 100}) if err != nil { t.Fatal(err) } found := false for _, v := range list.Items { found = found || v.ID == updated.Schedule.ID } if !found { t.Fatal("schedule missing from list") } before, err := api.ListScheduleOccurrences(ctx, updated.Schedule.ID, SchedulePageRequest{}) if err != nil { t.Fatal(err) } select { case <-ctx.Done(): t.Fatal(ctx.Err()) case <-time.After(1200 * time.Millisecond): } after, err := api.ListScheduleOccurrences(ctx, updated.Schedule.ID, SchedulePageRequest{}) if err != nil || len(after.Items) != len(before.Items) { t.Fatal("paused version fired") } stop() if err := <-done; err != nil { t.Fatal(err) } ended = true }