go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/tasks/sync_builds_with_backend_tasks_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 tasks 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 "testing" 22 "time" 23 24 "github.com/golang/mock/gomock" 25 26 "google.golang.org/genproto/googleapis/rpc/status" 27 "google.golang.org/grpc" 28 "google.golang.org/protobuf/types/known/timestamppb" 29 30 "go.chromium.org/luci/common/clock/testclock" 31 "go.chromium.org/luci/common/errors" 32 "go.chromium.org/luci/common/sync/parallel" 33 "go.chromium.org/luci/gae/filter/txndefer" 34 "go.chromium.org/luci/gae/impl/memory" 35 "go.chromium.org/luci/gae/service/datastore" 36 "go.chromium.org/luci/server/caching" 37 "go.chromium.org/luci/server/tq" 38 39 "go.chromium.org/luci/buildbucket/appengine/internal/clients" 40 "go.chromium.org/luci/buildbucket/appengine/internal/config" 41 "go.chromium.org/luci/buildbucket/appengine/internal/metrics" 42 "go.chromium.org/luci/buildbucket/appengine/model" 43 pb "go.chromium.org/luci/buildbucket/proto" 44 45 . "github.com/smartystreets/goconvey/convey" 46 . "go.chromium.org/luci/common/testing/assertions" 47 ) 48 49 var shards int32 = 10 50 51 const ( 52 defaultUpdateID = 5 53 staleUpdateID = 3 54 newUpdateID = 10 55 ) 56 57 // fakeFetchTasksResponse mocks the FetchTasks RPC. 58 func fakeFetchTasksResponse(ctx context.Context, taskReq *pb.FetchTasksRequest, opts ...grpc.CallOption) (*pb.FetchTasksResponse, error) { 59 responses := make([]*pb.FetchTasksResponse_Response, 0, len(taskReq.TaskIds)) 60 for _, tID := range taskReq.TaskIds { 61 responseStatus := pb.Status_STARTED 62 updateID := newUpdateID 63 switch { 64 case strings.HasSuffix(tID.Id, "all_fail"): 65 return nil, errors.Reason("idk, wanted to fail i guess :/").Err() 66 case strings.HasSuffix(tID.Id, "fail_me"): 67 responses = append(responses, &pb.FetchTasksResponse_Response{ 68 Response: &pb.FetchTasksResponse_Response_Error{ 69 Error: &status.Status{ 70 Code: 500, 71 Message: fmt.Sprintf("could not find task for taskId: %s", tID.Id), 72 }, 73 }, 74 }) 75 continue 76 case strings.HasSuffix(tID.Id, "ended"): 77 responseStatus = pb.Status_SUCCESS 78 case strings.HasSuffix(tID.Id, "stale"): 79 updateID = staleUpdateID 80 case strings.HasSuffix(tID.Id, "unchanged"): 81 updateID = defaultUpdateID 82 } 83 responses = append(responses, &pb.FetchTasksResponse_Response{ 84 Response: &pb.FetchTasksResponse_Response_Task{ 85 Task: &pb.Task{ 86 Id: tID, 87 Status: responseStatus, 88 UpdateId: int64(updateID), 89 }, 90 }, 91 }) 92 } 93 return &pb.FetchTasksResponse{Responses: responses}, nil 94 } 95 96 func prepEntities(ctx context.Context, bID int64, buildStatus, outputStatus, taskStatus pb.Status, tIDSuffix string, updateTime time.Time) *datastore.Key { 97 tID := "" 98 if tIDSuffix != "no_task" { 99 tID = fmt.Sprintf("task%d%s", bID, tIDSuffix) 100 } 101 b := &model.Build{ 102 ID: bID, 103 Proto: &pb.Build{ 104 Id: bID, 105 Builder: &pb.BuilderID{ 106 Project: "project", 107 Bucket: "bucket", 108 Builder: "builder", 109 }, 110 Status: buildStatus, 111 UpdateTime: timestamppb.New(updateTime), 112 Output: &pb.Build_Output{ 113 Status: outputStatus, 114 }, 115 }, 116 Status: buildStatus, 117 BackendTarget: "swarming", 118 BackendSyncInterval: 5 * time.Minute, 119 Project: "project", 120 } 121 b.GenerateNextBackendSyncTime(ctx, shards) 122 bk := datastore.KeyForObj(ctx, b) 123 inf := &model.BuildInfra{ 124 Build: bk, 125 Proto: &pb.BuildInfra{ 126 Backend: &pb.BuildInfra_Backend{ 127 Task: &pb.Task{ 128 Status: taskStatus, 129 Id: &pb.TaskID{ 130 Id: tID, 131 Target: "swarming", 132 }, 133 Link: "a link", 134 UpdateId: defaultUpdateID, 135 }, 136 }, 137 }, 138 } 139 bs := &model.BuildStatus{ 140 Build: bk, 141 Status: buildStatus, 142 } 143 So(datastore.Put(ctx, b, inf, bs), ShouldBeNil) 144 return bk 145 } 146 147 func TestQueryBuildsToSync(t *testing.T) { 148 ctx := context.Background() 149 now := testclock.TestRecentTimeUTC 150 ctx, _ = testclock.UseTime(ctx, now) 151 ctx = caching.WithEmptyProcessCache(ctx) 152 ctx = memory.UseWithAppID(ctx, "dev~app-id") 153 ctx = txndefer.FilterRDS(ctx) 154 datastore.GetTestable(ctx).AutoIndex(true) 155 datastore.GetTestable(ctx).Consistent(true) 156 157 t.Parallel() 158 159 Convey("queryBuildsToSync", t, func() { 160 put := func(ctx context.Context, project, backend string, bID int64, status pb.Status, updateTime time.Time) { 161 b := &model.Build{ 162 ID: bID, 163 Proto: &pb.Build{ 164 Id: bID, 165 Builder: &pb.BuilderID{ 166 Project: project, 167 Bucket: "bucket", 168 Builder: "builder", 169 }, 170 Status: status, 171 UpdateTime: timestamppb.New(updateTime), 172 }, 173 Status: status, 174 Project: project, 175 BackendTarget: backend, 176 BackendSyncInterval: 5 * time.Minute, 177 } 178 b.GenerateNextBackendSyncTime(ctx, shards) 179 So(datastore.Put(ctx, b), ShouldBeNil) 180 } 181 182 project := "project" 183 backend := "swarming" 184 185 // Prepare build entities. 186 // Should be included in query results. 187 // updated 1 hour ago. 188 for i := 1; i <= 5; i++ { 189 put(ctx, project, backend, int64(i), pb.Status_STARTED, now.Add(-time.Hour)) 190 } 191 // Should not be included in query results. 192 // Just updated. 193 put(ctx, project, backend, 6, pb.Status_STARTED, now) 194 // Different project. 195 put(ctx, "another_project", backend, 7, pb.Status_STARTED, now.Add(-time.Hour)) 196 // Different backend. 197 put(ctx, project, "another_backend", 8, pb.Status_STARTED, now.Add(-time.Hour)) 198 // Build has completed. 199 put(ctx, project, backend, 9, pb.Status_SUCCESS, now.Add(-time.Hour)) 200 201 var allBks []*datastore.Key 202 err := parallel.RunMulti(ctx, int(shards), func(mr parallel.MultiRunner) error { 203 bkC := make(chan []*datastore.Key) 204 return mr.RunMulti(func(work chan<- func() error) { 205 work <- func() error { 206 defer close(bkC) 207 return queryBuildsToSync(ctx, mr, backend, project, shards, now, bkC) 208 } 209 210 for bks := range bkC { 211 bks := bks 212 allBks = append(allBks, bks...) 213 } 214 }) 215 }) 216 So(err, ShouldBeNil) 217 So(len(allBks), ShouldEqual, 5) 218 }) 219 } 220 221 func TestSyncBuildsWithBackendTasksOneFetchBatch(t *testing.T) { 222 ctl := gomock.NewController(t) 223 defer ctl.Finish() 224 ctx := context.Background() 225 mockBackend := clients.NewMockTaskBackendClient(ctl) 226 mockBackend.EXPECT().FetchTasks(gomock.Any(), gomock.Any()).DoAndReturn(fakeFetchTasksResponse).AnyTimes() 227 now := testclock.TestRecentTimeUTC 228 ctx, _ = testclock.UseTime(ctx, now) 229 ctx = context.WithValue(ctx, clients.MockTaskBackendClientKey, mockBackend) 230 ctx = caching.WithEmptyProcessCache(ctx) 231 ctx = memory.UseWithAppID(ctx, "dev~app-id") 232 ctx = txndefer.FilterRDS(ctx) 233 ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins") 234 datastore.GetTestable(ctx).AutoIndex(true) 235 datastore.GetTestable(ctx).Consistent(true) 236 237 getEntities := func(bIDs []int64) []*model.Build { 238 var blds []*model.Build 239 for _, id := range bIDs { 240 blds = append(blds, &model.Build{ID: id}) 241 } 242 So(datastore.Get(ctx, blds), ShouldBeNil) 243 return blds 244 } 245 246 Convey("syncBuildsWithBackendTasks", t, func() { 247 ctx, sch := tq.TestingContext(ctx, nil) 248 backendSetting := []*pb.BackendSetting{ 249 &pb.BackendSetting{ 250 Target: "swarming", 251 Hostname: "hostname", 252 }, 253 } 254 settingsCfg := &pb.SettingsCfg{Backends: backendSetting} 255 256 bc, err := clients.NewBackendClient(ctx, "project", "swarming", settingsCfg) 257 So(err, ShouldBeNil) 258 259 sync := func(bks []*datastore.Key) error { 260 return parallel.RunMulti(ctx, 5, func(mr parallel.MultiRunner) error { 261 return mr.RunMulti(func(work chan<- func() error) { 262 work <- func() error { 263 return syncBuildsWithBackendTasks(ctx, mr, bc, bks, now) 264 } 265 }) 266 }) 267 } 268 269 Convey("nothing to update", func() { 270 updateTime := now.Add(-2 * time.Minute) 271 bIDs := []int64{3, 4, 5} 272 var bks []*datastore.Key 273 bks = append(bks, prepEntities(ctx, 3, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "", updateTime)) 274 bks = append(bks, prepEntities(ctx, 4, pb.Status_FAILURE, pb.Status_FAILURE, pb.Status_FAILURE, "", updateTime)) 275 bks = append(bks, prepEntities(ctx, 5, pb.Status_SCHEDULED, pb.Status_SCHEDULED, pb.Status_SCHEDULED, "no_task", updateTime)) 276 err := sync(bks) 277 So(err, ShouldBeNil) 278 So(sch.Tasks(), ShouldBeEmpty) 279 blds := getEntities(bIDs) 280 for _, b := range blds { 281 So(b.Proto.UpdateTime.AsTime(), ShouldEqual, updateTime) 282 } 283 }) 284 285 Convey("ok", func() { 286 bIDs := []int64{1, 2} 287 var bks []*datastore.Key 288 for _, id := range bIDs { 289 bks = append(bks, prepEntities(ctx, id, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "", now.Add(-time.Hour))) 290 } 291 err = sync(bks) 292 So(err, ShouldBeNil) 293 So(sch.Tasks(), ShouldBeEmpty) 294 blds := getEntities(bIDs) 295 for _, b := range blds { 296 So(b.Proto.UpdateTime.AsTime(), ShouldEqual, now) 297 } 298 }) 299 300 Convey("ok end builds", func() { 301 bIDs := []int64{3, 4} 302 var bks []*datastore.Key 303 for _, id := range bIDs { 304 bks = append(bks, prepEntities(ctx, id, pb.Status_STARTED, pb.Status_SUCCESS, pb.Status_STARTED, "ended", now.Add(-time.Hour))) 305 } 306 updateBatchSize = 1 // To test update in multiple batches. 307 308 err := sync(bks) 309 So(err, ShouldBeNil) 310 // TQ tasks for pubsub-notification *2, bq-export, and invocation-finalization per build. 311 So(sch.Tasks(), ShouldHaveLength, 8) 312 blds := getEntities(bIDs) 313 for _, b := range blds { 314 So(b.Proto.UpdateTime.AsTime(), ShouldEqual, now) 315 So(b.Status, ShouldEqual, pb.Status_SUCCESS) 316 } 317 }) 318 319 Convey("partially ok", func() { 320 preSyncUpdateTime := now.Add(-time.Hour) 321 bIDs := []int64{5, 6, 7, 8} 322 var bks []*datastore.Key 323 // build 5 is ok. 324 bks = append(bks, prepEntities(ctx, 5, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "", preSyncUpdateTime)) 325 // failed to get the task for build 6. 326 bks = append(bks, prepEntities(ctx, 6, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "fail_me", preSyncUpdateTime)) 327 // task for build 7 is stale. 328 bks = append(bks, prepEntities(ctx, 7, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "stale", preSyncUpdateTime)) 329 // task for build 8 is unchanged. 330 bks = append(bks, prepEntities(ctx, 8, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "unchanged", preSyncUpdateTime)) 331 332 blds := getEntities(bIDs) 333 nextSyncTimeBeforeSync := blds[3].NextBackendSyncTime 334 335 err := sync(bks) 336 So(err, ShouldBeNil) 337 So(sch.Tasks(), ShouldBeEmpty) 338 blds = getEntities(bIDs) 339 // build 5 is updated with new update_id 340 So(blds[0].Proto.UpdateTime.AsTime(), ShouldEqual, now) 341 // build 6 is not updated due to failing to get the task 342 So(blds[1].Proto.UpdateTime.AsTime(), ShouldEqual, preSyncUpdateTime) 343 // build 7 has a stale updateID so it is not udpated 344 So(blds[2].Proto.UpdateTime.AsTime(), ShouldEqual, preSyncUpdateTime) 345 // build 8 is unchanged, but we still update the builds update time 346 So(blds[3].Proto.UpdateTime.AsTime(), ShouldEqual, now) 347 So(blds[3].NextBackendSyncTime, ShouldBeGreaterThan, nextSyncTimeBeforeSync) 348 349 }) 350 351 Convey("all fail", func() { 352 preSyncUpdateTime := now.Add(-time.Hour) 353 bIDs := []int64{5, 6} 354 var bks []*datastore.Key 355 bks = append(bks, prepEntities(ctx, 5, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "", preSyncUpdateTime)) 356 bks = append(bks, prepEntities(ctx, 6, pb.Status_STARTED, pb.Status_STARTED, pb.Status_STARTED, "all_fail", preSyncUpdateTime)) 357 358 err := sync(bks) 359 So(err, ShouldErrLike, "idk, wanted to fail i guess :/") 360 So(sch.Tasks(), ShouldBeEmpty) 361 blds := getEntities(bIDs) 362 for _, b := range blds { 363 So(b.Proto.UpdateTime.AsTime(), ShouldEqual, preSyncUpdateTime) 364 } 365 }) 366 }) 367 } 368 369 func TestSyncBuildsWithBackendTasks(t *testing.T) { 370 ctl := gomock.NewController(t) 371 defer ctl.Finish() 372 ctx := context.Background() 373 mockBackend := clients.NewMockTaskBackendClient(ctl) 374 mockBackend.EXPECT().FetchTasks(gomock.Any(), gomock.Any()).DoAndReturn(fakeFetchTasksResponse).AnyTimes() 375 now := testclock.TestRecentTimeUTC 376 ctx, _ = testclock.UseTime(ctx, now) 377 ctx = context.WithValue(ctx, clients.MockTaskBackendClientKey, mockBackend) 378 ctx = caching.WithEmptyProcessCache(ctx) 379 ctx = memory.UseWithAppID(ctx, "dev~app-id") 380 ctx = txndefer.FilterRDS(ctx) 381 ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins") 382 datastore.GetTestable(ctx).AutoIndex(true) 383 datastore.GetTestable(ctx).Consistent(true) 384 385 getEntities := func(bIDs []int64) []*model.Build { 386 var blds []*model.Build 387 for _, id := range bIDs { 388 blds = append(blds, &model.Build{ID: id}) 389 } 390 So(datastore.Get(ctx, blds), ShouldBeNil) 391 return blds 392 } 393 394 Convey("SyncBuildsWithBackendTasks", t, func() { 395 ctx, sch := tq.TestingContext(ctx, nil) 396 backendSetting := []*pb.BackendSetting{ 397 { 398 Target: "swarming", 399 Hostname: "hostname", 400 Mode: &pb.BackendSetting_FullMode_{ 401 FullMode: &pb.BackendSetting_FullMode{ 402 BuildSyncSetting: &pb.BackendSetting_BuildSyncSetting{ 403 Shards: shards, 404 }, 405 }, 406 }, 407 }, 408 { 409 Target: "foo", 410 Hostname: "foo_hostname", 411 Mode: &pb.BackendSetting_LiteMode_{ 412 LiteMode: &pb.BackendSetting_LiteMode{}, 413 }, 414 }, 415 } 416 settingsCfg := &pb.SettingsCfg{Backends: backendSetting} 417 err := config.SetTestSettingsCfg(ctx, settingsCfg) 418 So(err, ShouldBeNil) 419 420 Convey("ok - full mode", func() { 421 bIDs := []int64{101, 102, 103, 104, 105} 422 fetchBatchSize = 1 423 updateBatchSize = 1 424 for _, id := range bIDs { 425 prepEntities(ctx, id, pb.Status_STARTED, pb.Status_SUCCESS, pb.Status_STARTED, "", now.Add(-time.Hour)) 426 } 427 prepEntities(ctx, 106, pb.Status_STARTED, pb.Status_SUCCESS, pb.Status_STARTED, "ended", now.Add(-time.Hour)) 428 bIDs = append(bIDs, 106) 429 err = SyncBuildsWithBackendTasks(ctx, "swarming", "project") 430 So(err, ShouldBeNil) 431 So(sch.Tasks(), ShouldHaveLength, 4) // 106 completed 432 blds := getEntities(bIDs) 433 for _, b := range blds { 434 So(b.Proto.UpdateTime.AsTime(), ShouldEqual, now) 435 if b.ID == int64(106) { 436 So(b.Status, ShouldEqual, pb.Status_SUCCESS) 437 } 438 } 439 }) 440 441 Convey("no sync - lite mode", func() { 442 err = SyncBuildsWithBackendTasks(ctx, "foo", "project") 443 So(err, ShouldBeNil) 444 So(sch.Tasks(), ShouldHaveLength, 0) 445 }) 446 447 Convey("backend setting not found", func() { 448 err = SyncBuildsWithBackendTasks(ctx, "not_exist", "project") 449 So(err, ShouldErrLike, "failed to find backend not_exist from global config") 450 }) 451 }) 452 }