go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/tq/internal/sweep/scan_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 sweep 16 17 import ( 18 "context" 19 "math/big" 20 "testing" 21 "time" 22 23 "github.com/golang/mock/gomock" 24 25 "go.chromium.org/luci/common/clock" 26 "go.chromium.org/luci/common/clock/testclock" 27 "go.chromium.org/luci/common/errors" 28 29 "go.chromium.org/luci/server/tq/internal/db" 30 "go.chromium.org/luci/server/tq/internal/partition" 31 "go.chromium.org/luci/server/tq/internal/reminder" 32 "go.chromium.org/luci/server/tq/internal/testutil" 33 34 . "github.com/smartystreets/goconvey/convey" 35 ) 36 37 func TestScan(t *testing.T) { 38 t.Parallel() 39 40 const keySpaceBytes = 16 41 42 Convey("Scan works", t, func() { 43 epoch := testclock.TestRecentTimeLocal 44 ctx, tclock := testclock.UseTime(context.Background(), epoch) 45 db := db.DB(&testutil.FakeDB{}) 46 47 mkReminder := func(id int64, freshUntil time.Time) *reminder.Reminder { 48 low, _ := partition.FromInts(id, id+1).QueryBounds(keySpaceBytes) 49 return &reminder.Reminder{ID: low, FreshUntil: freshUntil} 50 } 51 part0to10 := partition.FromInts(0, 10) 52 53 tasksPerScan := 100 54 secondaryScanShards := 16 55 56 scan := func(ctx context.Context, part *partition.Partition) ([]*reminder.Reminder, partition.SortedPartitions) { 57 return Scan(ctx, &ScanParams{ 58 DB: db, 59 Partition: part, 60 KeySpaceBytes: keySpaceBytes, 61 TasksPerScan: tasksPerScan, 62 SecondaryScanShards: secondaryScanShards, 63 }) 64 } 65 66 Convey("Normal operation", func() { 67 Convey("Empty", func() { 68 rems, more := scan(ctx, part0to10) 69 So(rems, ShouldBeEmpty) 70 So(more, ShouldBeEmpty) 71 }) 72 73 stale := epoch.Add(30 * time.Second) 74 fresh := epoch.Add(90 * time.Second) 75 savedReminders := []*reminder.Reminder{ 76 mkReminder(1, fresh), 77 mkReminder(2, stale), 78 mkReminder(3, fresh), 79 mkReminder(4, stale), 80 } 81 for _, r := range savedReminders { 82 So(db.SaveReminder(ctx, r), ShouldBeNil) 83 } 84 tclock.Set(epoch.Add(60 * time.Second)) 85 86 Convey("Scan complete partition", func() { 87 tasksPerScan = 10 88 89 rems, more := scan(ctx, part0to10) 90 So(more, ShouldBeEmpty) 91 So(rems, ShouldResemble, []*reminder.Reminder{ 92 mkReminder(2, stale), 93 mkReminder(4, stale), 94 }) 95 96 Convey("but only within given partition", func() { 97 rems, more := scan(ctx, partition.FromInts(0, 4)) 98 So(more, ShouldBeEmpty) 99 So(rems, ShouldResemble, []*reminder.Reminder{mkReminder(2, stale)}) 100 }) 101 }) 102 103 Convey("Scan reaches TasksPerScan limit", func() { 104 // Only Reminders 01..03 will be fetched. 02 is stale. 105 // Follow up scans should start from 04. 106 tasksPerScan = 3 107 secondaryScanShards = 2 108 109 rems, more := scan(ctx, part0to10) 110 111 So(rems, ShouldResemble, []*reminder.Reminder{mkReminder(2, stale)}) 112 113 Convey("Scan returns correct follow up ScanItems", func() { 114 So(len(more), ShouldEqual, secondaryScanShards) 115 So(more[0].Low, ShouldResemble, *big.NewInt(3 + 1)) 116 So(more[0].High, ShouldResemble, more[1].Low) 117 So(more[1].High, ShouldResemble, *big.NewInt(10)) 118 }) 119 }) 120 }) 121 122 Convey("Abnormal operation", func() { 123 ctrl := gomock.NewController(t) 124 defer ctrl.Finish() 125 mockDB := testutil.NewMockDB(ctrl) 126 mockDB.EXPECT().Kind().AnyTimes().Return("mockdb") 127 128 db = mockDB 129 tasksPerScan = 2048 130 secondaryScanShards = 2 131 132 stale := epoch.Add(30 * time.Second) 133 fresh := epoch.Add(90 * time.Second) 134 someReminders := []*reminder.Reminder{ 135 mkReminder(1, fresh), 136 mkReminder(2, stale), 137 } 138 tclock.Set(epoch.Add(60 * time.Second)) 139 140 // Simulate context expiry by using deadline in the past. 141 // TODO(tandrii): find a way to expire ctx within FetchRemindersMeta for a 142 // realistic test. 143 ctx, cancel := clock.WithDeadline(ctx, epoch) 144 defer cancel() 145 So(ctx.Err(), ShouldResemble, context.DeadlineExceeded) 146 147 Convey("Timeout without anything fetched", func() { 148 mockDB.EXPECT().FetchRemindersMeta(ctx, gomock.Any(), gomock.Any(), gomock.Any()). 149 Return(nil, errors.Annotate(ctx.Err(), "failed to fetch all").Err()) 150 151 rems, more := scan(ctx, part0to10) 152 153 So(rems, ShouldBeEmpty) 154 So(len(more), ShouldEqual, secondaryScanShards) 155 So(more[0].Low, ShouldResemble, *big.NewInt(0)) 156 So(more[1].High, ShouldResemble, *big.NewInt(10)) 157 }) 158 159 Convey("Timeout after fetching some", func() { 160 mockDB.EXPECT().FetchRemindersMeta(ctx, gomock.Any(), gomock.Any(), gomock.Any()). 161 Return(someReminders, ctx.Err()) 162 163 rems, more := scan(ctx, part0to10) 164 165 So(rems, ShouldResemble, []*reminder.Reminder{mkReminder(2, stale)}) 166 So(more, ShouldHaveLength, 1) // partition is so small, only 1 follow up scan suffices. 167 So(more[0].Low, ShouldResemble, *big.NewInt(2 + 1)) 168 So(more[0].High, ShouldResemble, *big.NewInt(10)) 169 }) 170 }) 171 }) 172 }