go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/changelist/updater_test.go (about) 1 // Copyright 2021 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 changelist 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "testing" 22 "time" 23 24 "google.golang.org/protobuf/proto" 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 "go.chromium.org/luci/common/clock/testclock" 28 "go.chromium.org/luci/common/errors" 29 gerrit "go.chromium.org/luci/common/proto/gerrit" 30 "go.chromium.org/luci/common/retry/transient" 31 "go.chromium.org/luci/gae/service/datastore" 32 "go.chromium.org/luci/server/tq" 33 "go.chromium.org/luci/server/tq/tqtesting" 34 35 "go.chromium.org/luci/cv/internal/common" 36 "go.chromium.org/luci/cv/internal/cvtesting" 37 "go.chromium.org/luci/cv/internal/metrics" 38 39 . "github.com/smartystreets/goconvey/convey" 40 . "go.chromium.org/luci/common/testing/assertions" 41 ) 42 43 func externalTime(t time.Time) *UpdateCLTask_Hint { 44 return &UpdateCLTask_Hint{ExternalUpdateTime: timestamppb.New(t)} 45 } 46 47 func TestUpdaterSchedule(t *testing.T) { 48 t.Parallel() 49 50 Convey("Correctly generate dedup keys for Updater TQ tasks", t, func() { 51 ct := cvtesting.Test{} 52 ctx, cancel := ct.SetUp(t) 53 defer cancel() 54 55 Convey("Correctly generate dedup keys for Updater TQ tasks", func() { 56 Convey("Diff CLIDs have diff dedup keys", func() { 57 t := &UpdateCLTask{LuciProject: "proj", Id: 7} 58 k1 := makeTaskDeduplicationKey(ctx, t, 0) 59 t.Id = 8 60 k2 := makeTaskDeduplicationKey(ctx, t, 0) 61 So(k1, ShouldNotResemble, k2) 62 }) 63 64 Convey("Diff ExternalID have diff dedup keys", func() { 65 t := &UpdateCLTask{LuciProject: "proj"} 66 t.ExternalId = "kind1/foo/23" 67 k1 := makeTaskDeduplicationKey(ctx, t, 0) 68 t.ExternalId = "kind4/foo/56" 69 k2 := makeTaskDeduplicationKey(ctx, t, 0) 70 So(k1, ShouldNotResemble, k2) 71 }) 72 73 Convey("Even if ExternalID and internal ID refer to the same CL, they have diff dedup keys", func() { 74 t1 := &UpdateCLTask{LuciProject: "proj", ExternalId: "kind1/foo/23"} 75 t2 := &UpdateCLTask{LuciProject: "proj", Id: 2} 76 k1 := makeTaskDeduplicationKey(ctx, t1, 0) 77 k2 := makeTaskDeduplicationKey(ctx, t2, 0) 78 So(k1, ShouldNotResemble, k2) 79 }) 80 81 Convey("Diff updatedHint have diff dedup keys", func() { 82 t := &UpdateCLTask{LuciProject: "proj", ExternalId: "kind1/foo/23"} 83 t.Hint = externalTime(ct.Clock.Now()) 84 k1 := makeTaskDeduplicationKey(ctx, t, 0) 85 t.Hint = externalTime(ct.Clock.Now().Add(time.Second)) 86 k2 := makeTaskDeduplicationKey(ctx, t, 0) 87 So(k1, ShouldNotResemble, k2) 88 }) 89 90 Convey("Same CLs but diff LUCI projects have diff dedup keys", func() { 91 t := &UpdateCLTask{LuciProject: "proj", ExternalId: "kind1/foo/23"} 92 k1 := makeTaskDeduplicationKey(ctx, t, 0) 93 t.LuciProject += "-diff" 94 k2 := makeTaskDeduplicationKey(ctx, t, 0) 95 So(k1, ShouldNotResemble, k2) 96 }) 97 98 Convey("Same CL at the same time is de-duped", func() { 99 t := &UpdateCLTask{LuciProject: "proj", ExternalId: "kind1/foo/23"} 100 k1 := makeTaskDeduplicationKey(ctx, t, 0) 101 k2 := makeTaskDeduplicationKey(ctx, t, 0) 102 So(k1, ShouldResemble, k2) 103 104 Convey("Internal ID doesn't affect dedup based on ExternalID", func() { 105 t.Id = 123 106 k3 := makeTaskDeduplicationKey(ctx, t, 0) 107 So(k3, ShouldResemble, k1) 108 }) 109 }) 110 111 Convey("Same CL with a delay or after the same delay is de-duped", func() { 112 t := &UpdateCLTask{LuciProject: "proj", Id: 123} 113 k1 := makeTaskDeduplicationKey(ctx, t, time.Second) 114 ct.Clock.Add(time.Second) 115 k2 := makeTaskDeduplicationKey(ctx, t, 0) 116 So(k1, ShouldResemble, k2) 117 }) 118 119 Convey("Same CL at mostly same time is also de-duped", func() { 120 t := &UpdateCLTask{LuciProject: "proj", ExternalId: "kind1/foo/23"} 121 k1 := makeTaskDeduplicationKey(ctx, t, 0) 122 // NOTE: this check may fail if common.DistributeOffset is changed, 123 // making new timestamp in the next epoch. If so, adjust the increment. 124 ct.Clock.Add(time.Second) 125 k2 := makeTaskDeduplicationKey(ctx, t, 0) 126 So(k1, ShouldResemble, k2) 127 }) 128 129 Convey("Same CL after sufficient time is no longer de-duped", func() { 130 t := &UpdateCLTask{LuciProject: "proj", ExternalId: "kind1/foo/23"} 131 k1 := makeTaskDeduplicationKey(ctx, t, 0) 132 k2 := makeTaskDeduplicationKey(ctx, t, blindRefreshInterval) 133 So(k1, ShouldNotResemble, k2) 134 }) 135 136 Convey("Same CL with the same MetaRevId is de-duped", func() { 137 t := &UpdateCLTask{LuciProject: "proj", ExternalId: "kind1/foo/23"} 138 t.Hint = &UpdateCLTask_Hint{MetaRevId: "foo"} 139 k1 := makeTaskDeduplicationKey(ctx, t, 0) 140 k2 := makeTaskDeduplicationKey(ctx, t, 0) 141 So(k1, ShouldResemble, k2) 142 }) 143 144 Convey("Same CL with the different MetaRevId is not de-duped", func() { 145 t := &UpdateCLTask{LuciProject: "proj", ExternalId: "kind1/foo/23"} 146 t.Hint = &UpdateCLTask_Hint{MetaRevId: "foo"} 147 k1 := makeTaskDeduplicationKey(ctx, t, 0) 148 t.Hint = &UpdateCLTask_Hint{MetaRevId: "bar"} 149 k2 := makeTaskDeduplicationKey(ctx, t, 0) 150 So(k1, ShouldNotResemble, k2) 151 }) 152 }) 153 154 Convey("makeTQTitleForHumans works", func() { 155 So(makeTQTitleForHumans(&UpdateCLTask{ 156 LuciProject: "proj", 157 Id: 123, 158 }), ShouldResemble, "proj/123") 159 So(makeTQTitleForHumans(&UpdateCLTask{ 160 LuciProject: "proj", 161 ExternalId: "kind/xyz/44", 162 Id: 123, 163 }), ShouldResemble, "proj/123/kind/xyz/44") 164 So(makeTQTitleForHumans(&UpdateCLTask{ 165 LuciProject: "proj", 166 ExternalId: "gerrit/chromium-review.googlesource.com/1111111", 167 Id: 123, 168 }), ShouldResemble, "proj/123/gerrit/chromium/1111111") 169 So(makeTQTitleForHumans(&UpdateCLTask{ 170 LuciProject: "proj", 171 ExternalId: "gerrit/chromium-review.googlesource.com/1111111", 172 Hint: externalTime(testclock.TestRecentTimeUTC), 173 }), ShouldResemble, "proj/gerrit/chromium/1111111/u2016-02-03T04:05:06Z") 174 }) 175 176 Convey("Works overall", func() { 177 u := NewUpdater(ct.TQDispatcher, nil) 178 t := &UpdateCLTask{ 179 LuciProject: "proj", 180 Id: 123, 181 Hint: externalTime(ct.Clock.Now().Add(-time.Second)), 182 Requester: UpdateCLTask_RUN_POKE, 183 } 184 delay := time.Minute 185 So(u.ScheduleDelayed(ctx, t, delay), ShouldBeNil) 186 So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []proto.Message{t}) 187 188 _, _ = Println("Dedup works") 189 ct.Clock.Add(delay) 190 So(u.Schedule(ctx, t), ShouldBeNil) 191 So(ct.TQ.Tasks().Payloads(), ShouldHaveLength, 1) 192 193 _, _ = Println("But not within the transaction") 194 err := datastore.RunInTransaction(ctx, func(ctx context.Context) error { 195 return u.Schedule(ctx, t) 196 }, nil) 197 So(err, ShouldBeNil) 198 So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []proto.Message{t, t}) 199 200 _, _ = Println("Once out of dedup window, schedules a new task") 201 ct.Clock.Add(knownRefreshInterval) 202 So(u.Schedule(ctx, t), ShouldBeNil) 203 So(ct.TQ.Tasks().Payloads(), ShouldResembleProto, []proto.Message{t, t, t}) 204 }) 205 }) 206 } 207 208 func TestUpdaterBatch(t *testing.T) { 209 t.Parallel() 210 211 Convey("Correctly handle batches", t, func() { 212 ct := cvtesting.Test{} 213 ctx, cancel := ct.SetUp(t) 214 defer cancel() 215 216 sortedTQPayloads := func() []proto.Message { 217 payloads := ct.TQ.Tasks().Payloads() 218 sort.Slice(payloads, func(i, j int) bool { 219 return payloads[i].(*UpdateCLTask).GetExternalId() < payloads[j].(*UpdateCLTask).GetExternalId() 220 }) 221 return payloads 222 } 223 224 u := NewUpdater(ct.TQDispatcher, nil) 225 clA := ExternalID("foo/a/1").MustCreateIfNotExists(ctx) 226 clB := ExternalID("foo/b/2").MustCreateIfNotExists(ctx) 227 228 expectedPayloads := []proto.Message{ 229 &UpdateCLTask{ 230 LuciProject: "proj", 231 ExternalId: "foo/a/1", 232 Id: int64(clA.ID), 233 Requester: UpdateCLTask_RUN_POKE, 234 }, 235 &UpdateCLTask{ 236 LuciProject: "proj", 237 ExternalId: "foo/b/2", 238 Id: int64(clB.ID), 239 Requester: UpdateCLTask_RUN_POKE, 240 }, 241 } 242 243 Convey("outside of a transaction, enqueues individual tasks", func() { 244 Convey("special case of just one task", func() { 245 err := u.ScheduleBatch(ctx, "proj", []*CL{clA}, UpdateCLTask_RUN_POKE) 246 So(err, ShouldBeNil) 247 So(sortedTQPayloads(), ShouldResembleProto, expectedPayloads[:1]) 248 }) 249 Convey("multiple", func() { 250 err := u.ScheduleBatch(ctx, "proj", []*CL{clA, clB}, UpdateCLTask_RUN_POKE) 251 So(err, ShouldBeNil) 252 So(sortedTQPayloads(), ShouldResembleProto, expectedPayloads) 253 }) 254 }) 255 256 Convey("inside of a transaction, enqueues just one task", func() { 257 err := datastore.RunInTransaction(ctx, func(ctx context.Context) error { 258 return u.ScheduleBatch(ctx, "proj", []*CL{clA, clB}, UpdateCLTask_RUN_POKE) 259 }, nil) 260 So(err, ShouldBeNil) 261 So(ct.TQ.Tasks(), ShouldHaveLength, 1) 262 // Run just the batch task. 263 ct.TQ.Run(ctx, tqtesting.StopAfterTask(BatchUpdateCLTaskClass)) 264 So(sortedTQPayloads(), ShouldResembleProto, expectedPayloads) 265 }) 266 }) 267 } 268 269 // TestUpdaterWorkingHappyPath is the simplest test for an updater, which also 270 // illustrates the simplest UpdaterBackend. 271 func TestUpdaterHappyPath(t *testing.T) { 272 t.Parallel() 273 274 Convey("Updater's happy path with simplest possible backend", t, func() { 275 ct := cvtesting.Test{} 276 ctx, cancel := ct.SetUp(t) 277 defer cancel() 278 279 pm, rm, tj := pmMock{}, rmMock{}, tjMock{} 280 u := NewUpdater(ct.TQDispatcher, NewMutator(ct.TQDispatcher, &pm, &rm, &tj)) 281 b := &fakeUpdaterBackend{} 282 u.RegisterBackend(b) 283 284 //////////////////////////////////////////// 285 // Phase 1: import CL for the first time. // 286 //////////////////////////////////////////// 287 288 b.fetchResult = UpdateFields{ 289 Snapshot: &Snapshot{ 290 ExternalUpdateTime: timestamppb.New(ct.Clock.Now().Add(-1 * time.Second)), 291 Patchset: 2, 292 MinEquivalentPatchset: 1, 293 LuciProject: "luci-project", 294 Kind: nil, // but should be set in practice, 295 }, 296 ApplicableConfig: &ApplicableConfig{ 297 Projects: []*ApplicableConfig_Project{ 298 { 299 Name: "luci-project", 300 ConfigGroupIds: []string{"hash/name"}, 301 }, 302 }, 303 }, 304 } 305 // Actually run the Updater. 306 So(u.handleCL(ctx, &UpdateCLTask{ 307 LuciProject: "luci-project", 308 ExternalId: "fake/123", 309 Requester: UpdateCLTask_PUBSUB_POLL, 310 }), ShouldBeNil) 311 312 // Ensure that it reported metrics for the CL fetch events. 313 So(ct.TSMonSentValue(ctx, metrics.Internal.CLIngestionAttempted, 314 UpdateCLTask_PUBSUB_POLL.String(), // metric:requester, 315 true, // metric:changed == true 316 false, // metric:dep 317 "luci-project", // metric:project, 318 true, // metric:changed_snapshot == true 319 ), ShouldEqual, 1) 320 So(ct.TSMonSentDistr(ctx, metrics.Internal.CLIngestionLatency, 321 UpdateCLTask_PUBSUB_POLL.String(), // metric:requester, 322 false, // metric:dep 323 "luci-project", // metric:project, 324 true, // metric:changed_snapshot == true 325 ).Sum(), ShouldAlmostEqual, 1) 326 So(ct.TSMonSentDistr(ctx, metrics.Internal.CLIngestionLatencyWithoutFetch, 327 UpdateCLTask_PUBSUB_POLL.String(), // metric:requester, 328 false, // metric:dep 329 "luci-project", // metric:project, 330 true, // metric:changed_snapshot == true 331 ).Sum(), ShouldNotBeNil) 332 333 // Ensure CL is created with correct data. 334 cl, err := ExternalID("fake/123").Load(ctx) 335 So(err, ShouldBeNil) 336 So(cl.Snapshot, ShouldResembleProto, b.fetchResult.Snapshot) 337 So(cl.ApplicableConfig, ShouldResembleProto, b.fetchResult.ApplicableConfig) 338 So(cl.UpdateTime, ShouldHappenWithin, time.Microsecond /*see DS.RoundTime()*/, ct.Clock.Now()) 339 340 // Since there are no Runs associated with the CL, the outstanding TQ task 341 // should ultimately notify the Project Manager. 342 ct.TQ.Run(ctx, tqtesting.StopWhenDrained()) 343 So(pm.byProject, ShouldResemble, map[string]map[common.CLID]int64{ 344 "luci-project": {cl.ID: cl.EVersion}, 345 }) 346 347 // Later, a Run will start on this CL. 348 const runID = "luci-project/123-1-beef" 349 cl.IncompleteRuns = common.RunIDs{runID} 350 cl.EVersion++ 351 So(datastore.Put(ctx, cl), ShouldBeNil) 352 353 /////////////////////////////////////////////////// 354 // Phase 2: update the CL with the new patchset. // 355 /////////////////////////////////////////////////// 356 357 ct.Clock.Add(time.Hour) 358 b.reset() 359 b.fetchResult.Snapshot = proto.Clone(cl.Snapshot).(*Snapshot) 360 b.fetchResult.Snapshot.ExternalUpdateTime = timestamppb.New(ct.Clock.Now()) 361 b.fetchResult.Snapshot.Patchset++ 362 b.lookupACfgResult = cl.ApplicableConfig // unchanged 363 364 // Actually run the Updater. 365 So(u.handleCL(ctx, &UpdateCLTask{ 366 LuciProject: "luci-project", 367 ExternalId: "fake/123", 368 }), ShouldBeNil) 369 cl2 := reloadCL(ctx, cl) 370 371 // The CL entity should have a new patchset and PM/RM should be notified. 372 So(cl2.Snapshot.GetPatchset(), ShouldEqual, 3) 373 ct.TQ.Run(ctx, tqtesting.StopWhenDrained()) 374 So(pm.byProject, ShouldResemble, map[string]map[common.CLID]int64{ 375 "luci-project": {cl.ID: cl2.EVersion}, 376 }) 377 So(rm.byRun, ShouldResemble, map[common.RunID]map[common.CLID]int64{ 378 runID: {cl.ID: cl2.EVersion}, 379 }) 380 381 /////////////////////////////////////////////////// 382 // Phase 3: update if backend detect a change // 383 /////////////////////////////////////////////////// 384 b.reset() 385 b.fetchResult.Snapshot = proto.Clone(cl.Snapshot).(*Snapshot) // unchanged 386 b.lookupACfgResult = cl.ApplicableConfig // unchanged 387 b.backendSnapshotUpdated = true 388 389 // Actually run the Updater. 390 So(u.handleCL(ctx, &UpdateCLTask{ 391 LuciProject: "luci-project", 392 ExternalId: "fake/123", 393 }), ShouldBeNil) 394 cl3 := reloadCL(ctx, cl) 395 396 // The CL entity have been updated 397 So(cl3.EVersion, ShouldBeGreaterThan, cl2.EVersion) 398 ct.TQ.Run(ctx, tqtesting.StopWhenDrained()) 399 So(pm.byProject, ShouldResemble, map[string]map[common.CLID]int64{ 400 "luci-project": {cl.ID: cl3.EVersion}, 401 }) 402 So(rm.byRun, ShouldResemble, map[common.RunID]map[common.CLID]int64{ 403 runID: {cl.ID: cl3.EVersion}, 404 }) 405 }) 406 } 407 408 func TestUpdaterFetchedNoNewData(t *testing.T) { 409 t.Parallel() 410 411 Convey("Updater skips updating the CL when no new data is fetched", t, func() { 412 ct := cvtesting.Test{} 413 ctx, cancel := ct.SetUp(t) 414 defer cancel() 415 416 pm, rm, tj := pmMock{}, rmMock{}, tjMock{} 417 u := NewUpdater(ct.TQDispatcher, NewMutator(ct.TQDispatcher, &pm, &rm, &tj)) 418 b := &fakeUpdaterBackend{} 419 u.RegisterBackend(b) 420 421 snap := &Snapshot{ 422 ExternalUpdateTime: timestamppb.New(ct.Clock.Now()), 423 Patchset: 2, 424 MinEquivalentPatchset: 1, 425 LuciProject: "luci-project", 426 Kind: nil, // but should be set in practice 427 } 428 acfg := &ApplicableConfig{Projects: []*ApplicableConfig_Project{ 429 { 430 Name: "luci-projectj", 431 ConfigGroupIds: []string{"hash/old"}, 432 }, 433 }} 434 // Put an existing CL. 435 cl := ExternalID("fake/1").MustCreateIfNotExists(ctx) 436 cl.ApplicableConfig = acfg 437 cl.Snapshot = snap 438 cl.EVersion++ 439 So(datastore.Put(ctx, cl), ShouldBeNil) 440 441 Convey("updaterBackend is aware that there is no new data", func() { 442 b.fetchResult = UpdateFields{} 443 }) 444 Convey("updaterBackend is not aware that it fetched the exact same data", func() { 445 b.fetchResult = UpdateFields{ 446 Snapshot: snap, 447 ApplicableConfig: acfg, 448 } 449 }) 450 451 err := u.handleCL(ctx, &UpdateCLTask{ 452 LuciProject: "luci-project", 453 ExternalId: "fake/1", 454 Requester: UpdateCLTask_PUBSUB_POLL}) 455 456 So(err, ShouldBeNil) 457 458 // Check the monitoring data 459 if b.fetchResult.IsEmpty() { 460 // Empty fetched result implies that it didn't even perform 461 // a fetch. Hence, no metrics should have been reported. 462 So(ct.TSMonStore.GetAll(ctx), ShouldBeNil) 463 } else { 464 // This is the case where a fetch was performed but 465 // the data was actually the same as the existing snapshot. 466 So(ct.TSMonSentValue(ctx, metrics.Internal.CLIngestionAttempted, 467 UpdateCLTask_PUBSUB_POLL.String(), // metric:requester, 468 false, // metric:changed == false 469 false, // metric:dep 470 "luci-project", // metric:project, 471 false, // metric:changed_snapshot == false 472 ), ShouldEqual, 1) 473 So(ct.TSMonSentDistr(ctx, metrics.Internal.CLIngestionLatency, 474 UpdateCLTask_PUBSUB_POLL.String(), // metric:requester, 475 false, // metric:dep 476 "luci-project", // metric:project, 477 false, // metric:changed_snapshot == false, 478 ), ShouldBeNil) 479 So(ct.TSMonSentDistr(ctx, metrics.Internal.CLIngestionLatencyWithoutFetch, 480 UpdateCLTask_PUBSUB_POLL.String(), // metric:requester, 481 false, // metric:dep 482 "luci-project", // metric:project, 483 false, // metric:changed_snapshot == false 484 ), ShouldBeNil) 485 } 486 487 // CL entity shouldn't change and notifications should not be emitted. 488 cl2 := reloadCL(ctx, cl) 489 So(cl2.EVersion, ShouldEqual, cl.EVersion) 490 // CL Mutator guarantees that EVersion is bumped on every write, but check 491 // the entire CL contents anyway in case there is a buggy by-pass of 492 // Mutator somewhere. 493 So(cl2, cvtesting.SafeShouldResemble, cl) 494 So(ct.TQ.Tasks(), ShouldBeEmpty) 495 }) 496 } 497 498 func TestUpdaterAccessRestriction(t *testing.T) { 499 t.Parallel() 500 501 Convey("Updater works correctly when backend denies access to the CL", t, func() { 502 // This is a long test, don't debug it first if other TestUpdater* tests are 503 // also failing. 504 ct := cvtesting.Test{} 505 ctx, cancel := ct.SetUp(t) 506 defer cancel() 507 508 pm, rm, tj := pmMock{}, rmMock{}, tjMock{} 509 u := NewUpdater(ct.TQDispatcher, NewMutator(ct.TQDispatcher, &pm, &rm, &tj)) 510 b := &fakeUpdaterBackend{} 511 u.RegisterBackend(b) 512 513 ////////////////////////////////////////////////////////////////////////// 514 // Phase 1: prepare an old CL previously fetched for another LUCI project. 515 ////////////////////////////////////////////////////////////////////////// 516 517 longTimeAgo := ct.Clock.Now().Add(-180 * 24 * time.Hour) 518 cl := ExternalID("fake/1").MustCreateIfNotExists(ctx) 519 cl.Snapshot = &Snapshot{ 520 ExternalUpdateTime: timestamppb.New(longTimeAgo), 521 Patchset: 2, 522 MinEquivalentPatchset: 1, 523 LuciProject: "previously-existing-project", 524 Kind: nil, // but should be set in practice, 525 } 526 cl.ApplicableConfig = &ApplicableConfig{Projects: []*ApplicableConfig_Project{ 527 { 528 Name: "previously-existing-project", 529 ConfigGroupIds: []string{"old-hash/old-name"}, 530 }, 531 }} 532 alsoLongTimeAgo := longTimeAgo.Add(time.Minute) 533 cl.Access = &Access{ByProject: map[string]*Access_Project{ 534 // One possibility is user makes a typo in the free-from Cq-Depend 535 // footer and accidentally referenced a CL from totally different 536 // project. 537 "another-project-with-invalid-cl-deps": {NoAccessTime: timestamppb.New(alsoLongTimeAgo)}, 538 }} 539 cl.EVersion++ 540 So(datastore.Put(ctx, cl), ShouldBeNil) 541 542 ////////////////////////////////////////////////////////////////////////// 543 // Phase 2: simulate a Fetch which got access denied from backend. 544 ////////////////////////////////////////////////////////////////////////// 545 546 b.fetchResult = UpdateFields{ 547 ApplicableConfig: &ApplicableConfig{Projects: []*ApplicableConfig_Project{ 548 // Note that the old project is no longer watching this CL. 549 { 550 Name: "luci-project", 551 ConfigGroupIds: []string{"hash/name"}, 552 }, 553 }}, 554 Snapshot: nil, // nothing was actually fetched. 555 AddDependentMeta: &Access{ByProject: map[string]*Access_Project{ 556 "luci-project": {NoAccessTime: timestamppb.New(ct.Clock.Now())}, 557 }}, 558 } 559 560 err := u.handleCL(ctx, &UpdateCLTask{LuciProject: "luci-project", ExternalId: "fake/1"}) 561 So(err, ShouldBeNil) 562 563 // Resulting CL entity should keep the Snapshot, rewrite ApplicableConfig, 564 // and merge Access. 565 cl2 := reloadCL(ctx, cl) 566 So(cl2.Snapshot, ShouldResembleProto, cl.Snapshot) 567 So(cl2.ApplicableConfig, ShouldResembleProto, b.fetchResult.ApplicableConfig) 568 So(cl2.Access, ShouldResembleProto, &Access{ByProject: map[string]*Access_Project{ 569 "another-project-with-invalid-cl-deps": {NoAccessTime: timestamppb.New(alsoLongTimeAgo)}, 570 "luci-project": {NoAccessTime: timestamppb.New(ct.Clock.Now())}, 571 }}) 572 // Notifications doesn't have to be sent to the project with invalid deps, 573 // as this update is irrelevant to the project. 574 ct.TQ.Run(ctx, tqtesting.StopWhenDrained()) 575 So(pm.byProject, ShouldResemble, map[string]map[common.CLID]int64{ 576 "luci-project": {cl.ID: cl2.EVersion}, 577 "previously-existing-project": {cl.ID: cl2.EVersion}, 578 }) 579 580 ////////////////////////////////////////////////////////////////////////// 581 // Phase 3: backend ACLs are fixed. 582 ////////////////////////////////////////////////////////////////////////// 583 ct.Clock.Add(time.Hour) 584 b.reset() 585 b.fetchResult = UpdateFields{ 586 Snapshot: &Snapshot{ 587 ExternalUpdateTime: timestamppb.New(ct.Clock.Now()), 588 Patchset: 4, 589 MinEquivalentPatchset: 1, 590 LuciProject: "luci-project", 591 Kind: nil, // but should be set in practice 592 }, 593 ApplicableConfig: cl2.ApplicableConfig, // same as before 594 DelAccess: []string{"luci-project"}, 595 } 596 err = u.handleCL(ctx, &UpdateCLTask{LuciProject: "luci-project", ExternalId: "fake/1"}) 597 So(err, ShouldBeNil) 598 cl3 := reloadCL(ctx, cl) 599 So(cl3.Snapshot, ShouldResembleProto, b.fetchResult.Snapshot) // replaced 600 So(cl3.ApplicableConfig, ShouldResembleProto, cl2.ApplicableConfig) // same 601 So(cl3.Access, ShouldResembleProto, &Access{ByProject: map[string]*Access_Project{ // updated 602 // No more "luci-project" entry. 603 "another-project-with-invalid-cl-deps": {NoAccessTime: timestamppb.New(alsoLongTimeAgo)}, 604 }}) 605 // Notifications are still not sent to the project with invalid deps. 606 ct.TQ.Run(ctx, tqtesting.StopWhenDrained()) 607 So(pm.byProject, ShouldResemble, map[string]map[common.CLID]int64{ 608 "luci-project": {cl.ID: cl3.EVersion}, 609 "previously-existing-project": {cl.ID: cl3.EVersion}, 610 }) 611 }) 612 } 613 614 func TestUpdaterHandlesErrors(t *testing.T) { 615 t.Parallel() 616 617 Convey("Updater handles errors", t, func() { 618 ct := cvtesting.Test{} 619 ctx, cancel := ct.SetUp(t) 620 defer cancel() 621 622 u := NewUpdater(ct.TQDispatcher, nil) 623 624 Convey("bails permanently in cases which should not happen", func() { 625 Convey("No ID given", func() { 626 err := u.handleCL(ctx, &UpdateCLTask{ 627 LuciProject: "luci-project", 628 }) 629 So(err, ShouldErrLike, "invalid task input") 630 So(tq.Fatal.In(err), ShouldBeTrue) 631 }) 632 Convey("No LUCI project given", func() { 633 err := u.handleCL(ctx, &UpdateCLTask{ 634 ExternalId: "fake/1", 635 }) 636 So(err, ShouldErrLike, "invalid task input") 637 So(tq.Fatal.In(err), ShouldBeTrue) 638 }) 639 Convey("Contradicting external and internal IDs", func() { 640 cl1 := ExternalID("fake/1").MustCreateIfNotExists(ctx) 641 cl2 := ExternalID("fake/2").MustCreateIfNotExists(ctx) 642 err := u.handleCL(ctx, &UpdateCLTask{ 643 LuciProject: "luci-project", 644 Id: int64(cl1.ID), 645 ExternalId: string(cl2.ExternalID), 646 }) 647 So(err, ShouldErrLike, "invalid task") 648 So(tq.Fatal.In(err), ShouldBeTrue) 649 }) 650 Convey("Internal ID doesn't actually exist", func() { 651 // While in most cases this is a bug, it can happen in prod 652 // if an old CL is being deleted due to data retention policy at the 653 // same time as something else inside the CV is requesting a refresh of 654 // the CL against external system. One example of this is if a new CL 655 // mistakenly marked a very old CL as a dependency. 656 err := u.handleCL(ctx, &UpdateCLTask{ 657 Id: 404, 658 LuciProject: "luci-project", 659 }) 660 So(err, ShouldErrLike, datastore.ErrNoSuchEntity) 661 So(tq.Fatal.In(err), ShouldBeTrue) 662 }) 663 Convey("CL from unregistered backend", func() { 664 err := u.handleCL(ctx, &UpdateCLTask{ 665 ExternalId: "unknown/404", 666 LuciProject: "luci-project", 667 }) 668 So(err, ShouldErrLike, "backend is not supported") 669 So(tq.Fatal.In(err), ShouldBeTrue) 670 }) 671 }) 672 673 Convey("Respects TQErrorSpec", func() { 674 ignoreMe := errors.New("ignore-me") 675 b := &fakeUpdaterBackend{ 676 tqErrorSpec: common.TQIfy{ 677 KnownIgnore: []error{ignoreMe}, 678 }, 679 fetchError: errors.Annotate(ignoreMe, "something went wrong").Err(), 680 } 681 u.RegisterBackend(b) 682 err := u.handleCL(ctx, &UpdateCLTask{LuciProject: "lp", ExternalId: "fake/1"}) 683 So(tq.Ignore.In(err), ShouldBeTrue) 684 So(err, ShouldErrLike, "ignore-me") 685 }) 686 }) 687 } 688 689 func TestUpdaterAvoidsFetchWhenPossible(t *testing.T) { 690 t.Parallel() 691 692 Convey("Updater skips fetching when possible", t, func() { 693 ct := cvtesting.Test{} 694 ctx, cancel := ct.SetUp(t) 695 defer cancel() 696 697 u := NewUpdater(ct.TQDispatcher, NewMutator(ct.TQDispatcher, &pmMock{}, &rmMock{}, &tjMock{})) 698 b := &fakeUpdaterBackend{} 699 u.RegisterBackend(b) 700 701 // Simulate a perfect case for avoiding the snapshot. 702 cl := ExternalID("fake/123").MustCreateIfNotExists(ctx) 703 cl.Snapshot = &Snapshot{ 704 ExternalUpdateTime: timestamppb.New(ct.Clock.Now()), 705 Patchset: 2, 706 MinEquivalentPatchset: 1, 707 LuciProject: "luci-project", 708 Kind: nil, // but should be set in practice, 709 } 710 cl.ApplicableConfig = &ApplicableConfig{ 711 Projects: []*ApplicableConfig_Project{ 712 { 713 Name: "luci-project", 714 ConfigGroupIds: []string{"hash/name"}, 715 }, 716 }, 717 } 718 cl.EVersion++ 719 So(datastore.Put(ctx, cl), ShouldBeNil) 720 721 task := &UpdateCLTask{ 722 LuciProject: "luci-project", 723 ExternalId: string(cl.ExternalID), 724 Hint: externalTime(ct.Clock.Now()), 725 } 726 // Typically, ApplicableConfig config (i.e. which LUCI project watch this 727 // CL) doesn't change, too. 728 b.lookupACfgResult = cl.ApplicableConfig 729 730 Convey("skips Fetch", func() { 731 Convey("happy path: everything is up to date", func() { 732 So(u.handleCL(ctx, task), ShouldBeNil) 733 734 cl2 := reloadCL(ctx, cl) 735 // Quick-fail if EVersion changes. 736 So(cl2.EVersion, ShouldEqual, cl.EVersion) 737 // Ensure nothing about the CL actually changed. 738 So(cl2, cvtesting.SafeShouldResemble, cl) 739 740 So(b.wasLookupApplicableConfigCalled(), ShouldBeTrue) 741 So(b.wasFetchCalled(), ShouldBeFalse) 742 }) 743 744 Convey("special path: changed ApplicableConfig is saved", func() { 745 Convey("CL is not watched by any project", func(c C) { 746 b.lookupACfgResult = &ApplicableConfig{} 747 }) 748 Convey("CL is watched by another project", func(c C) { 749 b.lookupACfgResult = &ApplicableConfig{Projects: []*ApplicableConfig_Project{ 750 { 751 Name: "other-project", 752 ConfigGroupIds: []string{"ohter-hash/other-name"}, 753 }, 754 }} 755 }) 756 Convey("CL is additionally watched by another project", func(c C) { 757 b.lookupACfgResult.Projects = append(b.lookupACfgResult.Projects, &ApplicableConfig_Project{ 758 Name: "other-project", 759 ConfigGroupIds: []string{"ohter-hash/other-name"}, 760 }) 761 }) 762 // Either way, fetch can be skipped & Snapshot can be preserved, but the 763 // ApplicableConfig must be updated. 764 So(u.handleCL(ctx, task), ShouldBeNil) 765 So(b.wasFetchCalled(), ShouldBeFalse) 766 cl2 := reloadCL(ctx, cl) 767 So(cl2.Snapshot, ShouldResembleProto, cl.Snapshot) 768 So(cl2.ApplicableConfig, ShouldResembleProto, b.lookupACfgResult) 769 }) 770 771 Convey("meta_rev_id is the same", func() { 772 Convey("even if the CL entity is really old", func(c C) { 773 ct.Clock.Add(autoRefreshAfter + time.Minute) 774 }) 775 776 cl.Snapshot.Kind = &Snapshot_Gerrit{Gerrit: &Gerrit{ 777 Info: &gerrit.ChangeInfo{MetaRevId: "deadbeef"}, 778 }} 779 So(datastore.Put(ctx, cl), ShouldBeNil) 780 task.Hint.MetaRevId = "deadbeef" 781 So(u.handleCL(ctx, task), ShouldBeNil) 782 So(b.wasFetchCalled(), ShouldBeFalse) 783 }) 784 }) 785 786 Convey("doesn't skip Fetch because ...", func() { 787 saveCLAndRun := func() { 788 cl.EVersion++ 789 So(datastore.Put(ctx, cl), ShouldBeNil) 790 So(u.handleCL(ctx, task), ShouldBeNil) 791 } 792 Convey("no snapshot", func(c C) { 793 cl.Snapshot = nil 794 saveCLAndRun() 795 So(b.wasFetchCalled(), ShouldBeTrue) 796 }) 797 Convey("snapshot marked outdated", func(c C) { 798 cl.Snapshot.Outdated = &Snapshot_Outdated{} 799 saveCLAndRun() 800 So(b.wasFetchCalled(), ShouldBeTrue) 801 }) 802 Convey("snapshot is definitely old", func(c C) { 803 cl.Snapshot.ExternalUpdateTime.Seconds -= 3600 804 saveCLAndRun() 805 So(b.wasFetchCalled(), ShouldBeTrue) 806 }) 807 Convey("snapshot might be old", func(c C) { 808 task.Hint = nil 809 saveCLAndRun() 810 So(b.wasFetchCalled(), ShouldBeTrue) 811 }) 812 Convey("CL entity is really old", func(c C) { 813 ct.Clock.Add(autoRefreshAfter + time.Minute) 814 saveCLAndRun() 815 So(b.wasFetchCalled(), ShouldBeTrue) 816 }) 817 Convey("snapshot is for a different project", func(c C) { 818 cl.Snapshot.LuciProject = "other" 819 saveCLAndRun() 820 So(b.wasFetchCalled(), ShouldBeTrue) 821 }) 822 Convey("backend isn't sure about applicable config", func(c C) { 823 b.lookupACfgResult = nil 824 saveCLAndRun() 825 So(b.wasFetchCalled(), ShouldBeTrue) 826 }) 827 Convey("CL entity has record of prior access restriction", func(c C) { 828 cl.Access = &Access{ 829 ByProject: map[string]*Access_Project{ 830 "luci-project": { 831 // In practice, actual fields are set, but they aren't important 832 // for this test. 833 }, 834 }, 835 } 836 saveCLAndRun() 837 So(b.wasFetchCalled(), ShouldBeTrue) 838 }) 839 Convey("meta_rev_id is different", func() { 840 cl.Snapshot.Kind = &Snapshot_Gerrit{Gerrit: &Gerrit{ 841 Info: &gerrit.ChangeInfo{MetaRevId: "deadbeef"}, 842 }} 843 So(datastore.Put(ctx, cl), ShouldBeNil) 844 task.Hint.MetaRevId = "foo" 845 So(u.handleCL(ctx, task), ShouldBeNil) 846 So(b.wasFetchCalled(), ShouldBeTrue) 847 }) 848 }) 849 850 Convey("aborts before the Fetch because LookupApplicableConfig failed", func() { 851 b.lookupACfgError = errors.New("boo", transient.Tag) 852 err := u.handleCL(ctx, task) 853 So(err, ShouldErrLike, b.lookupACfgError) 854 So(b.wasFetchCalled(), ShouldBeFalse) 855 }) 856 }) 857 } 858 859 func TestUpdaterResolveAndScheduleDepsUpdate(t *testing.T) { 860 t.Parallel() 861 862 Convey("ResolveAndScheduleDepsUpdate correctly resolves deps", t, func() { 863 ct := cvtesting.Test{} 864 ctx, cancel := ct.SetUp(t) 865 defer cancel() 866 867 u := NewUpdater(ct.TQDispatcher, NewMutator(ct.TQDispatcher, &pmMock{}, &rmMock{}, &tjMock{})) 868 869 scheduledUpdates := func() (out []string) { 870 for _, p := range ct.TQ.Tasks().Payloads() { 871 if task, ok := p.(*UpdateCLTask); ok { 872 // Each scheduled task should have ID set, as it is known, 873 // to save on future lookup. 874 So(task.GetId(), ShouldNotEqual, 0) 875 e := task.GetExternalId() 876 // But also ExternalID, primarily for debugging. 877 So(e, ShouldNotBeEmpty) 878 out = append(out, e) 879 } 880 } 881 sort.Strings(out) 882 return out 883 } 884 eids := func(cls ...*CL) []string { 885 out := make([]string, len(cls)) 886 for i, cl := range cls { 887 out[i] = string(cl.ExternalID) 888 } 889 sort.Strings(out) 890 return out 891 } 892 893 // Setup 4 existing CLs in various states. 894 const lProject = "luci-project" 895 // Various backend IDs are used here for test readability and debug-ability 896 // only. In practice, all deps likely come from the same backend. 897 var ( 898 clBareBones = ExternalID("bare-bones/10").MustCreateIfNotExists(ctx) 899 clUpToDate = ExternalID("up-to-date/12").MustCreateIfNotExists(ctx) 900 clUpToDateDiffProject = ExternalID("up-to-date-diff-project/13").MustCreateIfNotExists(ctx) 901 ) 902 clUpToDate.Snapshot = &Snapshot{ 903 ExternalUpdateTime: timestamppb.New(ct.Clock.Now()), 904 Patchset: 1, 905 MinEquivalentPatchset: 1, 906 LuciProject: lProject, 907 } 908 clUpToDateDiffProject.Snapshot = proto.Clone(clUpToDate.Snapshot).(*Snapshot) 909 clUpToDateDiffProject.Snapshot.LuciProject = "other-project" 910 So(datastore.Put(ctx, clUpToDate, clUpToDateDiffProject), ShouldBeNil) 911 912 Convey("no deps", func() { 913 deps, err := u.ResolveAndScheduleDepsUpdate(ctx, lProject, nil, UpdateCLTask_RUN_POKE) 914 So(err, ShouldBeNil) 915 So(deps, ShouldBeEmpty) 916 }) 917 918 Convey("only existing CLs", func() { 919 deps, err := u.ResolveAndScheduleDepsUpdate(ctx, lProject, map[ExternalID]DepKind{ 920 clBareBones.ExternalID: DepKind_SOFT, 921 clUpToDate.ExternalID: DepKind_HARD, 922 clUpToDateDiffProject.ExternalID: DepKind_SOFT, 923 }, UpdateCLTask_RUN_POKE) 924 So(err, ShouldBeNil) 925 So(deps, ShouldResembleProto, sortDeps([]*Dep{ 926 {Clid: int64(clBareBones.ID), Kind: DepKind_SOFT}, 927 {Clid: int64(clUpToDate.ID), Kind: DepKind_HARD}, 928 {Clid: int64(clUpToDateDiffProject.ID), Kind: DepKind_SOFT}, 929 })) 930 // Update for the `clUpToDate` is not necessary. 931 So(scheduledUpdates(), ShouldResemble, eids(clBareBones, clUpToDateDiffProject)) 932 }) 933 934 Convey("only new CLs", func() { 935 deps, err := u.ResolveAndScheduleDepsUpdate(ctx, lProject, map[ExternalID]DepKind{ 936 "new/1": DepKind_SOFT, 937 "new/2": DepKind_HARD, 938 }, UpdateCLTask_RUN_POKE) 939 So(err, ShouldBeNil) 940 cl1 := ExternalID("new/1").MustCreateIfNotExists(ctx) 941 cl2 := ExternalID("new/2").MustCreateIfNotExists(ctx) 942 So(deps, ShouldResembleProto, sortDeps([]*Dep{ 943 {Clid: int64(cl1.ID), Kind: DepKind_SOFT}, 944 {Clid: int64(cl2.ID), Kind: DepKind_HARD}, 945 })) 946 So(scheduledUpdates(), ShouldResemble, eids(cl1, cl2)) 947 }) 948 949 Convey("mix old and new CLs", func() { 950 deps, err := u.ResolveAndScheduleDepsUpdate(ctx, lProject, map[ExternalID]DepKind{ 951 "new/1": DepKind_SOFT, 952 "new/2": DepKind_HARD, 953 clBareBones.ExternalID: DepKind_HARD, 954 clUpToDate.ExternalID: DepKind_SOFT, 955 }, UpdateCLTask_RUN_POKE) 956 So(err, ShouldBeNil) 957 cl1 := ExternalID("new/1").MustCreateIfNotExists(ctx) 958 cl2 := ExternalID("new/2").MustCreateIfNotExists(ctx) 959 So(deps, ShouldResembleProto, sortDeps([]*Dep{ 960 {Clid: int64(cl1.ID), Kind: DepKind_SOFT}, 961 {Clid: int64(cl2.ID), Kind: DepKind_HARD}, 962 {Clid: int64(clBareBones.ID), Kind: DepKind_HARD}, 963 {Clid: int64(clUpToDate.ID), Kind: DepKind_SOFT}, 964 })) 965 // Update for the `clUpToDate` is not necessary. 966 So(scheduledUpdates(), ShouldResemble, eids(cl1, cl2, clBareBones)) 967 }) 968 969 Convey("high number of dependency CLs", func() { 970 const clCount = 1024 971 depCLMap := make(map[ExternalID]DepKind, clCount) 972 depCLs := make([]*CL, clCount) 973 for i := 0; i < clCount; i++ { 974 depCLs[i] = ExternalID(fmt.Sprintf("high-dep-cl/%04d", i)).MustCreateIfNotExists(ctx) 975 depCLMap[depCLs[i].ExternalID] = DepKind_HARD 976 } 977 978 deps, err := u.ResolveAndScheduleDepsUpdate(ctx, lProject, depCLMap, UpdateCLTask_RUN_POKE) 979 So(err, ShouldBeNil) 980 expectedDeps := make([]*Dep, clCount) 981 for i, depCL := range depCLs { 982 expectedDeps[i] = &Dep{ 983 Clid: int64(depCL.ID), 984 Kind: DepKind_HARD, 985 } 986 } 987 So(deps, ShouldResembleProto, expectedDeps) 988 So(scheduledUpdates(), ShouldResemble, eids(depCLs...)) 989 }) 990 }) 991 } 992 993 // fakeUpdaterBackend is a fake UpdaterBackend. 994 // 995 // It provides functionality which is a subset of what gomock would generate, 996 // but with additional assertions to validate the contract between Updater and 997 // its backend. 998 type fakeUpdaterBackend struct { 999 tqErrorSpec common.TQIfy 1000 1001 // LookupApplicableConfig related fields: 1002 1003 lookupACfgCL *CL // copy 1004 lookupACfgResult *ApplicableConfig 1005 lookupACfgError error 1006 1007 // Fetch related fields: 1008 fetchCL *CL // copy 1009 fetchProject string 1010 fetchUpdatedHint time.Time 1011 fetchResult UpdateFields 1012 fetchError error 1013 1014 backendSnapshotUpdated bool 1015 } 1016 1017 func (f *fakeUpdaterBackend) Kind() string { 1018 return "fake" 1019 } 1020 1021 func (f *fakeUpdaterBackend) TQErrorSpec() common.TQIfy { 1022 return f.tqErrorSpec 1023 } 1024 1025 func (f *fakeUpdaterBackend) wasLookupApplicableConfigCalled() bool { 1026 return f.lookupACfgCL != nil 1027 } 1028 1029 func (f *fakeUpdaterBackend) LookupApplicableConfig(ctx context.Context, saved *CL) (*ApplicableConfig, error) { 1030 So(f.wasLookupApplicableConfigCalled(), ShouldBeFalse) 1031 1032 // Check contract with a backend: 1033 So(saved, ShouldNotBeNil) 1034 So(saved.ID, ShouldNotEqual, 0) 1035 So(saved.ExternalID, ShouldNotBeEmpty) 1036 So(saved.Snapshot, ShouldNotBeNil) 1037 1038 // Shallow-copy to catch some mistakes in test. 1039 f.lookupACfgCL = &CL{} 1040 *f.lookupACfgCL = *saved 1041 return f.lookupACfgResult, f.lookupACfgError 1042 } 1043 1044 func (f *fakeUpdaterBackend) wasFetchCalled() bool { 1045 return f.fetchCL != nil 1046 } 1047 1048 func (f *fakeUpdaterBackend) Fetch(ctx context.Context, in *FetchInput) (UpdateFields, error) { 1049 So(f.wasFetchCalled(), ShouldBeFalse) 1050 1051 // Check contract with a backend: 1052 So(in.CL, ShouldNotBeNil) 1053 So(in.CL.ExternalID, ShouldNotBeEmpty) 1054 1055 // Shallow-copy to catch some mistakes in test. 1056 f.fetchCL = &CL{} 1057 *f.fetchCL = *in.CL 1058 f.fetchProject = in.Project 1059 f.fetchUpdatedHint = in.UpdatedHint 1060 return f.fetchResult, f.fetchError 1061 } 1062 1063 func (f *fakeUpdaterBackend) HasChanged(cvCurrent, backendCurrent *Snapshot) bool { 1064 switch { 1065 case backendCurrent == nil: 1066 panic("impossible. Backend must have a non-nil snapshot") 1067 case cvCurrent == nil: 1068 return true 1069 case backendCurrent.GetExternalUpdateTime().AsTime().After(cvCurrent.GetExternalUpdateTime().AsTime()): 1070 return true 1071 case backendCurrent.GetPatchset() > cvCurrent.GetPatchset(): 1072 return true 1073 case f.backendSnapshotUpdated: 1074 return true 1075 default: 1076 return false 1077 } 1078 } 1079 1080 // reset resets the fake for the next use. 1081 func (f *fakeUpdaterBackend) reset() { 1082 *f = fakeUpdaterBackend{} 1083 } 1084 1085 // reloadCL loads a new CL from Datastore. 1086 // 1087 // Doesn't re-use the object. 1088 func reloadCL(ctx context.Context, cl *CL) *CL { 1089 ret := &CL{ID: cl.ID} 1090 if err := datastore.Get(ctx, ret); err != nil { 1091 panic(err) 1092 } 1093 return ret 1094 }