go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/butler/butler_test.go (about) 1 // Copyright 2015 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 butler 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "io" 22 "sync" 23 "testing" 24 "time" 25 26 "google.golang.org/protobuf/types/known/timestamppb" 27 28 "go.chromium.org/luci/common/clock/testclock" 29 "go.chromium.org/luci/common/logging" 30 "go.chromium.org/luci/common/logging/gologger" 31 . "go.chromium.org/luci/common/testing/assertions" 32 "go.chromium.org/luci/logdog/api/logpb" 33 "go.chromium.org/luci/logdog/client/butler/bootstrap" 34 "go.chromium.org/luci/logdog/client/butler/output" 35 "go.chromium.org/luci/logdog/client/butler/output/null" 36 "go.chromium.org/luci/logdog/client/butlerlib/streamproto" 37 "go.chromium.org/luci/logdog/common/types" 38 39 . "github.com/smartystreets/goconvey/convey" 40 ) 41 42 type testOutput struct { 43 sync.Mutex 44 45 err error 46 maxSize int 47 streams map[string][]*logpb.LogEntry 48 terminal map[string]struct{} 49 50 closed bool 51 } 52 53 func (to *testOutput) SendBundle(b *logpb.ButlerLogBundle) error { 54 if to.err != nil { 55 return to.err 56 } 57 58 to.Lock() 59 defer to.Unlock() 60 61 if to.streams == nil { 62 to.streams = map[string][]*logpb.LogEntry{} 63 to.terminal = map[string]struct{}{} 64 } 65 for _, be := range b.Entries { 66 name := string(be.Desc.Name) 67 68 to.streams[name] = append(to.streams[name], be.Logs...) 69 if be.TerminalIndex >= 0 { 70 to.terminal[name] = struct{}{} 71 } 72 } 73 return nil 74 } 75 76 func (to *testOutput) MaxSendBundles() int { return 1 } 77 78 func (to *testOutput) MaxSize() int { 79 return to.maxSize 80 } 81 82 func (to *testOutput) Stats() output.Stats { 83 return &output.StatsBase{} 84 } 85 86 func (to *testOutput) URLConstructionEnv() bootstrap.Environment { 87 return bootstrap.Environment{} 88 } 89 90 func (to *testOutput) Close() { 91 if to.closed { 92 panic("double close") 93 } 94 to.closed = true 95 } 96 97 func (to *testOutput) logs(name string) []*logpb.LogEntry { 98 to.Lock() 99 defer to.Unlock() 100 101 return to.streams[name] 102 } 103 104 func (to *testOutput) isTerminal(name string) bool { 105 to.Lock() 106 defer to.Unlock() 107 108 _, isTerminal := to.terminal[name] 109 return isTerminal 110 } 111 112 func shouldHaveTextLogs(actual any, expected ...any) string { 113 exp := make([]string, len(expected)) 114 for i, e := range expected { 115 exp[i] = e.(string) 116 } 117 118 var lines []string 119 var prev string 120 for _, le := range actual.([]*logpb.LogEntry) { 121 if le.GetText() == nil { 122 return fmt.Sprintf("non-text entry: %T %#v", le, le) 123 } 124 125 for _, l := range le.GetText().Lines { 126 prev += string(l.Value) 127 if l.Delimiter != "" { 128 lines = append(lines, prev) 129 prev = "" 130 } 131 } 132 } 133 134 // Add any trailing (non-delimited) data. 135 if prev != "" { 136 lines = append(lines, prev) 137 } 138 139 return ShouldResemble(lines, exp) 140 } 141 142 type testStreamData struct { 143 data []byte 144 err error 145 } 146 147 type testStream struct { 148 inC chan *testStreamData 149 closedC chan struct{} 150 151 allowDoubleClose bool 152 153 desc *logpb.LogStreamDescriptor 154 } 155 156 func (ts *testStream) data(d []byte, err error) { 157 ts.inC <- &testStreamData{ 158 data: d, 159 err: err, 160 } 161 if err == io.EOF { 162 // If EOF is hit, continue reporting EOF. 163 close(ts.inC) 164 } 165 } 166 167 func (ts *testStream) err(err error) { 168 ts.data(nil, err) 169 } 170 171 func (ts *testStream) isClosed() bool { 172 select { 173 case <-ts.closedC: 174 return true 175 default: 176 return false 177 } 178 } 179 180 func (ts *testStream) Close() error { 181 if ts.isClosed() { 182 if ts.allowDoubleClose { 183 return nil 184 } 185 panic("double close") 186 } 187 188 close(ts.closedC) 189 return nil 190 } 191 192 func (ts *testStream) Read(b []byte) (int, error) { 193 select { 194 case <-ts.closedC: 195 return 0, io.EOF 196 197 case d, ok := <-ts.inC: 198 // We have data on "inC", but we want closed status to trump this. 199 if !ok || ts.isClosed() { 200 return 0, io.EOF 201 } 202 return copy(b, d.data), d.err 203 } 204 } 205 206 type testStreamServer struct { 207 err error 208 onNext func() 209 streamC chan *testStream 210 } 211 212 func newTestStreamServer() *testStreamServer { 213 return &testStreamServer{ 214 streamC: make(chan *testStream), 215 } 216 } 217 218 func (tss *testStreamServer) Address() string { return "test" } 219 220 func (tss *testStreamServer) Listen() error { return tss.err } 221 222 func (tss *testStreamServer) Next() (io.ReadCloser, *logpb.LogStreamDescriptor) { 223 if tss.onNext != nil { 224 tss.onNext() 225 } 226 227 ts, ok := <-tss.streamC 228 if !ok { 229 return nil, nil 230 } 231 return ts, ts.desc 232 } 233 234 func (tss *testStreamServer) Close() { 235 close(tss.streamC) 236 } 237 238 func (tss *testStreamServer) enqueue(ts *testStream) { 239 tss.streamC <- ts 240 } 241 242 func TestConfig(t *testing.T) { 243 t.Parallel() 244 245 Convey(`A Config instance`, t, func() { 246 to := testOutput{ 247 maxSize: 1024, 248 } 249 conf := Config{ 250 Output: &to, 251 } 252 253 Convey(`Will validate.`, func() { 254 So(conf.Validate(), ShouldBeNil) 255 }) 256 257 Convey(`Will not validate with a nil Output.`, func() { 258 conf.Output = nil 259 So(conf.Validate(), ShouldErrLike, "an Output must be supplied") 260 }) 261 }) 262 } 263 264 // mkb is shorthand for "make Butler". It calls "new" and panics if there is 265 // an error. 266 func mkb(c context.Context, config Config) *Butler { 267 b, err := New(c, config) 268 if err != nil { 269 panic(err) 270 } 271 return b 272 } 273 274 func TestButler(t *testing.T) { 275 t.Parallel() 276 277 Convey(`A testing Butler instance`, t, func() { 278 c, _ := testclock.UseTime(context.Background(), testclock.TestTimeUTC) 279 to := testOutput{ 280 maxSize: 1024, 281 } 282 283 conf := Config{ 284 Output: &to, 285 BufferLogs: false, 286 } 287 288 Convey(`Will error if an invalid Config is passed.`, func() { 289 conf.Output = nil 290 So(conf.Validate(), ShouldNotBeNil) 291 292 _, err := New(c, conf) 293 So(err, ShouldNotBeNil) 294 }) 295 296 Convey(`Will Run until Activated, then shut down.`, func() { 297 conf.BufferLogs = true // (Coverage) 298 299 b := mkb(c, conf) 300 b.Activate() 301 So(b.Wait(), ShouldBeNil) 302 }) 303 304 Convey(`Will forward a Shutdown error.`, func() { 305 conf.BufferLogs = true // (Coverage) 306 307 b := mkb(c, conf) 308 b.shutdown(errors.New("shutdown error")) 309 So(b.Wait(), ShouldErrLike, "shutdown error") 310 }) 311 312 Convey(`Will retain the first error and ignore duplicate shutdowns.`, func() { 313 b := mkb(c, conf) 314 b.shutdown(errors.New("first error")) 315 b.shutdown(errors.New("second error")) 316 So(b.Wait(), ShouldErrLike, "first error") 317 }) 318 319 Convey(`Using a generic stream Properties`, func() { 320 newTestStream := func(setup func(d *logpb.LogStreamDescriptor)) *testStream { 321 desc := &logpb.LogStreamDescriptor{ 322 Name: "test", 323 StreamType: logpb.StreamType_TEXT, 324 ContentType: string(types.ContentTypeText), 325 } 326 if setup != nil { 327 setup(desc) 328 } 329 330 return &testStream{ 331 inC: make(chan *testStreamData, 16), 332 closedC: make(chan struct{}), 333 desc: desc, 334 } 335 } 336 337 Convey(`Will not add a stream with an invalid configuration.`, func() { 338 // No content type. 339 s := newTestStream(func(d *logpb.LogStreamDescriptor) { 340 d.ContentType = "" 341 }) 342 b := mkb(c, conf) 343 So(b.AddStream(s, s.desc), ShouldNotBeNil) 344 }) 345 346 Convey(`Will not add a stream with a duplicate stream name.`, func() { 347 b := mkb(c, conf) 348 349 s0 := newTestStream(nil) 350 So(b.AddStream(s0, s0.desc), ShouldBeNil) 351 352 s1 := newTestStream(nil) 353 So(b.AddStream(s1, s1.desc), ShouldErrLike, "duplicate registration") 354 }) 355 356 Convey(`Can apply global tags.`, func() { 357 conf.GlobalTags = streamproto.TagMap{ 358 "foo": "bar", 359 "baz": "qux", 360 } 361 desc := &logpb.LogStreamDescriptor{ 362 Name: "stdout", 363 ContentType: "test/data", 364 Timestamp: timestamppb.New(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)), 365 } 366 367 closeStreams := make(chan *testStreamData) 368 369 // newTestStream returns a stream that hangs in Read() until 370 // 'closeStreams' is closed. We do this to ensure Bundler doesn't 371 // "drain" and unregister the stream before GetStreamDescs() calls 372 // below have a chance to notice it exists. 373 newTestStream := func() io.ReadCloser { 374 return &testStream{ 375 closedC: make(chan struct{}), 376 inC: closeStreams, 377 } 378 } 379 380 b := mkb(c, conf) 381 defer func() { 382 close(closeStreams) 383 b.Activate() 384 b.Wait() 385 }() 386 387 Convey(`Applies global tags, but allows the stream to override.`, func() { 388 desc.Tags = map[string]string{ 389 "baz": "override", 390 } 391 392 So(b.AddStream(newTestStream(), desc), ShouldBeNil) 393 So(b.bundler.GetStreamDescs()["stdout"], ShouldResembleProto, &logpb.LogStreamDescriptor{ 394 Name: "stdout", 395 ContentType: "test/data", 396 Timestamp: desc.Timestamp, 397 Tags: map[string]string{ 398 "foo": "bar", 399 "baz": "override", 400 }, 401 }) 402 }) 403 404 Convey(`Will apply global tags if the stream has none (nil).`, func() { 405 So(b.AddStream(newTestStream(), desc), ShouldBeNil) 406 So(b.bundler.GetStreamDescs()["stdout"], ShouldResembleProto, &logpb.LogStreamDescriptor{ 407 Name: "stdout", 408 ContentType: "test/data", 409 Timestamp: desc.Timestamp, 410 Tags: map[string]string{ 411 "foo": "bar", 412 "baz": "qux", 413 }, 414 }) 415 }) 416 }) 417 418 Convey(`Run with 256 streams, stream{0..256} will deplete and finish.`, func() { 419 b := mkb(c, conf) 420 streams := make([]*testStream, 256) 421 for i := range streams { 422 streams[i] = newTestStream(func(d *logpb.LogStreamDescriptor) { 423 d.Name = fmt.Sprintf("stream%d", i) 424 }) 425 } 426 427 for _, s := range streams { 428 So(b.AddStream(s, s.desc), ShouldBeNil) 429 s.data([]byte("stream data 0!\n"), nil) 430 s.data([]byte("stream data 1!\n"), nil) 431 } 432 433 // Add data to the streams after shutdown. 434 for _, s := range streams { 435 s.data([]byte("stream data 2!\n"), io.EOF) 436 } 437 438 b.Activate() 439 So(b.Wait(), ShouldBeNil) 440 441 for _, s := range streams { 442 name := string(s.desc.Name) 443 444 So(to.logs(name), shouldHaveTextLogs, "stream data 0!", "stream data 1!", "stream data 2!") 445 So(to.isTerminal(name), ShouldBeTrue) 446 } 447 }) 448 449 Convey(`Shutdown with 256 in-progress streams, stream{0..256} will terminate if they emitted logs.`, func() { 450 b := mkb(c, conf) 451 streams := make([]*testStream, 256) 452 for i := range streams { 453 streams[i] = newTestStream(func(d *logpb.LogStreamDescriptor) { 454 d.Name = fmt.Sprintf("stream%d", i) 455 }) 456 } 457 458 for _, s := range streams { 459 So(b.AddStream(s, s.desc), ShouldBeNil) 460 s.data([]byte("stream data!\n"), nil) 461 } 462 463 b.shutdown(errors.New("test shutdown")) 464 So(b.Wait(), ShouldErrLike, "test shutdown") 465 466 for _, s := range streams { 467 if len(to.logs(s.desc.Name)) > 0 { 468 So(to.isTerminal(string(s.desc.Name)), ShouldBeTrue) 469 } else { 470 So(to.isTerminal(string(s.desc.Name)), ShouldBeFalse) 471 } 472 } 473 }) 474 475 Convey(`Using ten test stream servers`, func() { 476 servers := make([]*testStreamServer, 10) 477 for i := range servers { 478 servers[i] = newTestStreamServer() 479 } 480 streams := []*testStream(nil) 481 482 Convey(`Can register both before Run and will retain streams.`, func() { 483 b := mkb(c, conf) 484 for i, tss := range servers { 485 b.AddStreamServer(tss) 486 487 s := newTestStream(func(d *logpb.LogStreamDescriptor) { 488 d.Name = fmt.Sprintf("stream%d", i) 489 }) 490 streams = append(streams, s) 491 s.data([]byte("test data"), io.EOF) 492 tss.enqueue(s) 493 } 494 495 b.Activate() 496 So(b.Wait(), ShouldBeNil) 497 498 for _, s := range streams { 499 So(to.logs(s.desc.Name), shouldHaveTextLogs, "test data") 500 So(to.isTerminal(s.desc.Name), ShouldBeTrue) 501 } 502 }) 503 504 Convey(`Can register both during Run and will retain streams.`, func() { 505 b := mkb(c, conf) 506 for i, tss := range servers { 507 b.AddStreamServer(tss) 508 509 s := newTestStream(func(d *logpb.LogStreamDescriptor) { 510 d.Name = fmt.Sprintf("stream%d", i) 511 }) 512 streams = append(streams, s) 513 s.data([]byte("test data"), io.EOF) 514 tss.enqueue(s) 515 } 516 517 b.Activate() 518 So(b.Wait(), ShouldBeNil) 519 520 for _, s := range streams { 521 So(to.logs(s.desc.Name), shouldHaveTextLogs, "test data") 522 So(to.isTerminal(s.desc.Name), ShouldBeTrue) 523 } 524 }) 525 }) 526 527 Convey(`Will ignore stream registration errors, allowing re-registration.`, func() { 528 tss := newTestStreamServer() 529 530 // Generate an invalid stream for "tss" to register. 531 sGood := newTestStream(nil) 532 sGood.data([]byte("good test data"), io.EOF) 533 534 sBad := newTestStream(func(d *logpb.LogStreamDescriptor) { 535 d.ContentType = "" 536 }) 537 sBad.data([]byte("bad test data"), io.EOF) 538 539 b := mkb(c, conf) 540 b.AddStreamServer(tss) 541 tss.enqueue(sBad) 542 tss.enqueue(sGood) 543 b.Activate() 544 So(b.Wait(), ShouldBeNil) 545 546 So(sBad.isClosed(), ShouldBeTrue) 547 So(sGood.isClosed(), ShouldBeTrue) 548 So(to.logs("test"), shouldHaveTextLogs, "good test data") 549 So(to.isTerminal("test"), ShouldBeTrue) 550 }) 551 }) 552 553 Convey(`Will terminate if the stream server panics.`, func() { 554 tss := newTestStreamServer() 555 tss.onNext = func() { 556 panic("test panic") 557 } 558 559 b := mkb(c, conf) 560 b.AddStreamServer(tss) 561 So(b.Wait(), ShouldErrLike, "test panic") 562 }) 563 564 Convey(`Can wait for a subset of streams to complete.`, func() { 565 c = logging.SetLevel(gologger.StdConfig.Use(c), logging.Debug) 566 567 b := mkb(c, conf) 568 defer func() { 569 b.Activate() 570 b.Wait() 571 }() 572 573 newTestStream := func(setup func(d *logpb.LogStreamDescriptor)) *testStream { 574 desc := &logpb.LogStreamDescriptor{ 575 Name: "test", 576 StreamType: logpb.StreamType_TEXT, 577 ContentType: string(types.ContentTypeText), 578 } 579 if setup != nil { 580 setup(desc) 581 } 582 583 return &testStream{ 584 inC: make(chan *testStreamData, 16), 585 closedC: make(chan struct{}), 586 desc: desc, 587 allowDoubleClose: true, 588 } 589 } 590 591 deep := newTestStream(func(d *logpb.LogStreamDescriptor) { d.Name = "ns/deep/s" }) 592 defer deep.Close() 593 594 ns := newTestStream(func(d *logpb.LogStreamDescriptor) { d.Name = "ns/s" }) 595 defer ns.Close() 596 597 s := newTestStream(func(d *logpb.LogStreamDescriptor) { d.Name = "s" }) 598 defer s.Close() 599 600 b.AddStream(deep, deep.desc) 601 b.AddStream(ns, ns.desc) 602 b.AddStream(s, s.desc) 603 604 wait := func(cvctx C, ns ...types.StreamName) <-chan struct{} { 605 ch := make(chan struct{}) 606 ret := sync.WaitGroup{} 607 ret.Add(len(ns)) 608 go func() { 609 ret.Wait() 610 close(ch) 611 }() 612 for _, singleNS := range ns { 613 singleNS := singleNS 614 go func() { 615 defer ret.Done() 616 cvctx.So(b.DrainNamespace(c, singleNS), ShouldBeNil) 617 }() 618 } 619 return ch 620 } 621 622 check := func(toWait <-chan struct{}, toClose ...*testStream) { 623 select { 624 case <-time.After(time.Millisecond): 625 case <-toWait: 626 panic("we should time out here") 627 } 628 629 for _, c := range toClose { 630 So(c.Close(), ShouldBeNil) 631 } 632 <-toWait 633 } 634 635 Convey(`waiting for already-empty namespace works`, func() { 636 So(b.DrainNamespace(c, "other"), ShouldBeNil) 637 }) 638 639 Convey(`can wait at a deep level`, func(cvctx C) { 640 check(wait(cvctx, "ns/deep"), deep) 641 642 Convey(`and we now cannot open new streams under that namespace`, func() { 643 err := b.AddStream(nil, &logpb.LogStreamDescriptor{ 644 Name: "ns/deep/other", 645 ContentType: "wat", 646 }) 647 So(err, ShouldErrLike, `namespace "ns/deep/": already closed`) 648 }) 649 650 Convey(`can still open new adjacent streams`, func() { 651 cool := newTestStream(func(d *logpb.LogStreamDescriptor) { d.Name = "ns/side/other" }) 652 defer cool.Close() 653 So(b.AddStream(cool, cool.desc), ShouldBeNil) 654 }) 655 656 Convey(`then we can also wait at higher levels`, func(cvctx C) { 657 check(wait(cvctx, "ns"), ns) 658 }) 659 }) 660 661 Convey(`can wait at multiple levels`, func(cvctx C) { 662 check(wait(cvctx, "ns", "ns/deep"), ns, deep) 663 }) 664 665 Convey(`can cancel the wait and see leftovers`, func() { 666 cctx, cancel := context.WithCancel(c) 667 cancel() 668 So(b.DrainNamespace(cctx, "ns"), ShouldResemble, []types.StreamName{ 669 "ns/deep/s", 670 "ns/s", 671 }) 672 }) 673 674 Convey(`can cancel entire namespace`, func(cvctx C) { 675 // TODO(iannucci): We should use this in Wait() for a more orderly 676 // shutdown. 677 check(wait(cvctx, ""), ns, deep, s) 678 }) 679 }) 680 }) 681 } 682 683 // Command to run: `go test -cpu 1,4,8 -benchmem -benchtime 5s -bench .` 684 func BenchmarkButler(b *testing.B) { 685 numOfStreams := []int{10, 100, 500} 686 testText := []string{ 687 "This is line one\n", 688 "This is another two\n", 689 "This is final line", 690 } 691 692 for _, n := range numOfStreams { 693 b.Run(fmt.Sprintf("%d-streams", n), func(b *testing.B) { 694 for i := 0; i <= b.N; i++ { 695 if err := runButlerBenchmark(testText, n); err != nil { 696 b.Fatal(err) 697 } 698 } 699 }) 700 } 701 } 702 703 // runButlerBenchmark creates a new butler instance and given number of 704 // streams, then write the supplied text to each stream and wait for 705 // the butler instance to complete. 706 func runButlerBenchmark(testText []string, numOfStream int) error { 707 conf := Config{ 708 Output: &null.Output{}, 709 BufferLogs: false, 710 } 711 tb := mkb(context.Background(), conf) 712 tss := newTestStreamServer() 713 tb.AddStreamServer(tss) 714 715 testStreams := make([]*testStream, numOfStream) 716 717 for i := range testStreams { 718 testStreams[i] = &testStream{ 719 inC: make(chan *testStreamData, 16), 720 closedC: make(chan struct{}), 721 desc: &logpb.LogStreamDescriptor{ 722 Name: fmt.Sprintf("stream-%d", i), 723 StreamType: logpb.StreamType_TEXT, 724 ContentType: string(types.ContentTypeText), 725 }, 726 } 727 tss.enqueue(testStreams[i]) 728 } 729 730 for _, ts := range testStreams { 731 go func(s *testStream) { 732 for _, line := range testText { 733 s.data([]byte(line), nil) 734 } 735 s.err(io.EOF) 736 }(ts) 737 } 738 739 tb.Activate() 740 if err := tb.Wait(); err != nil { 741 return err 742 } 743 return nil 744 }