go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/tq/dispatcher_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 tq 16 17 import ( 18 "context" 19 "fmt" 20 "net/http" 21 "net/http/httptest" 22 "strings" 23 "sync" 24 "testing" 25 "time" 26 27 taskspb "cloud.google.com/go/cloudtasks/apiv2/cloudtaskspb" 28 "cloud.google.com/go/pubsub/apiv1/pubsubpb" 29 "google.golang.org/grpc/codes" 30 "google.golang.org/grpc/status" 31 "google.golang.org/protobuf/proto" 32 "google.golang.org/protobuf/reflect/protoreflect" 33 "google.golang.org/protobuf/types/known/durationpb" 34 "google.golang.org/protobuf/types/known/emptypb" 35 "google.golang.org/protobuf/types/known/timestamppb" 36 37 "go.chromium.org/luci/common/clock" 38 "go.chromium.org/luci/common/clock/testclock" 39 "go.chromium.org/luci/common/errors" 40 "go.chromium.org/luci/common/retry/transient" 41 "go.chromium.org/luci/common/tsmon" 42 "go.chromium.org/luci/common/tsmon/distribution" 43 "go.chromium.org/luci/common/tsmon/store" 44 "go.chromium.org/luci/common/tsmon/target" 45 "go.chromium.org/luci/common/tsmon/types" 46 47 "go.chromium.org/luci/server/router" 48 "go.chromium.org/luci/server/tq/internal/metrics" 49 "go.chromium.org/luci/server/tq/internal/reminder" 50 "go.chromium.org/luci/server/tq/internal/testutil" 51 "go.chromium.org/luci/server/tq/tqtesting" 52 53 . "github.com/smartystreets/goconvey/convey" 54 . "go.chromium.org/luci/common/testing/assertions" 55 ) 56 57 func TestAddTask(t *testing.T) { 58 t.Parallel() 59 60 Convey("With dispatcher", t, func() { 61 var now = time.Unix(1442540000, 0) 62 63 ctx, _ := testclock.UseTime(context.Background(), now) 64 submitter := &submitter{} 65 ctx = UseSubmitter(ctx, submitter) 66 67 d := Dispatcher{ 68 CloudProject: "proj", 69 CloudRegion: "reg", 70 DefaultTargetHost: "example.com", 71 PushAs: "push-as@example.com", 72 } 73 74 d.RegisterTaskClass(TaskClass{ 75 ID: "test-dur", 76 Prototype: &durationpb.Duration{}, // just some proto type 77 Kind: NonTransactional, 78 Queue: "queue-1", 79 }) 80 81 task := &Task{ 82 Payload: durationpb.New(10 * time.Second), 83 Title: "hi", 84 Delay: 123 * time.Second, 85 } 86 expectedPayload := []byte(`{ 87 "class": "test-dur", 88 "type": "google.protobuf.Duration", 89 "body": "10s" 90 }`) 91 92 expectedScheduleTime := timestamppb.New(now.Add(123 * time.Second)) 93 expectedHeaders := defaultHeaders() 94 expectedHeaders[ExpectedETAHeader] = fmt.Sprintf("%d.%06d", expectedScheduleTime.GetSeconds(), expectedScheduleTime.GetNanos()/1000) 95 96 Convey("Nameless HTTP task", func() { 97 So(d.AddTask(ctx, task), ShouldBeNil) 98 99 So(submitter.reqs, ShouldHaveLength, 1) 100 So(submitter.reqs[0].CreateTaskRequest, ShouldResembleProto, &taskspb.CreateTaskRequest{ 101 Parent: "projects/proj/locations/reg/queues/queue-1", 102 Task: &taskspb.Task{ 103 ScheduleTime: expectedScheduleTime, 104 MessageType: &taskspb.Task_HttpRequest{ 105 HttpRequest: &taskspb.HttpRequest{ 106 HttpMethod: taskspb.HttpMethod_POST, 107 Url: "https://example.com/internal/tasks/t/test-dur/hi", 108 Headers: expectedHeaders, 109 Body: expectedPayload, 110 AuthorizationHeader: &taskspb.HttpRequest_OidcToken{ 111 OidcToken: &taskspb.OidcToken{ 112 ServiceAccountEmail: "push-as@example.com", 113 }, 114 }, 115 }, 116 }, 117 }, 118 }) 119 }) 120 121 Convey("HTTP task with no delay", func() { 122 task.Delay = 0 123 So(d.AddTask(ctx, task), ShouldBeNil) 124 125 // See `var now = ...` above. 126 expectedHeaders[ExpectedETAHeader] = "1442540000.000000" 127 128 So(submitter.reqs, ShouldHaveLength, 1) 129 So(submitter.reqs[0].CreateTaskRequest, ShouldResembleProto, &taskspb.CreateTaskRequest{ 130 Parent: "projects/proj/locations/reg/queues/queue-1", 131 Task: &taskspb.Task{ 132 MessageType: &taskspb.Task_HttpRequest{ 133 HttpRequest: &taskspb.HttpRequest{ 134 HttpMethod: taskspb.HttpMethod_POST, 135 Url: "https://example.com/internal/tasks/t/test-dur/hi", 136 Headers: expectedHeaders, 137 Body: expectedPayload, 138 AuthorizationHeader: &taskspb.HttpRequest_OidcToken{ 139 OidcToken: &taskspb.OidcToken{ 140 ServiceAccountEmail: "push-as@example.com", 141 }, 142 }, 143 }, 144 }, 145 }, 146 }) 147 }) 148 149 Convey("Nameless GAE task", func() { 150 d.GAE = true 151 d.DefaultTargetHost = "" 152 So(d.AddTask(ctx, task), ShouldBeNil) 153 154 So(submitter.reqs, ShouldHaveLength, 1) 155 expectedScheduleTime := timestamppb.New(now.Add(123 * time.Second)) 156 157 So(submitter.reqs[0].CreateTaskRequest, ShouldResembleProto, &taskspb.CreateTaskRequest{ 158 Parent: "projects/proj/locations/reg/queues/queue-1", 159 Task: &taskspb.Task{ 160 ScheduleTime: expectedScheduleTime, 161 MessageType: &taskspb.Task_AppEngineHttpRequest{ 162 AppEngineHttpRequest: &taskspb.AppEngineHttpRequest{ 163 HttpMethod: taskspb.HttpMethod_POST, 164 RelativeUri: "/internal/tasks/t/test-dur/hi", 165 Headers: expectedHeaders, 166 Body: expectedPayload, 167 }, 168 }, 169 }, 170 }) 171 }) 172 173 Convey("Named task", func() { 174 task.DeduplicationKey = "key" 175 176 So(d.AddTask(ctx, task), ShouldBeNil) 177 178 So(submitter.reqs, ShouldHaveLength, 1) 179 So(submitter.reqs[0].CreateTaskRequest.Task.Name, ShouldEqual, 180 "projects/proj/locations/reg/queues/queue-1/tasks/"+ 181 "ca0a124846df4b453ae63e3ad7c63073b0d25941c6e63e5708fd590c016edcef") 182 }) 183 184 Convey("Titleless task", func() { 185 task.Title = "" 186 187 So(d.AddTask(ctx, task), ShouldBeNil) 188 189 So(submitter.reqs, ShouldHaveLength, 1) 190 So( 191 submitter.reqs[0].CreateTaskRequest.Task.MessageType.(*taskspb.Task_HttpRequest).HttpRequest.Url, 192 ShouldEqual, 193 "https://example.com/internal/tasks/t/test-dur", 194 ) 195 }) 196 197 Convey("Transient err", func() { 198 submitter.err = func(title string) error { 199 return status.Errorf(codes.Internal, "boo, go away") 200 } 201 err := d.AddTask(ctx, task) 202 So(transient.Tag.In(err), ShouldBeTrue) 203 }) 204 205 Convey("Fatal err", func() { 206 submitter.err = func(title string) error { 207 return status.Errorf(codes.PermissionDenied, "boo, go away") 208 } 209 err := d.AddTask(ctx, task) 210 So(err, ShouldNotBeNil) 211 So(transient.Tag.In(err), ShouldBeFalse) 212 }) 213 214 Convey("Unknown payload type", func() { 215 err := d.AddTask(ctx, &Task{ 216 Payload: ×tamppb.Timestamp{}, 217 }) 218 So(err, ShouldErrLike, "no task class matching type") 219 So(submitter.reqs, ShouldHaveLength, 0) 220 }) 221 222 Convey("Bad task title: spaces", func() { 223 task.Title = "No spaces please" 224 err := d.AddTask(ctx, task) 225 So(err, ShouldErrLike, "bad task title") 226 So(err, ShouldErrLike, "must not contain spaces") 227 So(submitter.reqs, ShouldHaveLength, 0) 228 }) 229 230 Convey("Bad task title: too long", func() { 231 task.Title = strings.Repeat("a", 2070) 232 err := d.AddTask(ctx, task) 233 So(err, ShouldErrLike, "bad task title") 234 So(err, ShouldErrLike, `too long; must not exceed 2083 characters when combined with "/internal/tasks/t/test-dur"`) 235 So(submitter.reqs, ShouldHaveLength, 0) 236 }) 237 238 Convey("Custom task payload on GAE", func() { 239 d.GAE = true 240 d.DefaultTargetHost = "" 241 d.RegisterTaskClass(TaskClass{ 242 ID: "test-ts", 243 Prototype: ×tamppb.Timestamp{}, // just some proto type 244 Kind: NonTransactional, 245 Queue: "queue-1", 246 Custom: func(ctx context.Context, m proto.Message) (*CustomPayload, error) { 247 ts := m.(*timestamppb.Timestamp) 248 return &CustomPayload{ 249 Method: "GET", 250 Meta: map[string]string{"k": "v"}, 251 RelativeURI: "/zzz", 252 Body: []byte(fmt.Sprintf("%d", ts.Seconds)), 253 }, nil 254 }, 255 }) 256 257 So(d.AddTask(ctx, &Task{ 258 Payload: ×tamppb.Timestamp{Seconds: 123}, 259 Delay: 444 * time.Second, 260 }), ShouldBeNil) 261 262 So(submitter.reqs, ShouldHaveLength, 1) 263 st := timestamppb.New(now.Add(444 * time.Second)) 264 So(submitter.reqs[0].CreateTaskRequest, ShouldResembleProto, &taskspb.CreateTaskRequest{ 265 Parent: "projects/proj/locations/reg/queues/queue-1", 266 Task: &taskspb.Task{ 267 ScheduleTime: st, 268 MessageType: &taskspb.Task_AppEngineHttpRequest{ 269 AppEngineHttpRequest: &taskspb.AppEngineHttpRequest{ 270 HttpMethod: taskspb.HttpMethod_GET, 271 RelativeUri: "/zzz", 272 Headers: map[string]string{"k": "v", ExpectedETAHeader: fmt.Sprintf("%d.%06d", st.GetSeconds(), st.GetNanos()/1000)}, 273 Body: []byte("123"), 274 }, 275 }, 276 }, 277 }) 278 }) 279 }) 280 } 281 282 func TestPushHandler(t *testing.T) { 283 t.Parallel() 284 285 Convey("With dispatcher", t, func() { 286 var handlerErr error 287 var handlerCb func(context.Context) 288 289 d := Dispatcher{DisableAuth: true} 290 ref := d.RegisterTaskClass(TaskClass{ 291 ID: "test-1", 292 Prototype: &emptypb.Empty{}, 293 Kind: NonTransactional, 294 Queue: "queue", 295 Handler: func(ctx context.Context, payload proto.Message) error { 296 if handlerCb != nil { 297 handlerCb(ctx) 298 } 299 return handlerErr 300 }, 301 }) 302 303 var now = time.Unix(1442540100, 0) 304 ctx, _ := testclock.UseTime(context.Background(), now) 305 ctx, _, _ = tsmon.WithFakes(ctx) 306 tsmon.GetState(ctx).SetStore(store.NewInMemory(&target.Task{})) 307 308 metric := func(m types.Metric, fieldVals ...any) any { 309 return tsmon.GetState(ctx).Store().Get(ctx, m, time.Time{}, fieldVals) 310 } 311 312 metricDist := func(m types.Metric, fieldVals ...any) (count int64, sum float64) { 313 val := metric(m, fieldVals...) 314 if val != nil { 315 So(val, ShouldHaveSameTypeAs, &distribution.Distribution{}) 316 count = val.(*distribution.Distribution).Count() 317 sum = val.(*distribution.Distribution).Sum() 318 } 319 return 320 } 321 322 srv := router.New() 323 d.InstallTasksRoutes(srv, "/pfx") 324 325 call := func(body string, header http.Header) int { 326 req := httptest.NewRequest("POST", "/pfx/ignored/part", strings.NewReader(body)).WithContext(ctx) 327 req.Header = header 328 rec := httptest.NewRecorder() 329 srv.ServeHTTP(rec, req) 330 return rec.Result().StatusCode 331 } 332 333 Convey("Using class ID", func() { 334 Convey("Success", func() { 335 So(call(`{"class": "test-1", "body": {}}`, nil), ShouldEqual, 200) 336 }) 337 Convey("Unknown", func() { 338 So(call(`{"class": "unknown", "body": {}}`, nil), ShouldEqual, 404) 339 }) 340 }) 341 342 Convey("Using type name", func() { 343 Convey("Success", func() { 344 So(call(`{"type": "google.protobuf.Empty", "body": {}}`, nil), ShouldEqual, 200) 345 }) 346 Convey("Totally unknown", func() { 347 So(call(`{"type": "unknown", "body": {}}`, nil), ShouldEqual, 404) 348 }) 349 Convey("Not a registered task", func() { 350 So(call(`{"type": "google.protobuf.Duration", "body": {}}`, nil), ShouldEqual, 404) 351 }) 352 }) 353 354 Convey("Not a JSON body", func() { 355 So(call(`blarg`, nil), ShouldEqual, 400) 356 }) 357 358 Convey("Bad envelope", func() { 359 So(call(`{}`, nil), ShouldEqual, 400) 360 }) 361 362 Convey("Missing message body", func() { 363 So(call(`{"class": "test-1"}`, nil), ShouldEqual, 400) 364 }) 365 366 Convey("Bad message body", func() { 367 So(call(`{"class": "test-1", "body": "huh"}`, nil), ShouldEqual, 400) 368 }) 369 370 Convey("Handler fatal error", func() { 371 handlerErr = errors.New("boo", Fatal) 372 So(call(`{"class": "test-1", "body": {}}`, nil), ShouldEqual, 202) 373 }) 374 375 Convey("Handler ignore error", func() { 376 handlerErr = errors.New("boo", Ignore) 377 So(call(`{"class": "test-1", "body": {}}`, nil), ShouldEqual, 204) 378 }) 379 380 Convey("Handler transient error", func() { 381 handlerErr = errors.New("boo", transient.Tag) 382 So(call(`{"class": "test-1", "body": {}}`, nil), ShouldEqual, 500) 383 }) 384 385 Convey("Handler non-fatal error", func() { 386 handlerErr = errors.New("boo") 387 So(call(`{"class": "test-1", "body": {}}`, nil), ShouldEqual, 429) 388 }) 389 390 Convey("No handler", func() { 391 ref.(*taskClassImpl).Handler = nil 392 So(call(`{"class": "test-1", "body": {}}`, nil), ShouldEqual, 404) 393 }) 394 395 Convey("Metrics work", func() { 396 callWithHeaders := func(headers map[string]string) { 397 hdr := make(http.Header) 398 for k, v := range headers { 399 hdr.Set(k, v) 400 } 401 So(call(`{"type": "google.protobuf.Empty", "body": {}}`, hdr), ShouldEqual, 200) 402 } 403 404 Convey("No ETA header", func() { 405 const fakeDelayMS = 33 406 407 handlerCb = func(ctx context.Context) { 408 info := TaskExecutionInfo(ctx) 409 So(info.ExecutionCount, ShouldEqual, 500) 410 So(info.TaskID, ShouldEqual, "task-without-eta") 411 So(info.expectedETA, ShouldBeZeroValue) 412 So(info.submitterTraceContext, ShouldEqual, "zzz") 413 clock.Get(ctx).(testclock.TestClock).Add(fakeDelayMS * time.Millisecond) 414 } 415 416 callWithHeaders(map[string]string{ 417 "X-CloudTasks-TaskExecutionCount": "500", 418 "X-CloudTasks-TaskName": "task-without-eta", 419 TraceContextHeader: "zzz", 420 }) 421 422 So(metric(metrics.ServerHandledCount, "test-1", "OK", metrics.MaxRetryFieldValue), ShouldEqual, 1) 423 424 durCount, durSum := metricDist(metrics.ServerDurationMS, "test-1", "OK") 425 So(durCount, ShouldEqual, 1) 426 So(durSum, ShouldEqual, float64(fakeDelayMS)) 427 428 latCount, _ := metricDist(metrics.ServerTaskLatency, "test-1", "OK", metrics.MaxRetryFieldValue) 429 So(latCount, ShouldEqual, 0) 430 }) 431 432 Convey("With ETA header", func() { 433 var etaValue = time.Unix(1442540050, 1000) 434 const fakeDelayMS = 33 435 436 handlerCb = func(ctx context.Context) { 437 info := TaskExecutionInfo(ctx) 438 So(info.ExecutionCount, ShouldEqual, 5) 439 So(info.TaskID, ShouldEqual, "task-with-eta") 440 So(info.expectedETA.Equal(etaValue), ShouldBeTrue) 441 clock.Get(ctx).(testclock.TestClock).Add(fakeDelayMS * time.Millisecond) 442 } 443 444 callWithHeaders(map[string]string{ 445 "X-CloudTasks-TaskExecutionCount": "5", 446 "X-CloudTasks-TaskName": "task-with-eta", 447 ExpectedETAHeader: "1442540050.000001", 448 }) 449 450 latCount, latSum := metricDist(metrics.ServerTaskLatency, "test-1", "OK", 5) 451 So(latCount, ShouldEqual, 1) 452 So(latSum, ShouldEqual, float64(now.Sub(etaValue).Milliseconds()+fakeDelayMS)) 453 }) 454 455 Convey("ServerRunning metric", func() { 456 handlerCb = func(ctx context.Context) { 457 d.ReportMetrics(ctx) 458 } 459 callWithHeaders(nil) 460 461 // Was reported while the handler was running. 462 So(metric(metrics.ServerRunning, "test-1"), ShouldEqual, 1) 463 464 // Should report 0 now, since the handler is not running anymore. 465 d.ReportMetrics(ctx) 466 So(metric(metrics.ServerRunning, "test-1"), ShouldEqual, 0) 467 }) 468 }) 469 }) 470 } 471 472 func TestTransactionalEnqueue(t *testing.T) { 473 t.Parallel() 474 475 Convey("With mocks", t, func() { 476 var now = time.Unix(1442540000, 0) 477 478 submitter := &submitter{} 479 db := testutil.FakeDB{} 480 d := Dispatcher{ 481 CloudProject: "proj", 482 CloudRegion: "reg", 483 DefaultTargetHost: "example.com", 484 PushAs: "push-as@example.com", 485 } 486 d.RegisterTaskClass(TaskClass{ 487 ID: "test-dur", 488 Prototype: &durationpb.Duration{}, // just some proto type 489 Kind: Transactional, 490 Queue: "queue-1", 491 }) 492 493 ctx, tc := testclock.UseTime(context.Background(), now) 494 ctx = UseSubmitter(ctx, submitter) 495 txn := db.Inject(ctx) 496 497 Convey("Happy path", func() { 498 task := &Task{ 499 Payload: durationpb.New(5 * time.Second), 500 Delay: 10 * time.Second, 501 } 502 err := d.AddTask(txn, task) 503 So(err, ShouldBeNil) 504 505 // Created the reminder. 506 So(db.AllReminders(), ShouldHaveLength, 1) 507 rem := db.AllReminders()[0] 508 509 // But didn't submitted the task yet. 510 So(submitter.reqs, ShouldBeEmpty) 511 512 // The defer will submit the task and wipe the reminder. 513 db.ExecDefers(ctx) 514 So(db.AllReminders(), ShouldBeEmpty) 515 So(submitter.reqs, ShouldHaveLength, 1) 516 req := submitter.reqs[0] 517 518 // Make sure the reminder and the task look as expected. 519 So(rem.ID, ShouldHaveLength, reminderKeySpaceBytes*2) 520 So(rem.FreshUntil.Equal(now.Add(happyPathMaxDuration)), ShouldBeTrue) 521 So(req.TaskClass, ShouldEqual, "test-dur") 522 So(req.Created.Equal(now), ShouldBeTrue) 523 So(req.Raw, ShouldEqual, task.Payload) // the exact same pointer 524 So(req.CreateTaskRequest.Task.Name, ShouldEqual, "projects/proj/locations/reg/queues/queue-1/tasks/"+rem.ID) 525 526 // The task request inside the reminder's raw payload is correct. 527 remPayload, err := rem.DropPayload().Payload() 528 So(err, ShouldBeNil) 529 So(req.CreateTaskRequest, ShouldResembleProto, remPayload.CreateTaskRequest) 530 }) 531 532 Convey("Fatal Submit error", func() { 533 submitter.err = func(string) error { return status.Errorf(codes.PermissionDenied, "boom") } 534 535 err := d.AddTask(txn, &Task{ 536 Payload: durationpb.New(5 * time.Second), 537 Delay: 10 * time.Second, 538 }) 539 So(err, ShouldBeNil) 540 541 So(db.AllReminders(), ShouldHaveLength, 1) 542 db.ExecDefers(ctx) 543 So(db.AllReminders(), ShouldBeEmpty) 544 }) 545 546 Convey("Transient Submit error", func() { 547 submitter.err = func(string) error { return status.Errorf(codes.Internal, "boom") } 548 549 err := d.AddTask(txn, &Task{ 550 Payload: durationpb.New(5 * time.Second), 551 Delay: 10 * time.Second, 552 }) 553 So(err, ShouldBeNil) 554 555 So(db.AllReminders(), ShouldHaveLength, 1) 556 db.ExecDefers(ctx) 557 So(db.AllReminders(), ShouldHaveLength, 1) 558 }) 559 560 Convey("Slow", func() { 561 err := d.AddTask(txn, &Task{ 562 Payload: durationpb.New(5 * time.Second), 563 Delay: 10 * time.Second, 564 }) 565 So(err, ShouldBeNil) 566 567 tc.Add(happyPathMaxDuration + 1*time.Second) 568 569 So(db.AllReminders(), ShouldHaveLength, 1) 570 db.ExecDefers(ctx) 571 So(db.AllReminders(), ShouldHaveLength, 1) 572 So(submitter.reqs, ShouldBeEmpty) 573 }) 574 }) 575 } 576 577 func TestTesting(t *testing.T) { 578 t.Parallel() 579 580 Convey("Works", t, func() { 581 var epoch = testclock.TestRecentTimeUTC 582 583 ctx, tc := testclock.UseTime(context.Background(), epoch) 584 tc.SetTimerCallback(func(d time.Duration, t clock.Timer) { 585 if testclock.HasTags(t, tqtesting.ClockTag) { 586 tc.Add(d) 587 } 588 }) 589 590 disp := Dispatcher{} 591 ctx, sched := TestingContext(ctx, &disp) 592 593 var success tqtesting.TaskList 594 sched.TaskSucceeded = tqtesting.TasksCollector(&success) 595 596 m := sync.Mutex{} 597 etas := []time.Duration{} 598 599 disp.RegisterTaskClass(TaskClass{ 600 ID: "test-dur", 601 Prototype: &durationpb.Duration{}, // just some proto type 602 Kind: NonTransactional, 603 Queue: "queue-1", 604 Handler: func(ctx context.Context, msg proto.Message) error { 605 m.Lock() 606 etas = append(etas, clock.Now(ctx).Sub(epoch)) 607 m.Unlock() 608 if clock.Now(ctx).Sub(epoch) < 3*time.Second { 609 disp.AddTask(ctx, &Task{ 610 Payload: &durationpb.Duration{ 611 Seconds: msg.(*durationpb.Duration).Seconds + 1, 612 }, 613 Delay: time.Second, 614 }) 615 } 616 return nil 617 }, 618 }) 619 620 So(disp.AddTask(ctx, &Task{Payload: &durationpb.Duration{Seconds: 1}}), ShouldBeNil) 621 sched.Run(ctx, tqtesting.StopWhenDrained()) 622 So(etas, ShouldResemble, []time.Duration{ 623 0, 1 * time.Second, 2 * time.Second, 3 * time.Second, 624 }) 625 626 So(success, ShouldHaveLength, 4) 627 So(success.Payloads(), ShouldResembleProto, []protoreflect.ProtoMessage{ 628 &durationpb.Duration{Seconds: 1}, 629 &durationpb.Duration{Seconds: 2}, 630 &durationpb.Duration{Seconds: 3}, 631 &durationpb.Duration{Seconds: 4}, 632 }) 633 }) 634 } 635 636 func TestPubSubEnqueue(t *testing.T) { 637 t.Parallel() 638 639 Convey("With dispatcher", t, func() { 640 var epoch = testclock.TestRecentTimeUTC 641 642 ctx, tc := testclock.UseTime(context.Background(), epoch) 643 db := testutil.FakeDB{} 644 645 disp := Dispatcher{Sweeper: NewInProcSweeper(InProcSweeperOptions{})} 646 ctx, sched := TestingContext(ctx, &disp) 647 648 disp.RegisterTaskClass(TaskClass{ 649 ID: "test-dur", 650 Prototype: &durationpb.Duration{}, // just some proto type 651 Kind: Transactional, 652 Topic: "topic-1", 653 Custom: func(_ context.Context, msg proto.Message) (*CustomPayload, error) { 654 return &CustomPayload{ 655 Meta: map[string]string{"a": "b"}, 656 Body: []byte(fmt.Sprintf("%d", msg.(*durationpb.Duration).Seconds)), 657 }, nil 658 }, 659 }) 660 661 So(disp.AddTask(db.Inject(ctx), &Task{Payload: &durationpb.Duration{Seconds: 1}}), ShouldBeNil) 662 663 Convey("Happy path", func() { 664 db.ExecDefers(ctx) // actually enqueue 665 666 So(sched.Tasks(), ShouldHaveLength, 1) 667 668 task := sched.Tasks()[0] 669 So(task.Payload, ShouldResembleProto, &durationpb.Duration{Seconds: 1}) 670 So(task.Message, ShouldResembleProto, &pubsubpb.PubsubMessage{ 671 Data: []byte("1"), 672 Attributes: map[string]string{ 673 "a": "b", 674 "X-Luci-Tq-Reminder-Id": task.Message.Attributes["X-Luci-Tq-Reminder-Id"], 675 }, 676 }) 677 }) 678 679 Convey("Unhappy path", func() { 680 // Not enqueued, but have a reminder. 681 So(sched.Tasks(), ShouldHaveLength, 0) 682 So(db.AllReminders(), ShouldHaveLength, 1) 683 684 // Make reminder sufficiently stale to be eligible for sweeping. 685 tc.Add(5 * time.Minute) 686 687 // Run the sweeper to enqueue from the reminder. 688 So(disp.Sweep(db.Inject(ctx)), ShouldBeNil) 689 690 // Have the task now! 691 So(sched.Tasks(), ShouldHaveLength, 1) 692 693 task := sched.Tasks()[0] 694 So(task.Payload, ShouldBeNil) // not available on non-happy path 695 So(task.Message, ShouldResembleProto, &pubsubpb.PubsubMessage{ 696 Data: []byte("1"), 697 Attributes: map[string]string{ 698 "a": "b", 699 "X-Luci-Tq-Reminder-Id": task.Message.Attributes["X-Luci-Tq-Reminder-Id"], 700 }, 701 }) 702 }) 703 }) 704 } 705 706 type submitter struct { 707 err func(title string) error 708 m sync.Mutex 709 reqs []*reminder.Payload 710 } 711 712 func (s *submitter) Submit(ctx context.Context, req *reminder.Payload) error { 713 s.m.Lock() 714 defer s.m.Unlock() 715 s.reqs = append(s.reqs, req) 716 if s.err == nil { 717 return nil 718 } 719 return s.err(title(req)) 720 } 721 722 func title(req *reminder.Payload) string { 723 url := "" 724 switch mt := req.CreateTaskRequest.Task.MessageType.(type) { 725 case *taskspb.Task_HttpRequest: 726 url = mt.HttpRequest.Url 727 case *taskspb.Task_AppEngineHttpRequest: 728 url = mt.AppEngineHttpRequest.RelativeUri 729 } 730 idx := strings.LastIndex(url, "/") 731 return url[idx+1:] 732 }