go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/tq/tqtesting/scheduler_test.go (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package tqtesting 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "strings" 22 "sync" 23 "testing" 24 "time" 25 26 taskspb "cloud.google.com/go/cloudtasks/apiv2/cloudtaskspb" 27 "google.golang.org/grpc/codes" 28 "google.golang.org/grpc/status" 29 "google.golang.org/protobuf/reflect/protoreflect" 30 "google.golang.org/protobuf/types/known/durationpb" 31 "google.golang.org/protobuf/types/known/timestamppb" 32 33 "go.chromium.org/luci/common/clock" 34 "go.chromium.org/luci/common/clock/testclock" 35 "go.chromium.org/luci/common/logging" 36 "go.chromium.org/luci/common/logging/gologger" 37 38 "go.chromium.org/luci/server/tq/internal/reminder" 39 40 . "github.com/smartystreets/goconvey/convey" 41 . "go.chromium.org/luci/common/testing/assertions" 42 ) 43 44 func TestScheduler(t *testing.T) { 45 t.Parallel() 46 47 Convey("With scheduler", t, func() { 48 var epoch = testclock.TestRecentTimeUTC 49 50 ctx := context.Background() 51 if testing.Verbose() { 52 ctx = logging.SetLevel(gologger.StdConfig.Use(ctx), logging.Debug) 53 } 54 55 ctx, tc := testclock.UseTime(ctx, epoch) 56 tc.SetTimerCallback(func(d time.Duration, t clock.Timer) { 57 if testclock.HasTags(t, ClockTag) { 58 tc.Add(d) 59 } 60 }) 61 62 exec := testExecutor{ 63 ctx: ctx, 64 ch: make(chan *Task, 1000), // ~= infinite buffer 65 } 66 sched := Scheduler{Executor: &exec} 67 68 run := func(untilCount int) { 69 ctx, cancel := context.WithCancel(ctx) 70 71 done := make(chan struct{}) 72 go func() { 73 defer close(done) 74 sched.Run(ctx) 75 }() 76 77 exec.waitForTasks(untilCount) 78 cancel() 79 <-done 80 81 So(sched.Tasks(), ShouldBeEmpty) 82 } 83 84 enqueue := func(payload, name string, eta time.Time, taskClassID string) codes.Code { 85 req := &taskspb.CreateTaskRequest{ 86 Parent: "projects/zzz/locations/zzz/queues/zzz", 87 Task: &taskspb.Task{ 88 MessageType: &taskspb.Task_HttpRequest{ 89 HttpRequest: &taskspb.HttpRequest{ 90 Url: payload, 91 }, 92 }, 93 }, 94 } 95 if name != "" { 96 req.Task.Name = req.Parent + "/tasks/" + name 97 } 98 if !eta.IsZero() { 99 req.Task.ScheduleTime = timestamppb.New(eta) 100 } 101 if taskClassID == "" { 102 taskClassID = "default-task-class" 103 } 104 return status.Code(sched.Submit(ctx, &reminder.Payload{ 105 TaskClass: taskClassID, 106 CreateTaskRequest: req, 107 })) 108 } 109 110 Convey("One by one tasks", func() { 111 So(enqueue("1", "name", time.Time{}, ""), ShouldEqual, codes.OK) 112 So(enqueue("2", "name", time.Time{}, ""), ShouldEqual, codes.AlreadyExists) 113 So(enqueue("3", "", time.Time{}, ""), ShouldEqual, codes.OK) 114 So(enqueue("4", "", time.Time{}, ""), ShouldEqual, codes.OK) 115 116 run(3) 117 118 So(orderByPayload(exec.tasks), ShouldResemble, []string{"1", "3", "4"}) 119 }) 120 121 Convey("Task chain", func() { 122 exec.execute = func(payload string, t *Task) bool { 123 if len(payload) < 3 { 124 enqueue(payload+".", "", time.Time{}, "") 125 } 126 return true 127 } 128 enqueue(".", "", time.Time{}, "") 129 run(3) 130 So(orderByPayload(exec.tasks), ShouldResemble, []string{".", "..", "..."}) 131 }) 132 133 Convey("Tasks with ETA", func() { 134 now := clock.Now(ctx) 135 for i := 2; i >= 0; i-- { 136 enqueue(fmt.Sprintf("B %d", i), fmt.Sprintf("B %d", i), now.Add(time.Duration(i)*time.Millisecond), "") 137 enqueue(fmt.Sprintf("A %d", i), fmt.Sprintf("A %d", i), now.Add(time.Duration(i)*time.Millisecond), "") 138 } 139 run(6) 140 So(payloads(exec.tasks), ShouldResemble, []string{"A 0", "B 0", "A 1", "B 1", "A 2", "B 2"}) 141 }) 142 143 Convey("Retries", func() { 144 var capturedTask *Task 145 sched.TaskSucceeded = func(_ context.Context, t *Task) { 146 capturedTask = t 147 } 148 149 exec.execute = func(payload string, t *Task) bool { 150 return t.Attempts == 4 151 } 152 153 enqueue(".", "", time.Time{}, "") 154 run(4) 155 So(payloads(exec.tasks), ShouldHaveLength, 4) 156 157 So(capturedTask, ShouldNotBeNil) 158 So(capturedTask.Attempts, ShouldEqual, 4) 159 }) 160 161 Convey("Fails after multiple attempts", func() { 162 sched.MaxAttempts = 10 163 164 var capturedTask *Task 165 sched.TaskFailed = func(_ context.Context, t *Task) { 166 capturedTask = t 167 } 168 169 exec.execute = func(payload string, t *Task) bool { 170 return false 171 } 172 173 enqueue(".", "", time.Time{}, "") 174 run(10) 175 So(payloads(exec.tasks), ShouldHaveLength, 10) 176 177 So(capturedTask, ShouldNotBeNil) 178 So(capturedTask.Attempts, ShouldEqual, 10) 179 }) 180 181 Convey("State capture", func() { 182 var captured []*Task 183 184 exec.execute = func(payload string, t *Task) bool { 185 if payload == "A 1" { 186 captured = sched.Tasks() 187 } 188 return true 189 } 190 191 now := clock.Now(ctx) 192 for i := 2; i >= 0; i-- { 193 enqueue(fmt.Sprintf("B %d", i), fmt.Sprintf("B %d", i), now.Add(time.Duration(i)*time.Millisecond), "") 194 enqueue(fmt.Sprintf("A %d", i), fmt.Sprintf("A %d", i), now.Add(time.Duration(i)*time.Millisecond), "") 195 } 196 run(6) 197 So(payloads(exec.tasks), ShouldResemble, []string{"A 0", "B 0", "A 1", "B 1", "A 2", "B 2"}) 198 199 So(payloads(captured), ShouldResemble, []string{"A 1", "B 1", "A 2", "B 2"}) 200 So(captured[0].Executing, ShouldBeTrue) 201 So(captured[1].Executing, ShouldBeFalse) 202 }) 203 204 Convey("Run(StopWhenDrained)", func() { 205 Convey("Noop if already drained", func() { 206 exec.execute = func(string, *Task) bool { panic("must no be called") } 207 sched.Run(ctx, StopWhenDrained()) 208 So(clock.Now(ctx).Equal(epoch), ShouldBeTrue) 209 }) 210 211 Convey("Stops after executing a pending task", func() { 212 exec.execute = func(string, *Task) bool { return true } 213 enqueue("1", "", epoch.Add(5*time.Second), "") 214 sched.Run(ctx, StopWhenDrained()) 215 So(clock.Now(ctx).Sub(epoch), ShouldEqual, 5*time.Second) 216 So(exec.tasks, ShouldHaveLength, 1) 217 }) 218 219 Convey("Stops after draining", func() { 220 exec.execute = func(payload string, _ *Task) bool { 221 if payload == "1" { 222 enqueue("2", "", clock.Now(ctx).Add(5*time.Second), "") 223 } 224 return true 225 } 226 enqueue("1", "", epoch.Add(5*time.Second), "") 227 sched.Run(ctx, StopWhenDrained()) 228 So(clock.Now(ctx).Sub(epoch), ShouldEqual, 10*time.Second) 229 So(exec.tasks, ShouldHaveLength, 2) 230 }) 231 }) 232 233 Convey("Run(StopAfterTask)", func() { 234 Convey("Stops immediately after the right task if ran serially", func() { 235 enqueue("1", "", epoch.Add(3*time.Second), "classA") 236 enqueue("2", "", epoch.Add(6*time.Second), "classB") 237 enqueue("3", "", epoch.Add(9*time.Second), "classB") 238 sched.Run(ctx, StopAfterTask("classB")) 239 So(payloads(exec.tasks), ShouldResemble, []string{"1", "2"}) 240 241 Convey("Doesn't take into account previously executed tasks", func() { 242 sched.Run(ctx, StopAfterTask("classB")) 243 So(payloads(exec.tasks), ShouldResemble, []string{"1", "2", "3"}) 244 }) 245 }) 246 247 Convey("Stops immediately after the right task in a chain if ran serially", func() { 248 exec.execute = func(payload string, _ *Task) bool { 249 switch payload { 250 case "1": 251 enqueue("2", "", clock.Now(ctx).Add(5*time.Second), "classB") 252 case "2": 253 enqueue("3", "", clock.Now(ctx).Add(5*time.Second), "classB") 254 } 255 return true 256 } 257 enqueue("1", "", time.Time{}, "classA") 258 sched.Run(ctx, StopAfterTask("classB")) 259 So(payloads(exec.tasks), ShouldResemble, []string{"1", "2"}) 260 }) 261 262 Convey("Stops eventually if ran in parallel", func() { 263 // Generate task tree: 264 // Z 265 // ZA ZB 266 // ZAA ZAB ZBA ZBB 267 exec.execute = func(payload string, _ *Task) bool { 268 if len(payload) <= 3 { 269 enqueue(payload+"A", "", time.Time{}, "classA") 270 enqueue(payload+"B", "", time.Time{}, "classB") 271 } 272 return true 273 } 274 enqueue("Z", "", time.Time{}, "classZ") 275 276 sched.Run(ctx, StopAfterTask("classA"), ParallelExecute()) 277 // At least Z and at least one of ZA, ZAA, ZBA must have been executed. 278 exec.waitForTasks(2) 279 exec.m.Lock() 280 ps := payloads(exec.tasks) 281 exec.m.Unlock() 282 found := false 283 for _, p := range ps { 284 if strings.HasSuffix(p, "A") { 285 found = true 286 } 287 } 288 So(found, ShouldBeTrue) 289 }) 290 }) 291 292 Convey("Run(StopBeforeTask)", func() { 293 Convey("Stops after the prior task if ran serially", func() { 294 enqueue("1", "", epoch.Add(2*time.Second), "classA") 295 enqueue("2", "", epoch.Add(4*time.Second), "classB") 296 enqueue("3", "", epoch.Add(6*time.Second), "classA") 297 enqueue("4", "", epoch.Add(8*time.Second), "classB") 298 sched.Run(ctx, StopBeforeTask("classB")) 299 So(payloads(exec.tasks), ShouldResemble, []string{"1"}) 300 301 Convey("Even if it doesn't run anything", func() { 302 sched.Run(ctx, StopBeforeTask("classB")) 303 // The payloasd must be exactly same. 304 So(payloads(exec.tasks), ShouldResemble, []string{"1"}) 305 }) 306 }) 307 308 Convey("Takes into account newly scheduled tasks", func() { 309 exec.execute = func(payload string, _ *Task) bool { 310 switch payload { 311 case "1": 312 enqueue("2->a", "", clock.Now(ctx).Add(2*time.Second), "classA") 313 enqueue("2->b", "", clock.Now(ctx).Add(2*time.Second), "classA") 314 case "2->a": 315 enqueue("3a", "", clock.Now(ctx).Add(8*time.Second), "classA") // eta after 3b 316 case "2->b": 317 enqueue("3b", "", clock.Now(ctx).Add(6*time.Second), "classB") // eta before 3a 318 } 319 return true 320 } 321 enqueue("1", "", time.Time{}, "classA") 322 323 Convey("Stops before 3a and 3b if run serially", func() { 324 sched.Run(ctx, StopBeforeTask("classB")) 325 So(payloads(exec.tasks), ShouldResemble, []string{"1", "2->a", "2->b"}) 326 }) 327 Convey("Stops before 3b, but 3a may be executed, if run in parallel", func() { 328 sched.Run(ctx, StopBeforeTask("classB"), ParallelExecute()) 329 ps := orderByPayload(exec.tasks) 330 So(ps[:3], ShouldResemble, []string{"1", "2->a", "2->b"}) 331 So(ps[3:], ShouldNotContain, "3b") 332 }) 333 }) 334 }) 335 }) 336 } 337 338 func TestTaskList(t *testing.T) { 339 t.Parallel() 340 341 Convey("With task list", t, func() { 342 var epoch = time.Unix(1442540000, 0) 343 344 task := func(payload int, exec bool, eta int, class, name string) *Task { 345 return &Task{ 346 Name: name, 347 Class: class, 348 Executing: exec, 349 ETA: epoch.Add(time.Duration(eta) * time.Second), 350 Payload: &durationpb.Duration{Seconds: int64(payload)}, 351 } 352 } 353 354 tl := TaskList{ 355 task(0, true, 3, "", ""), 356 task(1, false, 1, "", ""), 357 task(2, true, 2, "", ""), 358 task(3, false, 4, "", ""), 359 task(4, true, 5, "classB", ""), 360 task(5, true, 5, "classA", ""), 361 task(6, true, 5, "classA", "b"), 362 task(7, true, 5, "classA", "a"), 363 } 364 365 Convey("Payloads", func() { 366 So(tl.Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{ 367 &durationpb.Duration{Seconds: 0}, 368 &durationpb.Duration{Seconds: 1}, 369 &durationpb.Duration{Seconds: 2}, 370 &durationpb.Duration{Seconds: 3}, 371 &durationpb.Duration{Seconds: 4}, 372 &durationpb.Duration{Seconds: 5}, 373 &durationpb.Duration{Seconds: 6}, 374 &durationpb.Duration{Seconds: 7}, 375 }) 376 }) 377 378 Convey("Executing/Pending", func() { 379 So(tl.Executing().Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{ 380 &durationpb.Duration{Seconds: 0}, 381 &durationpb.Duration{Seconds: 2}, 382 &durationpb.Duration{Seconds: 4}, 383 &durationpb.Duration{Seconds: 5}, 384 &durationpb.Duration{Seconds: 6}, 385 &durationpb.Duration{Seconds: 7}, 386 }) 387 388 So(tl.Pending().Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{ 389 &durationpb.Duration{Seconds: 1}, 390 &durationpb.Duration{Seconds: 3}, 391 }) 392 }) 393 394 Convey("SortByETA", func() { 395 So(tl.SortByETA().Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{ 396 &durationpb.Duration{Seconds: 2}, 397 &durationpb.Duration{Seconds: 0}, 398 &durationpb.Duration{Seconds: 5}, 399 &durationpb.Duration{Seconds: 7}, 400 &durationpb.Duration{Seconds: 6}, 401 &durationpb.Duration{Seconds: 4}, 402 &durationpb.Duration{Seconds: 1}, 403 &durationpb.Duration{Seconds: 3}, 404 }) 405 }) 406 }) 407 408 Convey("TasksCollector", t, func() { 409 var tl TaskList 410 cb := TasksCollector(&tl) 411 cb(context.Background(), &Task{}) 412 cb(context.Background(), &Task{}) 413 So(tl, ShouldHaveLength, 2) 414 }) 415 } 416 417 type testExecutor struct { 418 ctx context.Context 419 execute func(payload string, t *Task) bool 420 ch chan *Task 421 422 m sync.Mutex 423 tasks []*Task 424 } 425 426 func (exe *testExecutor) Execute(ctx context.Context, t *Task, done func(retry bool)) { 427 t = t.Copy() 428 429 success := true 430 if exe.execute != nil { 431 success = exe.execute(t.Task.GetHttpRequest().Url, t) 432 } 433 434 exe.m.Lock() 435 exe.tasks = append(exe.tasks, t) 436 exe.m.Unlock() 437 exe.ch <- t 438 done(!success) 439 } 440 441 func (exe *testExecutor) waitForTasks(n int) { 442 for ; n > 0; n-- { 443 select { 444 case <-exe.ch: 445 case <-exe.ctx.Done(): 446 So("the scheduler is stuck", ShouldBeNil) 447 } 448 } 449 } 450 451 func payloads(tasks []*Task) []string { 452 payloads := make([]string, len(tasks)) 453 for i, t := range tasks { 454 payloads[i] = t.Task.GetHttpRequest().Url 455 } 456 return payloads 457 } 458 459 func orderByPayload(tasks []*Task) []string { 460 sort.Slice(tasks, func(i, j int) bool { 461 l, r := tasks[i].Task, tasks[j].Task 462 return l.GetHttpRequest().Url < r.GetHttpRequest().Url 463 }) 464 return payloads(tasks) 465 }