go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/common/eventbox/box_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 eventbox 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "testing" 22 23 "go.chromium.org/luci/common/logging" 24 "go.chromium.org/luci/common/retry/transient" 25 "go.chromium.org/luci/gae/service/datastore" 26 27 "go.chromium.org/luci/cv/internal/common" 28 "go.chromium.org/luci/cv/internal/cvtesting" 29 30 . "github.com/smartystreets/goconvey/convey" 31 . "go.chromium.org/luci/common/testing/assertions" 32 ) 33 34 // processor simulates a variant of game of life on one cell in an array of 35 // cells. 36 type processor struct { 37 index int 38 } 39 40 type cell struct { 41 Index int `gae:"$id"` 42 EVersion EVersion `gae:",noindex"` 43 Population int `gae:",noindex"` 44 } 45 46 func (p *processor) LoadState(ctx context.Context) (State, EVersion, error) { 47 c, err := get(ctx, p.index) 48 if err != nil { 49 return nil, 0, err 50 } 51 return State(&c.Population), c.EVersion, nil 52 } 53 54 func (p *processor) FetchEVersion(ctx context.Context) (EVersion, error) { 55 c, err := get(ctx, p.index) 56 if err != nil { 57 return 0, err 58 } 59 return c.EVersion, nil 60 } 61 62 func (p *processor) SaveState(ctx context.Context, s State, e EVersion) error { 63 c := cell{Index: p.index, EVersion: e, Population: *(s.(*int))} 64 return transient.Tag.Apply(datastore.Put(ctx, &c)) 65 } 66 67 func (p *processor) PrepareMutation(ctx context.Context, events Events, s State) (ts []Transition, _ Events, err error) { 68 ctx = logging.SetField(ctx, "index", p.index) 69 // Simulate variation of game of life. 70 population := s.(*int) 71 add := func(delta int) *int { 72 n := new(int) 73 *n = delta + (*population) 74 return n 75 } 76 77 if len(events) == 0 { 78 switch { 79 case *population == 0: 80 ts = append(ts, Transition{ 81 SideEffectFn: func(ctx context.Context) error { 82 logging.Debugf(ctx, "advertised to %d to migrate", p.index+1) 83 return Emit(ctx, []byte{'-'}, mkRecipient(ctx, p.index+1)) 84 }, 85 Events: nil, // Don't consume any events. 86 TransitionTo: population, // Same state. 87 }) 88 case *population < 3: 89 population = add(+3) 90 logging.Debugf(ctx, "growing +3=> %d", *population) 91 ts = append(ts, Transition{ 92 SideEffectFn: nil, 93 Events: nil, // Don't consume any events. 94 TransitionTo: population, 95 }) 96 } 97 return 98 } 99 100 // Triage events. 101 var minus, plus Events 102 for _, e := range events { 103 if e.Value[0] == '-' { 104 minus = append(minus, e) 105 } else { 106 plus = append(plus, e) 107 } 108 } 109 110 if len(plus) > 0 { 111 // Accept at most 1 at a time. 112 population = add(1) 113 logging.Debugf(ctx, "welcoming +1 out of %d => %d", len(plus), *population) 114 ts = append(ts, Transition{ 115 SideEffectFn: nil, 116 Events: plus[:1], // Consume only 1 event. 117 TransitionTo: population, 118 }) 119 } 120 if len(minus) > 0 { 121 t := Transition{ 122 Events: minus, // Always consume all advertisements to emigrate. 123 } 124 if *population <= 1 { 125 logging.Debugf(ctx, "consuming %d ads", len(minus)) 126 } else { 127 population = add(-1) 128 t.SideEffectFn = func(ctx context.Context) error { 129 logging.Debugf(ctx, "emigrated to %d", p.index-1) 130 return Emit(ctx, []byte{'+'}, mkRecipient(ctx, p.index-1)) 131 } 132 } 133 t.TransitionTo = population 134 ts = append(ts, t) 135 } 136 return 137 } 138 139 func mkRecipient(ctx context.Context, id int) Recipient { 140 return Recipient{ 141 Key: datastore.MakeKey(ctx, "cell", id), 142 MonitoringString: fmt.Sprintf("cell-%d", id), 143 } 144 } 145 146 func TestEventboxWorks(t *testing.T) { 147 t.Parallel() 148 149 Convey("eventbox works", t, func() { 150 ct := cvtesting.Test{} 151 ctx, cancel := ct.SetUp(t) 152 defer cancel() 153 154 const limit = 10000 155 156 // Seed the first cell. 157 So(Emit(ctx, []byte{'+'}, mkRecipient(ctx, 65)), ShouldBeNil) 158 l, err := List(ctx, mkRecipient(ctx, 65)) 159 So(err, ShouldBeNil) 160 So(l, ShouldHaveLength, 1) 161 162 ppfns, err := ProcessBatch(ctx, mkRecipient(ctx, 65), &processor{65}, limit) 163 So(err, ShouldBeNil) 164 So(ppfns, ShouldBeEmpty) 165 So(mustGet(ctx, 65).EVersion, ShouldEqual, 1) 166 So(mustGet(ctx, 65).Population, ShouldEqual, 1) 167 So(mustList(ctx, 65), ShouldHaveLength, 0) 168 169 // Let the cell grow without incoming events. 170 ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 65), &processor{65}, limit) 171 So(err, ShouldBeNil) 172 So(ppfns, ShouldBeEmpty) 173 So(mustGet(ctx, 65).EVersion, ShouldEqual, 2) 174 So(mustGet(ctx, 65).Population, ShouldEqual, 1+3) 175 // Can't grow any more, no change to anything. 176 ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 65), &processor{65}, limit) 177 So(err, ShouldBeNil) 178 So(ppfns, ShouldBeEmpty) 179 So(mustGet(ctx, 65).EVersion, ShouldEqual, 2) 180 So(mustGet(ctx, 65).Population, ShouldEqual, 1+3) 181 182 // Advertise from nearby cell, twice. 183 ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 64), &processor{64}, limit) 184 So(err, ShouldBeNil) 185 So(ppfns, ShouldBeEmpty) 186 ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 64), &processor{64}, limit) 187 So(err, ShouldBeNil) 188 So(ppfns, ShouldBeEmpty) 189 So(mustList(ctx, 65), ShouldHaveLength, 2) 190 // Emigrate, at most once. 191 ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 65), &processor{65}, limit) 192 So(err, ShouldBeNil) 193 So(ppfns, ShouldBeEmpty) 194 So(mustGet(ctx, 65).EVersion, ShouldEqual, 3) 195 So(mustGet(ctx, 65).Population, ShouldEqual, 4-1) 196 So(mustList(ctx, 65), ShouldHaveLength, 0) 197 198 // Accept immigrants. 199 ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 64), &processor{64}, limit) 200 So(err, ShouldBeNil) 201 So(ppfns, ShouldBeEmpty) 202 So(mustGet(ctx, 64).Population, ShouldEqual, +1) 203 204 // Advertise to a cell with population = 1 is a noop. 205 ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 63), &processor{63}, limit) 206 So(err, ShouldBeNil) 207 So(ppfns, ShouldBeEmpty) 208 ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 64), &processor{64}, limit) 209 So(err, ShouldBeNil) 210 So(ppfns, ShouldBeEmpty) 211 212 // Lots of events at once. 213 So(Emit(ctx, []byte{'+'}, mkRecipient(ctx, 49)), ShouldBeNil) 214 So(Emit(ctx, []byte{'+'}, mkRecipient(ctx, 49)), ShouldBeNil) // will have to wait 215 So(Emit(ctx, []byte{'+'}, mkRecipient(ctx, 49)), ShouldBeNil) // will have to wait 216 So(Emit(ctx, []byte{'-'}, mkRecipient(ctx, 49)), ShouldBeNil) // not enough people, ignored. 217 So(Emit(ctx, []byte{'-'}, mkRecipient(ctx, 49)), ShouldBeNil) // not enough people, ignored. 218 So(mustList(ctx, 49), ShouldHaveLength, 5) 219 ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 49), &processor{49}, limit) 220 So(err, ShouldBeNil) 221 So(ppfns, ShouldBeEmpty) 222 So(mustGet(ctx, 49).EVersion, ShouldEqual, 1) 223 So(mustGet(ctx, 49).Population, ShouldEqual, 1) 224 So(mustList(ctx, 49), ShouldHaveLength, 2) // 2x'+' are waiting 225 // Slowly welcome remaining newcomers. 226 ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 49), &processor{49}, limit) 227 So(err, ShouldBeNil) 228 So(ppfns, ShouldBeEmpty) 229 So(mustGet(ctx, 49).Population, ShouldEqual, 2) 230 ppfns, err = ProcessBatch(ctx, mkRecipient(ctx, 49), &processor{49}, limit) 231 So(err, ShouldBeNil) 232 So(ppfns, ShouldBeEmpty) 233 So(mustGet(ctx, 49).Population, ShouldEqual, 3) 234 // Finally, must be done. 235 So(mustList(ctx, 49), ShouldHaveLength, 0) 236 }) 237 } 238 239 func get(ctx context.Context, index int) (*cell, error) { 240 c := &cell{Index: index} 241 switch err := datastore.Get(ctx, c); { 242 case err == datastore.ErrNoSuchEntity || err == nil: 243 return c, nil 244 default: 245 return nil, transient.Tag.Apply(err) 246 } 247 } 248 249 func mustGet(ctx context.Context, index int) *cell { 250 c, err := get(ctx, index) 251 So(err, ShouldBeNil) 252 return c 253 } 254 255 func mustList(ctx context.Context, index int) Events { 256 l, err := List(ctx, mkRecipient(ctx, index)) 257 So(err, ShouldBeNil) 258 return l 259 } 260 261 func TestEventboxPostProcessFn(t *testing.T) { 262 t.Parallel() 263 264 Convey("eventbox", t, func() { 265 ct := cvtesting.Test{} 266 ctx, cancel := ct.SetUp(t) 267 defer cancel() 268 269 const limit = 10000 270 recipient := mkRecipient(ctx, 753) 271 272 initState := int(149) 273 p := &mockProc{ 274 loadState: func(context.Context) (State, EVersion, error) { 275 return State(&initState), EVersion(0), nil 276 }, 277 fetchEVersion: func(context.Context) (EVersion, error) { 278 return 0, nil 279 }, 280 saveState: func(context.Context, State, EVersion) error { 281 return nil 282 }, 283 } 284 Convey("Returns PostProcessFns for successful state transitions", func() { 285 var calledForStates []int 286 p.prepareMutation = func(ctx context.Context, es Events, s State) ([]Transition, Events, error) { 287 gotState := *(s.(*int)) 288 return []Transition{ 289 { 290 TransitionTo: gotState + 1, 291 PostProcessFn: func(ctx context.Context) error { 292 calledForStates = append(calledForStates, gotState+1) 293 return nil 294 }, 295 }, 296 { 297 TransitionTo: gotState + 2, 298 }, 299 { 300 TransitionTo: gotState + 3, 301 PostProcessFn: func(ctx context.Context) error { 302 calledForStates = append(calledForStates, gotState+3) 303 return nil 304 }, 305 }, 306 }, nil, nil 307 } 308 ppfns, err := ProcessBatch(ctx, recipient, p, limit) 309 So(err, ShouldBeNil) 310 So(ppfns, ShouldHaveLength, 2) 311 for _, ppfn := range ppfns { 312 So(ppfn(ctx), ShouldBeNil) 313 } 314 So(calledForStates, ShouldResemble, []int{150, 152}) 315 }) 316 }) 317 } 318 319 func TestEventboxFails(t *testing.T) { 320 t.Parallel() 321 322 Convey("eventbox fails as intended in failure cases", t, func() { 323 ct := cvtesting.Test{} 324 ctx, cancel := ct.SetUp(t) 325 defer cancel() 326 327 const limit = 100000 328 recipient := mkRecipient(ctx, 77) 329 330 So(Emit(ctx, []byte{'+'}, recipient), ShouldBeNil) 331 So(Emit(ctx, []byte{'-'}, recipient), ShouldBeNil) 332 333 initState := int(99) 334 p := &mockProc{ 335 loadState: func(_ context.Context) (State, EVersion, error) { 336 return State(&initState), EVersion(0), nil 337 }, 338 // since 3 other funcs are nil, calling their Upper-case counterparts will 339 // panic (see mockProc implementation below). 340 } 341 Convey("Mutate() failure aborts", func() { 342 p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) { 343 return nil, nil, errors.New("oops") 344 } 345 ppfns, err := ProcessBatch(ctx, recipient, p, limit) 346 So(err, ShouldErrLike, "oops") 347 So(ppfns, ShouldBeEmpty) 348 }) 349 350 firstSideEffectCalled := false 351 const firstIndex = 88 352 secondState := initState + 1 353 var second SideEffectFn 354 p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) { 355 return []Transition{ 356 { 357 SideEffectFn: func(ctx context.Context) error { 358 firstSideEffectCalled = true 359 return datastore.Put(ctx, &cell{Index: firstIndex}) 360 }, 361 Events: es[:1], 362 TransitionTo: s, 363 }, 364 { 365 SideEffectFn: second, 366 Events: es[1:], 367 TransitionTo: State(&secondState), 368 }, 369 }, nil, nil 370 } 371 372 Convey("Eversion must be checked", func() { 373 p.fetchEVersion = func(_ context.Context) (EVersion, error) { 374 return 0, errors.New("ev error") 375 } 376 ppfns, err := ProcessBatch(ctx, recipient, p, limit) 377 So(err, ShouldErrLike, "ev error") 378 So(ppfns, ShouldBeEmpty) 379 p.fetchEVersion = func(_ context.Context) (EVersion, error) { 380 return 1, nil 381 } 382 ppfns, err = ProcessBatch(ctx, recipient, p, limit) 383 So(common.DSContentionTag.In(err), ShouldBeTrue) 384 So(ppfns, ShouldBeEmpty) 385 So(firstSideEffectCalled, ShouldBeFalse) 386 }) 387 388 p.fetchEVersion = func(_ context.Context) (EVersion, error) { 389 return 0, nil 390 } 391 392 Convey("No call to save if any Transition fails", func() { 393 second = func(_ context.Context) error { 394 return transient.Tag.Apply(errors.New("2nd failed")) 395 } 396 ppfns, err := ProcessBatch(ctx, recipient, p, limit) 397 So(err, ShouldErrLike, "2nd failed") 398 So(ppfns, ShouldBeEmpty) 399 So(firstSideEffectCalled, ShouldBeTrue) 400 // ... but w/o any effect since transaction should have been aborted 401 So(datastore.Get(ctx, &cell{Index: firstIndex}), 402 ShouldEqual, datastore.ErrNoSuchEntity) 403 }) 404 405 second = func(_ context.Context) error { return nil } 406 Convey("Failed Save aborts any side effects, too", func() { 407 p.saveState = func(ctx context.Context, st State, ev EVersion) error { 408 s := *(st.(*int)) 409 So(ev, ShouldEqual, 1) 410 So(s, ShouldNotEqual, initState) 411 So(s, ShouldEqual, secondState) 412 return transient.Tag.Apply(errors.New("savvvvvvvvvvvvvvvvvvvvvvvvvv hung")) 413 } 414 ppfns, err := ProcessBatch(ctx, recipient, p, limit) 415 So(err, ShouldErrLike, "savvvvvvvvvvvvvvvv") 416 So(ppfns, ShouldBeEmpty) 417 // ... still no side effect. 418 So(datastore.Get(ctx, &cell{Index: firstIndex}), 419 ShouldEqual, datastore.ErrNoSuchEntity) 420 }) 421 422 // In all cases, there must still be 2 unconsumed events. 423 l, err := List(ctx, recipient) 424 So(err, ShouldBeNil) 425 So(l, ShouldHaveLength, 2) 426 427 // Finally, check that first side effect is real, otherwise assertions above 428 // might be giving false sense of correctness. 429 p.saveState = func(context.Context, State, EVersion) error { return nil } 430 ppfns, err := ProcessBatch(ctx, recipient, p, limit) 431 So(err, ShouldBeNil) 432 So(ppfns, ShouldBeEmpty) 433 So(mustGet(ctx, firstIndex), ShouldNotBeNil) 434 }) 435 } 436 437 func TestEventboxNoopTransitions(t *testing.T) { 438 t.Parallel() 439 440 Convey("Noop Transitions are detected", t, func() { 441 t := Transition{} 442 So(t.isNoop(nil), ShouldBeTrue) 443 initState := int(99) 444 t.TransitionTo = initState 445 So(t.isNoop(nil), ShouldBeFalse) 446 So(t.isNoop(initState), ShouldBeTrue) 447 t.Events = Events{Event{}} 448 So(t.isNoop(initState), ShouldBeFalse) 449 t.Events = nil 450 t.SideEffectFn = func(context.Context) error { return nil } 451 So(t.isNoop(initState), ShouldBeFalse) 452 t.SideEffectFn = nil 453 t.PostProcessFn = func(context.Context) error { return nil } 454 So(t.isNoop(initState), ShouldBeFalse) 455 }) 456 457 Convey("eventbox doesn't transact on nil transitions", t, func() { 458 ct := cvtesting.Test{} 459 ctx, cancel := ct.SetUp(t) 460 defer cancel() 461 462 const limit = 100000 463 recipient := mkRecipient(ctx, 77) 464 initState := int(99) 465 panicErr := errors.New("must not be transact!") 466 467 p := &mockProc{ 468 loadState: func(_ context.Context) (State, EVersion, error) { 469 return State(&initState), EVersion(0), nil 470 }, 471 fetchEVersion: func(_ context.Context) (EVersion, error) { 472 panic(panicErr) 473 }, 474 } 475 476 Convey("Mutate returns no transitions", func() { 477 p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) { 478 return nil, nil, nil 479 } 480 ppfns, err := ProcessBatch(ctx, recipient, p, limit) 481 So(err, ShouldBeNil) 482 So(ppfns, ShouldBeEmpty) 483 }) 484 Convey("Mutate returns no transitions, but some semantic garbage is still cleaned up", func() { 485 So(Emit(ctx, []byte("msg"), recipient), ShouldBeNil) 486 So(Emit(ctx, []byte("msg"), recipient), ShouldBeNil) 487 p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) { 488 return nil, es[:1], nil 489 } 490 ppfns, err := ProcessBatch(ctx, recipient, p, limit) 491 So(err, ShouldBeNil) 492 So(ppfns, ShouldBeEmpty) 493 l, err := List(ctx, recipient) 494 So(err, ShouldBeNil) 495 So(l, ShouldHaveLength, 1) 496 So(l[0].Value, ShouldResemble, []byte("msg")) 497 }) 498 Convey("Garbage is cleaned up even if Mutate also returns error", func() { 499 So(Emit(ctx, []byte("msg"), recipient), ShouldBeNil) 500 So(Emit(ctx, []byte("msg"), recipient), ShouldBeNil) 501 p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) { 502 return nil, es[:1], errors.New("boom") 503 } 504 _, err := ProcessBatch(ctx, recipient, p, limit) 505 So(err, ShouldErrLike, "boom") 506 l, err := List(ctx, recipient) 507 So(err, ShouldBeNil) 508 So(l, ShouldHaveLength, 1) 509 So(l[0].Value, ShouldResemble, []byte("msg")) 510 }) 511 Convey("Mutate returns empty slice of transitions", func() { 512 p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) { 513 return []Transition{}, nil, nil 514 } 515 ppfns, err := ProcessBatch(ctx, recipient, p, limit) 516 So(err, ShouldBeNil) 517 So(ppfns, ShouldBeEmpty) 518 }) 519 Convey("Mutate returns noop transitions only", func() { 520 p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) { 521 return []Transition{ 522 {TransitionTo: s}, 523 }, nil, nil 524 } 525 ppfns, err := ProcessBatch(ctx, recipient, p, limit) 526 So(err, ShouldBeNil) 527 So(ppfns, ShouldBeEmpty) 528 }) 529 530 Convey("Test's own sanity check that fetchEVersion is called and panics", func() { 531 p.prepareMutation = func(_ context.Context, es Events, s State) ([]Transition, Events, error) { 532 return []Transition{ 533 {TransitionTo: new(int)}, 534 }, nil, nil 535 } 536 So(func() { ProcessBatch(ctx, recipient, p, limit) }, ShouldPanicLike, panicErr) 537 }) 538 }) 539 } 540 541 type mockProc struct { 542 loadState func(_ context.Context) (State, EVersion, error) 543 prepareMutation func(_ context.Context, _ Events, _ State) ([]Transition, Events, error) 544 fetchEVersion func(_ context.Context) (EVersion, error) 545 saveState func(_ context.Context, _ State, _ EVersion) error 546 } 547 548 func (m *mockProc) LoadState(ctx context.Context) (State, EVersion, error) { 549 return m.loadState(ctx) 550 } 551 func (m *mockProc) PrepareMutation(ctx context.Context, e Events, s State) ([]Transition, Events, error) { 552 return m.prepareMutation(ctx, e, s) 553 } 554 func (m *mockProc) FetchEVersion(ctx context.Context) (EVersion, error) { 555 return m.fetchEVersion(ctx) 556 } 557 func (m *mockProc) SaveState(ctx context.Context, s State, e EVersion) error { 558 return m.saveState(ctx, s, e) 559 } 560 561 func PrepareMutation(t *testing.T) { 562 t.Parallel() 563 564 Convey("Chain of SideEffectFn works", t, func() { 565 ctx := context.Background() 566 var ops []string 567 f1 := func(context.Context) error { 568 ops = append(ops, "f1") 569 return nil 570 } 571 f2 := func(context.Context) error { 572 ops = append(ops, "f2") 573 return nil 574 } 575 breakChain := errors.New("break") 576 ferr := func(context.Context) error { 577 ops = append(ops, "ferr") 578 return breakChain 579 } 580 Convey("all nils chain to nil", func() { 581 So(Chain(), ShouldBeNil) 582 So(Chain(nil), ShouldBeNil) 583 So(Chain(nil, nil), ShouldBeNil) 584 }) 585 Convey("order is respected", func() { 586 So(Chain(nil, f2, nil, f1, f2, f1, nil)(ctx), ShouldBeNil) 587 So(ops, ShouldResemble, []string{"f2", "f1", "f2", "f1"}) 588 }) 589 Convey("error aborts", func() { 590 So(Chain(f1, nil, ferr, f2)(ctx), ShouldErrLike, breakChain) 591 So(ops, ShouldResemble, []string{"f1", "ferr"}) 592 }) 593 }) 594 }