go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/bq/export_test.go (about) 1 // Copyright 2023 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 bq 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "testing" 22 "time" 23 24 "google.golang.org/protobuf/types/known/durationpb" 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 "go.chromium.org/luci/common/clock/testclock" 28 "go.chromium.org/luci/gae/impl/memory" 29 "go.chromium.org/luci/gae/service/datastore" 30 "go.chromium.org/luci/server/tq" 31 "go.chromium.org/luci/server/tq/tqtesting" 32 33 "go.chromium.org/luci/swarming/server/bq/taskspb" 34 35 . "github.com/smartystreets/goconvey/convey" 36 . "go.chromium.org/luci/common/testing/assertions" 37 ) 38 39 func init() { 40 RegisterTQTasks() 41 } 42 43 func TestExportStateCleanup(t *testing.T) { 44 t.Parallel() 45 46 Convey("With mocks", t, func() { 47 setup := func() (context.Context, testclock.TestClock, *tqtesting.Scheduler) { 48 ctx, tc := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC) 49 ctx = memory.Use(ctx) 50 ctx, skdr := tq.TestingContext(ctx, nil) 51 return ctx, tc, skdr 52 } 53 Convey("cron deletes age(exportState.Created) >= 24hrs", func() { 54 const n = 200 55 ctx, tc, _ := setup() 56 datastore.GetTestable(ctx).AutoIndex(true) 57 datastore.GetTestable(ctx).Consistent(true) 58 now := tc.Now() 59 newest := now.Add(-2 * time.Hour) 60 oldest := now.Add(-48 * time.Hour) 61 ents := make([]*ExportState, 0, n*2) 62 for i := 0; i < n; i++ { 63 oldest = oldest.Add(-exportDuration) 64 ents = append(ents, &ExportState{ 65 Key: exportStateKey(ctx, &taskspb.CreateExportTask{ 66 Start: timestamppb.New(oldest), 67 Duration: durationpb.New(exportDuration), 68 CloudProject: "foo", 69 Dataset: "bar", 70 TableName: TaskRequests, 71 }), 72 CreatedAt: oldest, 73 }) 74 75 newest = newest.Add(exportDuration) 76 ents = append(ents, &ExportState{ 77 Key: exportStateKey(ctx, &taskspb.CreateExportTask{ 78 Start: timestamppb.New(newest), 79 Duration: durationpb.New(exportDuration), 80 CloudProject: "foo", 81 Dataset: "bar", 82 TableName: TaskRequests, 83 }), 84 CreatedAt: newest, 85 }) 86 } 87 So(datastore.Put(ctx, ents), ShouldBeNil) 88 So(CleanupExportState(ctx), ShouldBeNil) 89 q := datastore.NewQuery(exportStateKind) 90 count := 0 91 cu := now.Add(-maxExportStateAge) 92 err := datastore.Run(ctx, q, func(e *ExportState) error { 93 if e.CreatedAt.After(cu) { 94 count += 1 95 return nil 96 } 97 return fmt.Errorf("Too recent: %+v, co: %s, now: %s", e, cu, now) 98 }) 99 So(err, ShouldBeNil) 100 So(count, ShouldEqual, n) 101 }) 102 }) 103 } 104 105 func TestCreateExportTask(t *testing.T) { 106 t.Parallel() 107 108 // TODO(jonahhooper) Add custom error handler to test failure to schedule 109 // a task. 110 // See: https://crrev.com/c/5054492/11..14/swarming/server/bq/export.go#b97 111 Convey("With mocks", t, func() { 112 setup := func() (context.Context, testclock.TestClock, *tqtesting.Scheduler) { 113 ctx, tc := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC) 114 ctx = memory.Use(ctx) 115 ctx, skdr := tq.TestingContext(ctx, nil) 116 return ctx, tc, skdr 117 } 118 Convey("Creates 4 ExportTasks", func() { 119 ctx, tc, skdr := setup() 120 cutoff := tc.Now().UTC().Add(-latestAge) 121 start := cutoff.Add(-time.Minute) 122 // It appears that datastore testing implementation strips away 123 // nanosecond time precision. 124 // Truncate by microsecond matches this behaviour in UTs 125 start = start.Truncate(time.Microsecond) 126 state := ExportSchedule{ 127 Key: exportScheduleKey(ctx, TaskRequests), 128 NextExport: start, 129 } 130 err := datastore.Put(ctx, &state) 131 So(err, ShouldBeNil) 132 err = ScheduleExportTasks(ctx, "foo", "bar", TaskRequests) 133 So(err, ShouldBeNil) 134 expected := []*taskspb.CreateExportTask{ 135 { 136 CloudProject: "foo", 137 Dataset: "bar", 138 TableName: TaskRequests, 139 Start: timestamppb.New(start), 140 Duration: durationpb.New(exportDuration), 141 }, 142 { 143 CloudProject: "foo", 144 Dataset: "bar", 145 TableName: TaskRequests, 146 Start: timestamppb.New(start.Add(1 * exportDuration)), 147 Duration: durationpb.New(exportDuration), 148 }, 149 { 150 CloudProject: "foo", 151 Dataset: "bar", 152 TableName: TaskRequests, 153 Start: timestamppb.New(start.Add(2 * exportDuration)), 154 Duration: durationpb.New(exportDuration), 155 }, 156 { 157 CloudProject: "foo", 158 Dataset: "bar", 159 TableName: TaskRequests, 160 Start: timestamppb.New(start.Add(3 * exportDuration)), 161 Duration: durationpb.New(exportDuration), 162 }, 163 } 164 So(skdr.Tasks(), ShouldHaveLength, len(expected)) 165 payloads := make([]*taskspb.CreateExportTask, len(expected)) 166 for idx, tsk := range skdr.Tasks().Payloads() { 167 payloads[idx] = tsk.(*taskspb.CreateExportTask) 168 } 169 // We don't care about order and the test scheduler doesn't appear 170 // to return results in order all the time so sort results by time 171 sort.Slice(payloads, func(i, j int) bool { 172 return payloads[i].Start.AsTime().Before(payloads[j].Start.AsTime()) 173 }) 174 So(payloads, ShouldResembleProto, expected) 175 err = datastore.Get(ctx, &state) 176 So(err, ShouldBeNil) 177 So(state.NextExport, ShouldEqual, start.Add(4*exportDuration)) 178 }) 179 Convey("Creates 0 export tasks if ExportSchedule doesn't exist", func() { 180 ctx, tc, skdr := setup() 181 err := ScheduleExportTasks(ctx, "foo", "bar", TaskRequests) 182 So(err, ShouldBeNil) 183 So(skdr.Tasks(), ShouldBeEmpty) 184 state := ExportSchedule{Key: exportScheduleKey(ctx, TaskRequests)} 185 err = datastore.Get(ctx, &state) 186 So(err, ShouldBeNil) 187 So(state.NextExport, ShouldEqual, tc.Now().Truncate(time.Minute)) 188 }) 189 Convey("We cannot create more than 20 tasks at a time", func() { 190 ctx, tc, skdr := setup() 191 start := tc.Now().Add(-(2 + 10) * time.Minute).Truncate(time.Minute) 192 state := ExportSchedule{ 193 Key: exportScheduleKey(ctx, TaskRequests), 194 NextExport: start, 195 } 196 So(datastore.Put(ctx, &state), ShouldBeNil) 197 err := ScheduleExportTasks(ctx, "foo", "bar", TaskRequests) 198 So(err, ShouldBeNil) 199 So(skdr.Tasks(), ShouldHaveLength, 20) 200 }) 201 Convey("We cannot schedule an exportTask in the future", func() { 202 ctx, tc, skdr := setup() 203 start := tc.Now().Add(5 * time.Minute) 204 state := ExportSchedule{ 205 Key: exportScheduleKey(ctx, TaskRequests), 206 NextExport: start, 207 } 208 So(datastore.Put(ctx, &state), ShouldBeNil) 209 err := ScheduleExportTasks(ctx, "foo", "bar", TaskRequests) 210 So(err, ShouldBeNil) 211 So(skdr.Tasks(), ShouldHaveLength, 0) 212 }) 213 }) 214 }