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  }