go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/changelist/mutator_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 "math/rand" 21 "sync" 22 "testing" 23 "time" 24 25 "golang.org/x/sync/errgroup" 26 "google.golang.org/protobuf/types/known/timestamppb" 27 28 "go.chromium.org/luci/common/errors" 29 "go.chromium.org/luci/common/retry/transient" 30 "go.chromium.org/luci/gae/filter/featureBreaker" 31 "go.chromium.org/luci/gae/filter/featureBreaker/flaky" 32 "go.chromium.org/luci/gae/service/datastore" 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 38 . "github.com/smartystreets/goconvey/convey" 39 40 . "go.chromium.org/luci/common/testing/assertions" 41 ) 42 43 func TestMutatorSingleCL(t *testing.T) { 44 t.Parallel() 45 46 Convey("Mutator works on a single CL", t, func() { 47 ct := cvtesting.Test{} 48 ctx, cancel := ct.SetUp(t) 49 defer cancel() 50 51 const lProject = "infra" 52 const run1 = lProject + "/1" 53 const run2 = lProject + "/2" 54 const gHost = "x-review.example.com" 55 const gChange = 44 56 eid := MustGobID(gHost, gChange) 57 58 pm := pmMock{} 59 rm := rmMock{} 60 tj := tjMock{} 61 m := NewMutator(ct.TQDispatcher, &pm, &rm, &tj) 62 63 execBatchOnCLUpdatedTask := func() { 64 So(ct.TQ.Tasks(), ShouldHaveLength, 1) 65 So(ct.TQ.Tasks()[0].Class, ShouldResemble, BatchOnCLUpdatedTaskClass) 66 ct.TQ.Run(ctx, tqtesting.StopAfterTask(BatchOnCLUpdatedTaskClass)) 67 } 68 expectNoNotifications := func() { 69 So(ct.TQ.Tasks(), ShouldHaveLength, 0) 70 So(pm.byProject, ShouldBeEmpty) 71 So(rm.byRun, ShouldBeEmpty) 72 So(tj.clsNotified, ShouldBeEmpty) 73 } 74 75 Convey("Upsert method", func() { 76 Convey("creates", func() { 77 s := makeSnapshot(lProject, ct.Clock.Now()) 78 cl, err := m.Upsert(ctx, lProject, eid, func(cl *CL) error { 79 cl.Snapshot = s 80 return nil 81 }) 82 So(err, ShouldBeNil) 83 So(cl.ExternalID, ShouldResemble, eid) 84 So(cl.EVersion, ShouldEqual, 1) 85 So(cl.UpdateTime, ShouldEqual, ct.Clock.Now()) 86 So(cl.RetentionKey, ShouldNotBeEmpty) 87 So(cl.Snapshot, ShouldResembleProto, s) 88 89 execBatchOnCLUpdatedTask() 90 So(pm.byProject, ShouldResemble, map[string]map[common.CLID]int64{ 91 lProject: {cl.ID: cl.EVersion}, 92 }) 93 So(rm.byRun, ShouldBeEmpty) 94 So(tj.clsNotified, ShouldBeEmpty) 95 }) 96 97 Convey("skips creation", func() { 98 // This is a special case which isn't supposed to be needed, 99 // but it's kept here for completeness. 100 cl, err := m.Upsert(ctx, lProject, eid, func(cl *CL) error { 101 return ErrStopMutation 102 }) 103 So(err, ShouldBeNil) 104 So(cl, ShouldBeNil) 105 expectNoNotifications() 106 }) 107 108 Convey("updates", func() { 109 s1 := makeSnapshot(lProject, ct.Clock.Now()) 110 cl := eid.MustCreateIfNotExists(ctx) 111 cl.Snapshot = s1 112 cl.IncompleteRuns = common.MakeRunIDs(run1) 113 So(datastore.Put(ctx, cl), ShouldBeNil) 114 115 ct.Clock.Add(time.Second) 116 s2 := makeSnapshot(lProject, ct.Clock.Now()) 117 s2.MinEquivalentPatchset++ 118 var priorSnapshot *Snapshot 119 cl, err := m.Upsert(ctx, lProject, eid, func(cl *CL) error { 120 if priorSnapshot == nil { 121 priorSnapshot = cl.Snapshot 122 } 123 cl.Snapshot = s2 124 cl.IncompleteRuns.InsertSorted(run2) // idempotent 125 return nil 126 }) 127 So(err, ShouldBeNil) 128 129 So(priorSnapshot, ShouldResembleProto, s1) 130 So(cl.ExternalID, ShouldResemble, eid) 131 So(cl.EVersion, ShouldEqual, 2) 132 So(cl.UpdateTime, ShouldEqual, ct.Clock.Now().UTC()) 133 So(cl.RetentionKey, ShouldNotBeEmpty) 134 So(cl.Snapshot, ShouldResembleProto, s2) 135 So(tj.clsNotified[0], ShouldEqual, cl.ID) 136 137 execBatchOnCLUpdatedTask() 138 So(pm.byProject, ShouldResemble, map[string]map[common.CLID]int64{ 139 lProject: {cl.ID: cl.EVersion}, 140 }) 141 So(rm.byRun, ShouldResemble, map[common.RunID]map[common.CLID]int64{ 142 run1: {cl.ID: cl.EVersion}, 143 run2: {cl.ID: cl.EVersion}, 144 }) 145 }) 146 147 Convey("skips an update", func() { 148 priorCL := eid.MustCreateIfNotExists(ctx) 149 150 ct.Clock.Add(time.Second) 151 cl, err := m.Upsert(ctx, lProject, eid, func(cl *CL) error { 152 return ErrStopMutation 153 }) 154 So(err, ShouldBeNil) 155 156 So(cl.ExternalID, ShouldResemble, eid) 157 So(cl.EVersion, ShouldEqual, priorCL.EVersion) 158 So(cl.UpdateTime, ShouldEqual, priorCL.UpdateTime) 159 So(cl.RetentionKey, ShouldEqual, priorCL.RetentionKey) 160 161 So(pm.byProject, ShouldBeEmpty) 162 So(rm.byRun, ShouldBeEmpty) 163 So(tj.clsNotified, ShouldBeEmpty) 164 }) 165 166 Convey("propagates error without wrapping", func() { 167 myErr := errors.New("my error") 168 _, err := m.Upsert(ctx, lProject, eid, func(cl *CL) error { 169 return myErr 170 }) 171 So(myErr, ShouldEqual, err) 172 }) 173 }) 174 175 Convey("Update method", func() { 176 Convey("updates", func() { 177 s1 := makeSnapshot("prior-project", ct.Clock.Now()) 178 priorCL := eid.MustCreateIfNotExists(ctx) 179 priorCL.Snapshot = s1 180 So(datastore.Put(ctx, priorCL), ShouldBeNil) 181 182 ct.Clock.Add(time.Second) 183 s2 := makeSnapshot(lProject, ct.Clock.Now()) 184 s2.MinEquivalentPatchset++ 185 cl, err := m.Update(ctx, lProject, priorCL.ID, func(cl *CL) error { 186 cl.Snapshot = s2 187 return nil 188 }) 189 So(err, ShouldBeNil) 190 191 So(cl.ID, ShouldResemble, priorCL.ID) 192 So(cl.ExternalID, ShouldResemble, eid) 193 So(cl.EVersion, ShouldEqual, 2) 194 So(cl.UpdateTime, ShouldEqual, ct.Clock.Now().UTC()) 195 So(cl.RetentionKey, ShouldNotBeEmpty) 196 So(cl.Snapshot, ShouldResembleProto, s2) 197 198 execBatchOnCLUpdatedTask() 199 So(pm.byProject[lProject][cl.ID], ShouldEqual, cl.EVersion) 200 So(pm.byProject, ShouldResemble, map[string]map[common.CLID]int64{ 201 "prior-project": {cl.ID: cl.EVersion}, 202 lProject: {cl.ID: cl.EVersion}, 203 }) 204 So(rm.byRun, ShouldBeEmpty) 205 So(tj.clsNotified[0], ShouldEqual, cl.ID) 206 }) 207 208 Convey("skips an actual update", func() { 209 priorCL := eid.MustCreateIfNotExists(ctx) 210 211 ct.Clock.Add(time.Second) 212 cl, err := m.Update(ctx, lProject, priorCL.ID, func(cl *CL) error { 213 return ErrStopMutation 214 }) 215 So(err, ShouldBeNil) 216 217 So(cl.ID, ShouldResemble, priorCL.ID) 218 So(cl.ExternalID, ShouldResemble, eid) 219 So(cl.EVersion, ShouldEqual, priorCL.EVersion) 220 So(cl.UpdateTime, ShouldEqual, priorCL.UpdateTime) 221 So(cl.RetentionKey, ShouldEqual, priorCL.RetentionKey) 222 223 expectNoNotifications() 224 }) 225 226 Convey("propagates error without wrapping", func() { 227 priorCL := eid.MustCreateIfNotExists(ctx) 228 229 myErr := errors.New("my error") 230 _, err := m.Update(ctx, lProject, priorCL.ID, func(cl *CL) error { 231 return myErr 232 }) 233 So(myErr, ShouldEqual, err) 234 }) 235 236 Convey("errors out non-transiently if CL doesn't exist", func() { 237 _, err := m.Update(ctx, lProject, 123, func(cl *CL) error { 238 panic("must not be called") 239 }) 240 So(errors.Unwrap(err), ShouldEqual, datastore.ErrNoSuchEntity) 241 So(transient.Tag.In(err), ShouldBeFalse) 242 }) 243 }) 244 245 Convey("Invalid MutationCallback", func() { 246 type badCallback func(cl *CL) 247 cases := func(kind string, repro func(bad badCallback)) { 248 Convey(kind, func() { 249 So(func() { repro(func(cl *CL) { cl.EVersion = 2 }) }, ShouldPanicLike, "CL.EVersion") 250 So(func() { repro(func(cl *CL) { cl.UpdateTime = ct.Clock.Now() }) }, ShouldPanicLike, "CL.UpdateTime") 251 So(func() { repro(func(cl *CL) { cl.ID++ }) }, ShouldPanicLike, "CL.ID") 252 So(func() { repro(func(cl *CL) { cl.ExternalID = "don't do this" }) }, ShouldPanicLike, "CL.ExternalID") 253 }) 254 } 255 cases("Upsert creation", func(bad badCallback) { 256 _, _ = m.Upsert(ctx, lProject, eid, func(cl *CL) error { 257 bad(cl) 258 return nil 259 }) 260 }) 261 cases("Upsert update", func(bad badCallback) { 262 eid.MustCreateIfNotExists(ctx) 263 ct.Clock.Add(time.Second) 264 _, _ = m.Upsert(ctx, lProject, eid, func(cl *CL) error { 265 bad(cl) 266 return nil 267 }) 268 }) 269 cases("Update", func(bad badCallback) { 270 cl := eid.MustCreateIfNotExists(ctx) 271 ct.Clock.Add(time.Second) 272 _, _ = m.Update(ctx, lProject, cl.ID, func(cl *CL) error { 273 bad(cl) 274 return nil 275 }) 276 }) 277 }) 278 }) 279 } 280 281 func TestMutatorBatch(t *testing.T) { 282 t.Parallel() 283 284 Convey("Mutator works on batch of CLs", t, func() { 285 ct := cvtesting.Test{} 286 ctx, cancel := ct.SetUp(t) 287 defer cancel() 288 289 const lProjectAlt = "alt" 290 const lProject = "infra" 291 const run1 = lProject + "/1" 292 const run2 = lProject + "/2" 293 const run3 = lProject + "/3" 294 const gHost = "x-review.example.com" 295 const gChangeFirst = 100000 296 const N = 12 297 298 pm := pmMock{} 299 rm := rmMock{} 300 tj := tjMock{} 301 m := NewMutator(ct.TQDispatcher, &pm, &rm, &tj) 302 303 Convey(fmt.Sprintf("with %d CLs already in Datastore", N), func() { 304 var clids common.CLIDs 305 var expectedAltProject, expectedRun1, expectedRun2 common.CLIDs 306 for gChange := gChangeFirst; gChange < gChangeFirst+N; gChange++ { 307 cl := MustGobID(gHost, int64(gChange)).MustCreateIfNotExists(ctx) 308 clids = append(clids, cl.ID) 309 if gChange%2 == 0 { 310 cl.Snapshot = makeSnapshot(lProjectAlt, ct.Clock.Now()) 311 expectedAltProject = append(expectedAltProject, cl.ID) 312 } else { 313 cl.Snapshot = makeSnapshot(lProject, ct.Clock.Now()) 314 } 315 if gChange%3 == 0 { 316 cl.IncompleteRuns = append(cl.IncompleteRuns, run1) 317 expectedRun1 = append(expectedRun1, cl.ID) 318 319 } 320 if gChange%5 == 0 { 321 cl.IncompleteRuns = append(cl.IncompleteRuns, run2) 322 expectedRun2 = append(expectedRun2, cl.ID) 323 } 324 // Ensure each CL has unique EVersion later on. 325 cl.EVersion = int64(10 * gChange) 326 So(datastore.Put(ctx, cl), ShouldBeNil) 327 } 328 ct.Clock.Add(time.Minute) 329 330 // In all cases below, run3 is added to the list of incomplete CLs. 331 verify := func(resCLs []*CL) { 332 // Ensure the returned CLs are exactly what was stored in Datastore, 333 // and compute eversion map at the same time. 334 dsCLs, err := LoadCLsByIDs(ctx, clids) 335 So(err, ShouldBeNil) 336 eversions := make(map[common.CLID]int64, len(dsCLs)) 337 for i := range dsCLs { 338 So(dsCLs[i].IncompleteRuns.ContainsSorted(run3), ShouldBeTrue) 339 So(dsCLs[i].ID, ShouldEqual, resCLs[i].ID) 340 So(dsCLs[i].EVersion, ShouldEqual, resCLs[i].EVersion) 341 So(dsCLs[i].UpdateTime, ShouldEqual, resCLs[i].UpdateTime) 342 So(dsCLs[i].RetentionKey, ShouldEqual, resCLs[i].RetentionKey) 343 So(dsCLs[i].IncompleteRuns, ShouldResemble, resCLs[i].IncompleteRuns) 344 eversions[dsCLs[i].ID] = dsCLs[i].EVersion 345 } 346 347 // Ensure Project and Run managers were notified correctly. 348 assertNotified := func(actual map[common.CLID]int64, expectedIDs common.CLIDs) { 349 expected := make(map[common.CLID]int64, len(expectedIDs)) 350 for _, id := range expectedIDs { 351 expected[id] = eversions[id] 352 } 353 So(actual, ShouldResemble, expected) 354 } 355 // The project in the context of which CLs were mutated must be notified 356 // on all CLs. 357 assertNotified(pm.byProject[lProject], clids) 358 // Ditto for the run3, which was added to all CLs. 359 assertNotified(rm.byRun[run3], clids) 360 // Others must be notified on relevant CLs, only. 361 assertNotified(pm.byProject[lProjectAlt], expectedAltProject) 362 assertNotified(rm.byRun[run1], expectedRun1) 363 assertNotified(rm.byRun[run2], expectedRun2) 364 So(tj.clsNotified.Set(), ShouldBeEmpty) 365 } 366 367 Convey("BeginBatch + FinalizeBatch", func() { 368 var resCLs []*CL 369 transErr := datastore.RunInTransaction(ctx, func(ctx context.Context) error { 370 resCLs = nil // reset in case of retries 371 muts, err := m.BeginBatch(ctx, lProject, clids) 372 So(err, ShouldBeNil) 373 eg, _ := errgroup.WithContext(ctx) 374 for i := range muts { 375 mut := muts[i] 376 eg.Go(func() error { 377 mut.CL.IncompleteRuns = append(mut.CL.IncompleteRuns, run3) 378 return nil 379 }) 380 } 381 So(eg.Wait(), ShouldBeNil) 382 resCLs, err = m.FinalizeBatch(ctx, muts) 383 return err 384 }, nil) 385 So(transErr, ShouldBeNil) 386 387 // Execute the expected BatchOnCLUpdatedTask. 388 So(ct.TQ.Tasks(), ShouldHaveLength, 1) 389 ct.TQ.Run(ctx, tqtesting.StopWhenDrained()) 390 391 verify(resCLs) 392 }) 393 394 Convey("Manual Adopt + FinalizeBatch", func() { 395 var resCLs []*CL 396 transErr := datastore.RunInTransaction(ctx, func(ctx context.Context) error { 397 resCLs = nil // reset in case of retries 398 muts := make([]*CLMutation, len(clids)) 399 eg, egCtx := errgroup.WithContext(ctx) 400 for i, id := range clids { 401 i, id := i, id 402 eg.Go(func() error { 403 cl := &CL{ID: id} 404 if err := datastore.Get(egCtx, cl); err != nil { 405 return err 406 } 407 muts[i] = m.Adopt(ctx, lProject, cl) 408 muts[i].CL.IncompleteRuns = append(muts[i].CL.IncompleteRuns, run3) 409 return nil 410 }) 411 } 412 So(eg.Wait(), ShouldBeNil) 413 var err error 414 resCLs, err = m.FinalizeBatch(ctx, muts) 415 return err 416 }, nil) 417 So(transErr, ShouldBeNil) 418 419 // Execute the expected BatchOnCLUpdatedTask. 420 So(ct.TQ.Tasks(), ShouldHaveLength, 1) 421 ct.TQ.Run(ctx, tqtesting.StopWhenDrained()) 422 423 verify(resCLs) 424 }) 425 426 Convey("BeginBatch + manual finalization", func() { 427 // This is inefficient and really shouldn't be done in production. 428 var resCLs []*CL 429 transErr := datastore.RunInTransaction(ctx, func(ctx context.Context) error { 430 resCLs = make([]*CL, len(clids)) // reset in case of retries 431 muts, err := m.BeginBatch(ctx, lProject, clids) 432 So(err, ShouldBeNil) 433 eg, egCtx := errgroup.WithContext(ctx) 434 for i, mut := range muts { 435 i, mut := i, mut 436 eg.Go(func() error { 437 mut.CL.IncompleteRuns = append(mut.CL.IncompleteRuns, run3) 438 var err error 439 resCLs[i], err = mut.Finalize(egCtx) 440 return err 441 }) 442 } 443 return eg.Wait() 444 }, nil) 445 So(transErr, ShouldBeNil) 446 447 tasks := ct.TQ.Tasks() 448 So(tasks, ShouldHaveLength, N) 449 for _, t := range tasks { 450 So(t.Class, ShouldResemble, BatchOnCLUpdatedTaskClass) 451 ct.TQ.Run(ctx, tqtesting.StopAfterTask(BatchOnCLUpdatedTaskClass)) 452 } 453 454 verify(resCLs) 455 }) 456 }) 457 }) 458 } 459 460 func TestMutatorConcurrent(t *testing.T) { 461 t.Parallel() 462 463 Convey("Mutator works on single CL when called concurrently with flaky datastore", t, func() { 464 ct := cvtesting.Test{} 465 ctx, cancel := ct.SetUp(t) 466 defer cancel() 467 // Truncate to seconds to reduce noise in diffs of proto timestamps. 468 // use Seconds with lots of 0s at the end for easy grasp of assertion 469 // failures since they are done on protos. 470 epoch := (×tamppb.Timestamp{Seconds: 14500000000}).AsTime() 471 ct.Clock.Set(epoch) 472 473 const lProject = "infra" 474 const gHost = "x-review.example.com" 475 const gChange = 44 476 eid := MustGobID(gHost, gChange) 477 478 pm := pmMock{} 479 rm := rmMock{} 480 tj := tjMock{} 481 m := NewMutator(ct.TQDispatcher, &pm, &rm, &tj) 482 483 ctx, fb := featureBreaker.FilterRDS(ctx, nil) 484 // Use a single random source for all flaky.Errors(...) instances. Otherwise 485 // they repeat the same random pattern each time withBrokenDS is called. 486 rnd := rand.NewSource(0) 487 488 // Make datastore very faulty. 489 fb.BreakFeaturesWithCallback( 490 flaky.Errors(flaky.Params{ 491 Rand: rnd, 492 DeadlineProbability: 0.4, 493 ConcurrentTransactionProbability: 0.4, 494 }), 495 featureBreaker.DatastoreFeatures..., 496 ) 497 // Number of tries per worker. 498 // With probabilities above, it typically takes <60 tries. 499 // 500 // This value was set to 300 before 2024-01-16 and it flaked once, so 501 // let's increase it to 30000. 502 const R = 30000 503 // Number of workers. 504 const N = 20 505 506 wg := sync.WaitGroup{} 507 wg.Add(N) 508 for d := 0; d < N; d++ { 509 // Simulate each worker trying to update Snapshot and DependentMeta to 510 // at least pre-determined timestamp. 511 // For extra coverage, use different timestamps for them. 512 513 // For a co-prime p,N: 514 // assert sorted(set([((p*d)%N) for d in xrange(N)])) == range(N) 515 // 47, 59 are actual primes. 516 snapTS := epoch.Add(time.Second * time.Duration((47*d)%N)) 517 accTS := epoch.Add(time.Second * time.Duration((73*d)%N)) 518 go func() { 519 defer wg.Done() 520 snap := makeSnapshot(lProject, snapTS) 521 acc := makeAccess(lProject, accTS) 522 var err error 523 for i := 0; i < R; i++ { 524 // Make this thing a little more robust against flakiness and sleep for a millisecond 525 // every so often. 526 if i%1000 == 0 { 527 time.Sleep(1 * time.Millisecond) 528 } 529 _, err = m.Upsert(ctx, lProject, eid, func(cl *CL) error { 530 ret := ErrStopMutation 531 if t := cl.Snapshot.GetExternalUpdateTime(); t == nil || t.AsTime().Before(snapTS) { 532 cl.Snapshot = snap 533 ret = nil 534 } 535 if t := cl.Access.GetByProject()[lProject].GetUpdateTime(); t == nil || t.AsTime().Before(accTS) { 536 cl.Access = acc 537 ret = nil 538 } 539 return ret 540 }) 541 if err == nil { 542 t.Logf("succeeded after %d tries", i) 543 return 544 } 545 } 546 panic(errors.Annotate(err, "all %d tries exhausted", R).Err()) 547 }() 548 } 549 wg.Wait() 550 551 // "Fix" datastore, letting us examine it. 552 fb.BreakFeaturesWithCallback( 553 func(context.Context, string) error { return nil }, 554 featureBreaker.DatastoreFeatures..., 555 ) 556 cl, err := eid.Load(ctx) 557 So(err, ShouldBeNil) 558 So(cl, ShouldNotBeNil) 559 // Since all workers have succeeded, the latest snapshot 560 // (by ExternalUpdateTime) must be the current snapshot in datastore. 561 latestTS := epoch.Add((N - 1) * time.Second) 562 So(cl.Snapshot.GetExternalUpdateTime().AsTime(), ShouldEqual, latestTS) 563 So(cl.Access.GetByProject()[lProject].GetUpdateTime().AsTime(), ShouldResemble, latestTS) 564 // Furthermore, there must have been at most N non-noop UpdateSnapshot calls 565 // (one per worker, iff they did it exactly in the increasing order of 566 // timestamps. 567 t.Logf("%d updates done", cl.EVersion) 568 So(cl.EVersion, ShouldBeLessThan, N+1) 569 }) 570 } 571 572 func makeAccess(luciProject string, updatedTime time.Time) *Access { 573 return &Access{ByProject: map[string]*Access_Project{ 574 luciProject: { 575 NoAccess: true, 576 NoAccessTime: timestamppb.New(updatedTime), 577 UpdateTime: timestamppb.New(updatedTime), 578 }, 579 }} 580 } 581 582 type pmMock struct { 583 m sync.Mutex 584 byProject map[string]map[common.CLID]int64 // latest max EVersion 585 } 586 587 func (p *pmMock) NotifyCLsUpdated(ctx context.Context, project string, events *CLUpdatedEvents) error { 588 p.m.Lock() 589 defer p.m.Unlock() 590 if p.byProject == nil { 591 p.byProject = make(map[string]map[common.CLID]int64, 1) 592 } 593 m := p.byProject[project] 594 if m == nil { 595 m = make(map[common.CLID]int64, len(events.GetEvents())) 596 p.byProject[project] = m 597 } 598 for _, e := range events.GetEvents() { 599 clid := common.CLID(e.GetClid()) 600 m[clid] = max(m[clid], e.GetEversion()) 601 } 602 return nil 603 } 604 605 type rmMock struct { 606 m sync.Mutex 607 byRun map[common.RunID]map[common.CLID]int64 // latest max EVersion 608 } 609 610 func (r *rmMock) NotifyCLsUpdated(ctx context.Context, rid common.RunID, events *CLUpdatedEvents) error { 611 r.m.Lock() 612 defer r.m.Unlock() 613 if r.byRun == nil { 614 r.byRun = make(map[common.RunID]map[common.CLID]int64, 1) 615 } 616 m := r.byRun[rid] 617 if m == nil { 618 m = make(map[common.CLID]int64, 1) 619 r.byRun[rid] = m 620 } 621 for _, e := range events.GetEvents() { 622 clid := common.CLID(e.GetClid()) 623 m[clid] = max(m[clid], e.GetEversion()) 624 } 625 return nil 626 } 627 628 type tjMock struct { 629 clsNotified common.CLIDs 630 mutex sync.Mutex 631 } 632 633 func (t *tjMock) ScheduleCancelStale(ctx context.Context, clid common.CLID, prevMinEquivalentPatchset, currentMinEquivalentPatchset int32, eta time.Time) error { 634 t.mutex.Lock() 635 defer t.mutex.Unlock() 636 t.clsNotified = append(t.clsNotified, clid) 637 return nil 638 }