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  }