github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/prometheus/prometheus_test.go (about) 1 // Copyright 2023 The Inspektor Gadget 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 prometheus 16 17 import ( 18 "context" 19 "fmt" 20 "sync" 21 "testing" 22 "time" 23 24 "github.com/stretchr/testify/require" 25 26 "github.com/inspektor-gadget/inspektor-gadget/pkg/prometheus/config" 27 ) 28 29 // events that are generated in the test. Counters are incremented based on them and the metric 30 // configuration 31 var testEvents = []*stubEvent{ 32 {Comm: "cat", Uid: 0, IntVal: 105, FloatVal: 201.2}, 33 {Comm: "cat", Uid: 0, IntVal: 216, FloatVal: 423.3}, 34 {Comm: "cat", Uid: 1000, IntVal: 327, FloatVal: 645.4}, 35 {Comm: "ping", Uid: 0, IntVal: 428, FloatVal: 867.5}, 36 {Comm: "ls", Uid: 1000, IntVal: 429, FloatVal: 1089.6}, 37 } 38 39 func TestMetrics(t *testing.T) { 40 type testDefinition struct { 41 name string 42 config *config.Config 43 expectedErr bool 44 45 // outer key: metric name, inner key: attributes hash 46 expectedInt64Counters map[string]map[string]int64 47 expectedFloat64Counters map[string]map[string]float64 48 expectedInt64Gauges map[string]map[string]int64 49 expectedFloat64Gauges map[string]map[string]float64 50 expectedInt64Histograms map[string]map[string]int64 51 expectedFloat64Histograms map[string]map[string]float64 52 } 53 54 tests := []testDefinition{ 55 // Generic checks before 56 { 57 name: "wrong_metric_type", 58 config: &config.Config{ 59 MetricsName: "wrong_metric_type", 60 Metrics: []config.Metric{ 61 { 62 Name: "wrong_metric_type", 63 Type: "nonvalidtype", 64 Category: "trace", 65 Gadget: "stubtracer", 66 }, 67 }, 68 }, 69 expectedErr: true, 70 }, 71 // Wrong configurations 72 { 73 name: "counter_wrong_gadget_name", 74 config: &config.Config{ 75 MetricsName: "counter_wrong_gadget_name", 76 Metrics: []config.Metric{ 77 { 78 Name: "counter_wrong_gadget_name", 79 Type: "counter", 80 Category: "trace", 81 Gadget: "nonexisting", 82 }, 83 }, 84 }, 85 expectedErr: true, 86 }, 87 { 88 name: "counter_wrong_gadget_category", 89 config: &config.Config{ 90 MetricsName: "counter_wrong_gadget_category", 91 Metrics: []config.Metric{ 92 { 93 Name: "counter_wrong_gadget_category", 94 Type: "counter", 95 Category: "nonexisting", 96 Gadget: "stubtracer", 97 }, 98 }, 99 }, 100 expectedErr: true, 101 }, 102 { 103 name: "counter_wrong_gadget_type", 104 config: &config.Config{ 105 MetricsName: "counter_wrong_gadget_type", 106 Metrics: []config.Metric{ 107 { 108 Name: "counter_wrong_gadget_type", 109 Type: "counter", 110 Category: "snapshot", 111 Gadget: "stubsnapshotter", 112 }, 113 }, 114 }, 115 expectedErr: true, 116 }, 117 { 118 name: "counter_wrong_type_field", 119 config: &config.Config{ 120 MetricsName: "counter_wrong_type_field", 121 Metrics: []config.Metric{ 122 { 123 Name: "counter_wrong_type_field", 124 Type: "counter", 125 Category: "trace", 126 Gadget: "stubtracer", 127 Field: "comm", 128 }, 129 }, 130 }, 131 expectedErr: true, 132 }, 133 { 134 name: "counter_wrong_selector", 135 config: &config.Config{ 136 MetricsName: "counter_wrong_selector", 137 Metrics: []config.Metric{ 138 { 139 Name: "counter_wrong_selector", 140 Type: "counter", 141 Category: "trace", 142 Gadget: "stubtracer", 143 Field: "comm", 144 Selector: []string{"wrong:cat"}, 145 }, 146 }, 147 }, 148 expectedErr: true, 149 }, 150 { 151 name: "counter_wrong_labels", 152 config: &config.Config{ 153 MetricsName: "counter_wrong_labels", 154 Metrics: []config.Metric{ 155 { 156 Name: "counter_wrong_labels", 157 Type: "counter", 158 Category: "trace", 159 Gadget: "stubtracer", 160 Labels: []string{"wrong"}, 161 }, 162 }, 163 }, 164 expectedErr: true, 165 }, 166 // Check that counters are updated correctly 167 { 168 name: "counter_no_labels_nor_filtering", 169 config: &config.Config{ 170 MetricsName: "counter_no_labels_nor_filtering", 171 Metrics: []config.Metric{ 172 { 173 Name: "counter_no_labels_nor_filtering", 174 Type: "counter", 175 Category: "trace", 176 Gadget: "stubtracer", 177 }, 178 }, 179 }, 180 expectedInt64Counters: map[string]map[string]int64{ 181 "counter_no_labels_nor_filtering": {"": 5}, 182 }, 183 }, 184 { 185 name: "counter_filter_only_root_events", 186 config: &config.Config{ 187 MetricsName: "counter_filter_only_root_events", 188 Metrics: []config.Metric{ 189 { 190 Name: "counter_filter_only_root_events", 191 Type: "counter", 192 Category: "trace", 193 Gadget: "stubtracer", 194 Selector: []string{"uid:0"}, 195 }, 196 }, 197 }, 198 expectedInt64Counters: map[string]map[string]int64{ 199 "counter_filter_only_root_events": {"": 3}, 200 }, 201 }, 202 { 203 name: "counter_filter_only_root_cat_events", 204 config: &config.Config{ 205 MetricsName: "counter_filter_only_root_cat_events", 206 Metrics: []config.Metric{ 207 { 208 Name: "counter_filter_only_root_cat_events", 209 Type: "counter", 210 Category: "trace", 211 Gadget: "stubtracer", 212 Selector: []string{"uid:0", "comm:cat"}, 213 }, 214 }, 215 }, 216 expectedInt64Counters: map[string]map[string]int64{ 217 "counter_filter_only_root_cat_events": {"": 2}, 218 }, 219 }, 220 { 221 name: "counter_filter_uid_greater_than_0", 222 config: &config.Config{ 223 MetricsName: "counter_filter_uid_greater_than_0", 224 Metrics: []config.Metric{ 225 { 226 Name: "counter_filter_uid_greater_than_0", 227 Type: "counter", 228 Category: "trace", 229 Gadget: "stubtracer", 230 Selector: []string{"uid:>0"}, 231 }, 232 }, 233 }, 234 expectedInt64Counters: map[string]map[string]int64{ 235 "counter_filter_uid_greater_than_0": {"": 2}, 236 }, 237 }, 238 { 239 name: "counter_aggregate_by_comm", 240 config: &config.Config{ 241 MetricsName: "counter_aggregate_by_comm", 242 Metrics: []config.Metric{ 243 { 244 Name: "counter_aggregate_by_comm", 245 Type: "counter", 246 Category: "trace", 247 Gadget: "stubtracer", 248 Labels: []string{"comm"}, 249 }, 250 }, 251 }, 252 expectedInt64Counters: map[string]map[string]int64{ 253 "counter_aggregate_by_comm": {"comm=cat,": 3, "comm=ping,": 1, "comm=ls,": 1}, 254 }, 255 }, 256 { 257 name: "counter_aggregate_by_uid", 258 config: &config.Config{ 259 MetricsName: "counter_aggregate_by_uid", 260 Metrics: []config.Metric{ 261 { 262 Name: "counter_aggregate_by_uid", 263 Type: "counter", 264 Category: "trace", 265 Gadget: "stubtracer", 266 Labels: []string{"uid"}, 267 }, 268 }, 269 }, 270 expectedInt64Counters: map[string]map[string]int64{ 271 "counter_aggregate_by_uid": {"uid=0,": 3, "uid=1000,": 2}, 272 }, 273 }, 274 { 275 name: "counter_aggregate_by_uid_and_comm", 276 config: &config.Config{ 277 MetricsName: "counter_aggregate_by_uid_and_comm", 278 Metrics: []config.Metric{ 279 { 280 Name: "counter_aggregate_by_uid_and_comm", 281 Type: "counter", 282 Category: "trace", 283 Gadget: "stubtracer", 284 Labels: []string{"uid", "comm"}, 285 }, 286 }, 287 }, 288 expectedInt64Counters: map[string]map[string]int64{ 289 "counter_aggregate_by_uid_and_comm": { 290 "comm=cat,uid=0,": 2, 291 "comm=cat,uid=1000,": 1, 292 "comm=ping,uid=0,": 1, 293 "comm=ls,uid=1000,": 1, 294 }, 295 }, 296 }, 297 { 298 name: "counter_aggregate_by_uid_and_filter_by_comm", 299 config: &config.Config{ 300 MetricsName: "counter_aggregate_by_uid_and_filter_by_comm", 301 Metrics: []config.Metric{ 302 { 303 Name: "counter_aggregate_by_uid_and_filter_by_comm", 304 Type: "counter", 305 Category: "trace", 306 Gadget: "stubtracer", 307 Selector: []string{"comm:cat"}, 308 Labels: []string{"uid"}, 309 }, 310 }, 311 }, 312 expectedInt64Counters: map[string]map[string]int64{ 313 "counter_aggregate_by_uid_and_filter_by_comm": {"uid=0,": 2, "uid=1000,": 1}, 314 }, 315 }, 316 { 317 name: "counter_with_int_field", 318 config: &config.Config{ 319 MetricsName: "counter_with_int_field", 320 Metrics: []config.Metric{ 321 { 322 Name: "counter_with_int_field", 323 Type: "counter", 324 Category: "trace", 325 Gadget: "stubtracer", 326 Field: "intval", 327 }, 328 }, 329 }, 330 expectedInt64Counters: map[string]map[string]int64{ 331 "counter_with_int_field": {"": 105 + 216 + 327 + 428 + 429}, 332 }, 333 }, 334 { 335 name: "counter_with_float_field", 336 config: &config.Config{ 337 MetricsName: "counter_with_float_field", 338 Metrics: []config.Metric{ 339 { 340 Name: "counter_with_float_field", 341 Type: "counter", 342 Category: "trace", 343 Gadget: "stubtracer", 344 Field: "floatval", 345 }, 346 }, 347 }, 348 expectedFloat64Counters: map[string]map[string]float64{ 349 "counter_with_float_field": {"": 201.2 + 423.3 + 645.4 + 867.5 + 1089.6}, 350 }, 351 }, 352 { 353 name: "counter_with_float_field_aggregate_by_uid_and_filter_by_comm", 354 config: &config.Config{ 355 MetricsName: "counter_with_float_field_aggregate_by_uid_and_filter_by_comm", 356 Metrics: []config.Metric{ 357 { 358 Name: "counter_with_float_field_aggregate_by_uid_and_filter_by_comm", 359 Type: "counter", 360 Category: "trace", 361 Gadget: "stubtracer", 362 Field: "floatval", 363 Selector: []string{"comm:cat"}, 364 Labels: []string{"uid"}, 365 }, 366 }, 367 }, 368 expectedFloat64Counters: map[string]map[string]float64{ 369 "counter_with_float_field_aggregate_by_uid_and_filter_by_comm": {"uid=0,": 201.2 + 423.3, "uid=1000,": 645.4}, 370 }, 371 }, 372 // Multiple counters 373 { 374 name: "counter_multiple_mixed", 375 config: &config.Config{ 376 MetricsName: "counter_multiple_mixed", 377 Metrics: []config.Metric{ 378 { 379 Name: "counter_multiple1", 380 Type: "counter", 381 Category: "trace", 382 Gadget: "stubtracer", 383 Field: "floatval", 384 }, 385 { 386 Name: "counter_multiple2", 387 Type: "counter", 388 Category: "trace", 389 Gadget: "stubtracer", 390 }, 391 }, 392 }, 393 expectedInt64Counters: map[string]map[string]int64{ 394 "counter_multiple2": {"": 5}, 395 }, 396 expectedFloat64Counters: map[string]map[string]float64{ 397 "counter_multiple1": {"": 201.2 + 423.3 + 645.4 + 867.5 + 1089.6}, 398 }, 399 }, 400 // Gauges 401 { 402 name: "gauge_wrong_gadget_name", 403 config: &config.Config{ 404 MetricsName: "gauge_wrong_gadget_name", 405 Metrics: []config.Metric{ 406 { 407 Name: "gauge_wrong_gadget_name", 408 Type: "gauge", 409 Category: "snapshot", 410 Gadget: "nonexisting", 411 }, 412 }, 413 }, 414 expectedErr: true, 415 }, 416 { 417 name: "gauge_wrong_gadget_category", 418 config: &config.Config{ 419 MetricsName: "gauge_wrong_gadget_category", 420 Metrics: []config.Metric{ 421 { 422 Name: "gauge_wrong_gadget_category", 423 Type: "gauge", 424 Category: "nonexisting", 425 Gadget: "stubsnapshotter", 426 }, 427 }, 428 }, 429 expectedErr: true, 430 }, 431 { 432 name: "gauge_wrong_gadget_type", 433 config: &config.Config{ 434 MetricsName: "gauge_wrong_gadget_type", 435 Metrics: []config.Metric{ 436 { 437 Name: "counter_wrong_gadget_type", 438 Type: "gauge", 439 Category: "tracer", 440 Gadget: "stubtracer", 441 }, 442 }, 443 }, 444 expectedErr: true, 445 }, 446 { 447 name: "gauge_no_labels_nor_filtering", 448 config: &config.Config{ 449 MetricsName: "gauge_no_labels_nor_filtering", 450 Metrics: []config.Metric{ 451 { 452 Name: "gauge_no_labels_nor_filtering", 453 Type: "gauge", 454 Category: "snapshot", 455 Gadget: "stubsnapshotter", 456 }, 457 }, 458 }, 459 expectedInt64Gauges: map[string]map[string]int64{ 460 "gauge_no_labels_nor_filtering": {"": 5}, 461 }, 462 }, 463 { 464 name: "gauge_filter_only_root_events", 465 config: &config.Config{ 466 MetricsName: "gauge_filter_only_root_events", 467 Metrics: []config.Metric{ 468 { 469 Name: "gauge_filter_only_root_events", 470 Type: "gauge", 471 Category: "snapshot", 472 Gadget: "stubsnapshotter", 473 Selector: []string{"uid:0"}, 474 }, 475 }, 476 }, 477 expectedInt64Gauges: map[string]map[string]int64{ 478 "gauge_filter_only_root_events": {"": 3}, 479 }, 480 }, 481 { 482 name: "gauge_filter_only_root_cat_events", 483 config: &config.Config{ 484 MetricsName: "gauge_filter_only_root_cat_events", 485 Metrics: []config.Metric{ 486 { 487 Name: "gauge_filter_only_root_cat_events", 488 Type: "gauge", 489 Category: "snapshot", 490 Gadget: "stubsnapshotter", 491 Selector: []string{"uid:0", "comm:cat"}, 492 }, 493 }, 494 }, 495 expectedInt64Gauges: map[string]map[string]int64{ 496 "gauge_filter_only_root_cat_events": {"": 2}, 497 }, 498 }, 499 { 500 name: "gauge_with_int_field", 501 config: &config.Config{ 502 MetricsName: "gauge_with_int_field", 503 Metrics: []config.Metric{ 504 { 505 Name: "gauge_with_int_field", 506 Type: "gauge", 507 Category: "snapshot", 508 Gadget: "stubsnapshotter", 509 Field: "intval", 510 }, 511 }, 512 }, 513 expectedInt64Gauges: map[string]map[string]int64{ 514 "gauge_with_int_field": {"": 105 + 216 + 327 + 428 + 429}, 515 }, 516 }, 517 { 518 name: "gauge_with_float_field", 519 config: &config.Config{ 520 MetricsName: "gauge_with_float_field", 521 Metrics: []config.Metric{ 522 { 523 Name: "gauge_with_float_field", 524 Type: "gauge", 525 Category: "snapshot", 526 Gadget: "stubsnapshotter", 527 Field: "floatval", 528 }, 529 }, 530 }, 531 expectedFloat64Gauges: map[string]map[string]float64{ 532 "gauge_with_float_field": {"": 201.2 + 423.3 + 645.4 + 867.5 + 1089.6}, 533 }, 534 }, 535 { 536 name: "gauge_multiple", 537 config: &config.Config{ 538 MetricsName: "gauge_multiple", 539 Metrics: []config.Metric{ 540 { 541 Name: "gauge_no_labels_nor_filtering", 542 Type: "gauge", 543 Category: "snapshot", 544 Gadget: "stubsnapshotter", 545 }, 546 { 547 Name: "gauge_filter_only_root_events", 548 Type: "gauge", 549 Category: "snapshot", 550 Gadget: "stubsnapshotter", 551 Selector: []string{"uid:0"}, 552 }, 553 }, 554 }, 555 expectedInt64Gauges: map[string]map[string]int64{ 556 "gauge_no_labels_nor_filtering": {"": 5}, 557 "gauge_filter_only_root_events": {"": 3}, 558 }, 559 }, 560 { 561 name: "histogram_missing_bucket_config", 562 config: &config.Config{ 563 MetricsName: "histogram_missing_bucket_config", 564 Metrics: []config.Metric{ 565 { 566 Name: "histogram_missing_bucket_config", 567 Type: "histogram", 568 Category: "trace", 569 Gadget: "stubtracer", 570 Field: "intval", 571 }, 572 }, 573 }, 574 expectedErr: true, 575 }, 576 { 577 name: "histrogram_invalid_bucket_config", 578 config: &config.Config{ 579 MetricsName: "histrogram_invalid_bucket_config", 580 Metrics: []config.Metric{ 581 { 582 Name: "histrogram_invalid_bucket_config", 583 Type: "histogram", 584 Category: "trace", 585 Gadget: "stubtracer", 586 Field: "intval", 587 Bucket: config.Bucket{ 588 Type: "invalid", 589 Max: 1, 590 Multiplier: 10, 591 }, 592 }, 593 }, 594 }, 595 expectedErr: true, 596 }, 597 { 598 name: "histogram_int_field", 599 config: &config.Config{ 600 MetricsName: "histogram_int_field", 601 Metrics: []config.Metric{ 602 { 603 Name: "histogram_int_field", 604 Type: "histogram", 605 Category: "trace", 606 Gadget: "stubtracer", 607 Field: "intval", 608 Selector: []string{"uid:1000", "comm:cat"}, 609 Bucket: config.Bucket{ 610 Type: "linear", 611 Max: 5, 612 Min: 0, 613 Multiplier: 1, 614 }, 615 }, 616 }, 617 }, 618 expectedInt64Histograms: map[string]map[string]int64{ 619 "histogram_int_field": {"": 327}, 620 }, 621 }, 622 { 623 name: "histogram_float_field", 624 config: &config.Config{ 625 MetricsName: "histogram_float_field", 626 Metrics: []config.Metric{ 627 { 628 Name: "histogram_float_field", 629 Type: "histogram", 630 Category: "trace", 631 Gadget: "stubtracer", 632 Field: "floatval", 633 Selector: []string{"uid:1000", "comm:cat"}, 634 Bucket: config.Bucket{ 635 Type: "linear", 636 Max: 5, 637 Min: 0, 638 Multiplier: 1, 639 }, 640 }, 641 }, 642 }, 643 expectedFloat64Histograms: map[string]map[string]float64{ 644 "histogram_float_field": {"": 645.4}, 645 }, 646 }, 647 { 648 name: "histogram_multiple", 649 config: &config.Config{ 650 MetricsName: "histogram_multiple", 651 Metrics: []config.Metric{ 652 { 653 Name: "histogram_int_field", 654 Type: "histogram", 655 Category: "trace", 656 Gadget: "stubtracer", 657 Field: "intval", 658 Selector: []string{"uid:1000", "comm:cat"}, 659 Bucket: config.Bucket{ 660 Type: "linear", 661 Max: 5, 662 Multiplier: 1, 663 }, 664 }, 665 { 666 Name: "histogram_float_field", 667 Type: "histogram", 668 Category: "trace", 669 Gadget: "stubtracer", 670 Field: "floatval", 671 Selector: []string{"uid:1000", "comm:cat"}, 672 Bucket: config.Bucket{ 673 Type: "linear", 674 Max: 5, 675 Multiplier: 1, 676 }, 677 }, 678 }, 679 }, 680 expectedFloat64Histograms: map[string]map[string]float64{ 681 "histogram_float_field": {"": 645.4}, 682 }, 683 expectedInt64Histograms: map[string]map[string]int64{ 684 "histogram_int_field": {"": 327}, 685 }, 686 }, 687 } 688 689 for _, test := range tests { 690 test := test 691 t.Run(test.name, func(t *testing.T) { 692 t.Parallel() 693 694 ctx, cancel := context.WithCancel(context.Background()) 695 t.Cleanup(cancel) 696 697 wg := &sync.WaitGroup{} 698 wg.Add(len(test.config.Metrics)) 699 ctx = context.WithValue(ctx, valuekey, wg) 700 701 test.config.MetricsName = test.name 702 703 meterProvider := NewStubMeterProvider(t) 704 705 cleanup, err := CreateMetrics(ctx, test.config, meterProvider) 706 if test.expectedErr { 707 require.Error(t, err) 708 return 709 } 710 require.Nil(t, err) 711 t.Cleanup(cleanup) 712 713 name := fmt.Sprintf("gadgets.inspektor-gadget.io/%s", test.config.MetricsName) 714 meter := meterProvider.meters[name] 715 716 require.Equal(t, len(test.expectedInt64Counters), len(meter.int64counters)) 717 require.Equal(t, len(test.expectedFloat64Counters), len(meter.float64counters)) 718 require.Equal(t, len(test.expectedInt64Gauges), len(meter.int64gauges)) 719 require.Equal(t, len(test.expectedInt64Histograms), len(meter.int64histograms)) 720 require.Equal(t, len(test.expectedFloat64Histograms), len(meter.float64histograms)) 721 722 // Collect metrics: Update gauges 723 err = meter.Collect(ctx) 724 require.Nil(t, err, "failed to collect metrics") 725 726 // wait for the tracers to run 727 err = waitTimeout(wg, 5*time.Second) 728 require.Nil(t, err, "waiting timeout: %s", err) 729 730 // int64 counters 731 for name, expected := range test.expectedInt64Counters { 732 counter, ok := meter.int64counters[name] 733 require.True(t, ok, "int64 counter %q not found", name) 734 735 require.Equal(t, expected, counter.values, "counter values are wrong") 736 } 737 738 // float64 counters 739 for name, expected := range test.expectedFloat64Counters { 740 counter, ok := meter.float64counters[name] 741 require.True(t, ok, "float64 counter %q not found", name) 742 743 // require.Equal doesn't work because of float comparisons 744 require.InDeltaMapValues(t, expected, counter.values, 0.01, "counter values are wrong") 745 } 746 747 // int64 gauges 748 for name, expected := range test.expectedInt64Gauges { 749 gauge, ok := meter.int64gauges[name] 750 require.True(t, ok, "int64 gauge %q not found", name) 751 752 require.Equal(t, expected, gauge.values, "counter values are wrong") 753 } 754 755 // float64 gauges 756 for name, expected := range test.expectedFloat64Gauges { 757 gauge, ok := meter.float64gauges[name] 758 require.True(t, ok, "float gauge %q not found", name) 759 760 // require.Equal doesn't work because of float comparisons 761 require.InDeltaMapValues(t, expected, gauge.values, 0.01, "gauge values are wrong") 762 } 763 764 // int64 histograms 765 for name, expected := range test.expectedInt64Histograms { 766 histogram, ok := meter.int64histograms[name] 767 require.True(t, ok, "int64 histogram %q not found", name) 768 769 require.Equal(t, expected, histogram.values, "histogram values are wrong") 770 } 771 772 // float64 histograms 773 for name, expected := range test.expectedFloat64Histograms { 774 histogram, ok := meter.float64histograms[name] 775 require.True(t, ok, "float64 histogram %q not found", name) 776 777 // require.Equal doesn't work because of float comparisons 778 require.InDeltaMapValues(t, expected, histogram.values, 0.01, "histogram values are wrong") 779 } 780 }) 781 } 782 } 783 784 // Based on https://github.com/embano1/waitgroup/blob/e5229ff7bc061f391c12f2be244bb50f030a6688/waitgroup.go#L27 785 func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) error { 786 doneCh := make(chan struct{}) 787 timer := time.NewTimer(timeout) 788 defer timer.Stop() 789 790 go func() { 791 wg.Wait() 792 close(doneCh) 793 }() 794 795 select { 796 case <-timer.C: 797 return fmt.Errorf("timed out") 798 case <-doneCh: 799 return nil 800 } 801 }