github.com/m3db/m3@v1.5.0/src/cmd/services/m3coordinator/ingest/carbon/ingest_test.go (about) 1 // Copyright (c) 2019 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package ingestcarbon 22 23 import ( 24 "bytes" 25 "context" 26 "errors" 27 "fmt" 28 "io" 29 "math/rand" 30 "net" 31 "reflect" 32 "sort" 33 "sync" 34 "testing" 35 "time" 36 37 "github.com/m3db/m3/src/cmd/services/m3coordinator/downsample" 38 "github.com/m3db/m3/src/cmd/services/m3coordinator/ingest" 39 "github.com/m3db/m3/src/cmd/services/m3query/config" 40 "github.com/m3db/m3/src/dbnode/client" 41 "github.com/m3db/m3/src/metrics/aggregation" 42 "github.com/m3db/m3/src/metrics/policy" 43 "github.com/m3db/m3/src/query/graphite/graphite" 44 "github.com/m3db/m3/src/query/models" 45 "github.com/m3db/m3/src/query/storage/m3" 46 "github.com/m3db/m3/src/query/ts" 47 "github.com/m3db/m3/src/x/clock" 48 "github.com/m3db/m3/src/x/ident" 49 "github.com/m3db/m3/src/x/instrument" 50 xsync "github.com/m3db/m3/src/x/sync" 51 xtest "github.com/m3db/m3/src/x/test" 52 xtime "github.com/m3db/m3/src/x/time" 53 54 "github.com/golang/mock/gomock" 55 "github.com/stretchr/testify/assert" 56 "github.com/stretchr/testify/require" 57 ) 58 59 const ( 60 // Keep this value large enough to catch issues like the ingester 61 // not copying the name. 62 numLinesInTestPacket = 10000 63 64 graphiteSource = ts.SourceTypeGraphite 65 ) 66 67 var ( 68 // Created by init(). 69 testMetrics = []testMetric{} 70 testPacket = []byte{} 71 72 testOptions = Options{ 73 InstrumentOptions: instrument.NewOptions(), 74 WorkerPool: nil, // Set by init(). 75 } 76 77 testTagOpts = models.NewTagOptions(). 78 SetIDSchemeType(models.TypeGraphite) 79 80 testRulesMatchAll = CarbonIngesterRules{ 81 Rules: []config.CarbonIngesterRuleConfiguration{ 82 { 83 Pattern: ".*", // Match all. 84 Aggregation: config.CarbonIngesterAggregationConfiguration{ 85 Enabled: truePtr, 86 Type: aggregateMeanPtr, 87 }, 88 Policies: []config.CarbonIngesterStoragePolicyConfiguration{ 89 { 90 Resolution: 10 * time.Second, 91 Retention: 48 * time.Hour, 92 }, 93 }, 94 }, 95 }, 96 } 97 ) 98 99 type testRulesOptions struct { 100 substring string 101 prefix string 102 suffix string 103 } 104 105 func testRules(opts testRulesOptions) CarbonIngesterRules { 106 // Match prefix + substring + "1" + suffix twice with two patterns, and 107 // in one case with two policies and in the second with one policy. In 108 // addition, also match prefix + substring + "2" + suffix wit a single 109 // pattern and policy. 110 return CarbonIngesterRules{ 111 Rules: []config.CarbonIngesterRuleConfiguration{ 112 { 113 Pattern: opts.prefix + opts.substring + "1" + opts.suffix, 114 Aggregation: config.CarbonIngesterAggregationConfiguration{ 115 Enabled: truePtr, 116 Type: aggregateMeanPtr, 117 }, 118 Policies: []config.CarbonIngesterStoragePolicyConfiguration{ 119 { 120 Resolution: 10 * time.Second, 121 Retention: 48 * time.Hour, 122 }, 123 { 124 Resolution: 1 * time.Hour, 125 Retention: 7 * 24 * time.Hour, 126 }, 127 }, 128 }, 129 // Should never match as the previous one takes precedence. 130 { 131 Pattern: opts.prefix + opts.substring + "1" + opts.suffix, 132 Aggregation: config.CarbonIngesterAggregationConfiguration{ 133 Enabled: truePtr, 134 Type: aggregateMeanPtr, 135 }, 136 Policies: []config.CarbonIngesterStoragePolicyConfiguration{ 137 { 138 Resolution: time.Minute, 139 Retention: 24 * time.Hour, 140 }, 141 }, 142 }, 143 { 144 Pattern: opts.prefix + opts.substring + "2" + opts.suffix, 145 Aggregation: config.CarbonIngesterAggregationConfiguration{ 146 Enabled: truePtr, 147 Type: aggregateLastPtr, 148 }, 149 Policies: []config.CarbonIngesterStoragePolicyConfiguration{ 150 { 151 Resolution: 10 * time.Second, 152 Retention: 48 * time.Hour, 153 }, 154 }, 155 }, 156 { 157 Pattern: opts.prefix + opts.substring + "3" + opts.suffix, 158 Aggregation: config.CarbonIngesterAggregationConfiguration{ 159 Enabled: falsePtr, 160 }, 161 Policies: []config.CarbonIngesterStoragePolicyConfiguration{ 162 { 163 Resolution: 1 * time.Hour, 164 Retention: 7 * 24 * time.Hour, 165 }, 166 }, 167 }, 168 }, 169 } 170 } 171 172 func testExpectedWriteOptions(substring string) map[string]ingest.WriteOptions { 173 // Maps the rules above to their expected write options. 174 return map[string]ingest.WriteOptions{ 175 substring + "1": { 176 DownsampleOverride: true, 177 DownsampleMappingRules: []downsample.AutoMappingRule{ 178 { 179 Aggregations: []aggregation.Type{aggregation.Mean}, 180 Policies: []policy.StoragePolicy{ 181 policy.NewStoragePolicy(10*time.Second, xtime.Second, 48*time.Hour), 182 policy.NewStoragePolicy(1*time.Hour, xtime.Second, 7*24*time.Hour), 183 }, 184 }, 185 }, 186 WriteOverride: true, 187 }, 188 substring + "2": { 189 DownsampleOverride: true, 190 DownsampleMappingRules: []downsample.AutoMappingRule{ 191 { 192 Aggregations: []aggregation.Type{aggregation.Last}, 193 Policies: []policy.StoragePolicy{policy.NewStoragePolicy(10*time.Second, xtime.Second, 48*time.Hour)}, 194 }, 195 }, 196 WriteOverride: true, 197 }, 198 substring + "3": { 199 DownsampleOverride: true, 200 WriteOverride: true, 201 WriteStoragePolicies: []policy.StoragePolicy{ 202 policy.NewStoragePolicy(time.Hour, xtime.Second, 7*24*time.Hour), 203 }, 204 }, 205 } 206 } 207 208 func TestIngesterHandleConn(t *testing.T) { 209 ctrl := gomock.NewController(t) 210 mockDownsamplerAndWriter := ingest.NewMockDownsamplerAndWriter(ctrl) 211 212 var ( 213 lock = sync.Mutex{} 214 215 found = []testMetric{} 216 idx = 0 217 ) 218 mockDownsamplerAndWriter.EXPECT(). 219 Write(gomock.Any(), gomock.Any(), gomock.Any(), xtime.Second, gomock.Any(), gomock.Any(), graphiteSource). 220 DoAndReturn(func( 221 _ context.Context, 222 tags models.Tags, 223 dp ts.Datapoints, 224 unit xtime.Unit, 225 annotation []byte, 226 overrides ingest.WriteOptions, 227 _ ts.SourceType, 228 ) interface{} { 229 lock.Lock() 230 // Clone tags because they (and their underlying bytes) are pooled. 231 found = append(found, testMetric{ 232 tags: tags.Clone(), 233 timestamp: int(dp[0].Timestamp.Seconds()), 234 value: dp[0].Value, 235 }) 236 237 // Make 1 in 10 writes fail to test those paths. 238 returnErr := idx%10 == 0 239 idx++ 240 lock.Unlock() 241 242 if returnErr { 243 return errors.New("some_error") 244 } 245 return nil 246 }).AnyTimes() 247 248 session := client.NewMockSession(ctrl) 249 watcher := newTestWatcher(t, session, m3.AggregatedClusterNamespaceDefinition{ 250 NamespaceID: ident.StringID("10s:48h"), 251 Resolution: 10 * time.Second, 252 Retention: 48 * time.Hour, 253 Session: session, 254 }) 255 256 byteConn := &byteConn{b: bytes.NewBuffer(testPacket)} 257 ingester, err := NewIngester(mockDownsamplerAndWriter, watcher, newTestOpts(testRulesMatchAll)) 258 require.NoError(t, err) 259 ingester.Handle(byteConn) 260 261 assertTestMetricsAreEqual(t, testMetrics, found) 262 } 263 264 func TestIngesterHonorsMatchers(t *testing.T) { 265 tests := []struct { 266 name string 267 input string 268 rules CarbonIngesterRules 269 expectedWriteOptions map[string]ingest.WriteOptions 270 expectedMetrics []testMetric 271 }{ 272 { 273 name: "regexp matching", 274 input: "foo.match-regex1.bar.baz 1 1\n" + 275 "foo.match-regex2.bar.baz 2 2\n" + 276 "foo.match-regex3.bar.baz 3 3\n" + 277 "foo.match-not-regex.bar.baz 4 4", 278 rules: testRules(testRulesOptions{ 279 substring: "match-regex", 280 prefix: ".*", 281 suffix: ".*", 282 }), 283 expectedWriteOptions: testExpectedWriteOptions("match-regex"), 284 expectedMetrics: []testMetric{ 285 { 286 metric: []byte("foo.match-regex1.bar.baz"), 287 tags: mustGenerateTagsFromName(t, []byte("foo.match-regex1.bar.baz")), 288 timestamp: 1, 289 value: 1, 290 }, 291 { 292 metric: []byte("foo.match-regex2.bar.baz"), 293 tags: mustGenerateTagsFromName(t, []byte("foo.match-regex2.bar.baz")), 294 timestamp: 2, 295 value: 2, 296 }, 297 { 298 metric: []byte("foo.match-regex3.bar.baz"), 299 tags: mustGenerateTagsFromName(t, []byte("foo.match-regex3.bar.baz")), 300 timestamp: 3, 301 value: 3, 302 }, 303 }, 304 }, 305 { 306 name: "contains matching", 307 input: "foo.match-contains1.bar.baz 1 1\n" + 308 "foo.match-contains2.bar.baz 2 2\n" + 309 "foo.match-contains3.bar.baz 3 3\n" + 310 "foo.match-not-contains.bar.baz 4 4", 311 rules: testRules(testRulesOptions{ 312 substring: "match-contains", 313 prefix: ".*", 314 suffix: ".*", 315 }), 316 expectedWriteOptions: testExpectedWriteOptions("match-contains"), 317 expectedMetrics: []testMetric{ 318 { 319 metric: []byte("foo.match-contains1.bar.baz"), 320 tags: mustGenerateTagsFromName(t, []byte("foo.match-contains1.bar.baz")), 321 timestamp: 1, 322 value: 1, 323 }, 324 { 325 metric: []byte("foo.match-contains2.bar.baz"), 326 tags: mustGenerateTagsFromName(t, []byte("foo.match-contains2.bar.baz")), 327 timestamp: 2, 328 value: 2, 329 }, 330 { 331 metric: []byte("foo.match-contains3.bar.baz"), 332 tags: mustGenerateTagsFromName(t, []byte("foo.match-contains3.bar.baz")), 333 timestamp: 3, 334 value: 3, 335 }, 336 }, 337 }, 338 } 339 340 for _, test := range tests { 341 t.Run(test.name, func(t *testing.T) { 342 ctrl := gomock.NewController(t) 343 mockDownsamplerAndWriter := ingest.NewMockDownsamplerAndWriter(ctrl) 344 345 var ( 346 lock = sync.Mutex{} 347 found = []testMetric{} 348 ) 349 mockDownsamplerAndWriter.EXPECT(). 350 Write(gomock.Any(), gomock.Any(), gomock.Any(), xtime.Second, gomock.Any(), gomock.Any(), graphiteSource). 351 DoAndReturn(func( 352 _ context.Context, 353 tags models.Tags, 354 dp ts.Datapoints, 355 unit xtime.Unit, 356 annotation []byte, 357 writeOpts ingest.WriteOptions, 358 _ ts.SourceType, 359 ) interface{} { 360 lock.Lock() 361 // Clone tags because they (and their underlying bytes) are pooled. 362 found = append(found, testMetric{ 363 tags: tags.Clone(), 364 timestamp: int(dp[0].Timestamp.Seconds()), 365 value: dp[0].Value, 366 }) 367 lock.Unlock() 368 369 // Use panic's instead of require/assert because those don't behave properly when the assertion 370 // is run in a background goroutine. Also we match on the second tag val just due to the nature 371 // of how the patterns were written. 372 secondTagVal := string(tags.Tags[1].Value) 373 expectedWriteOpts, ok := test.expectedWriteOptions[secondTagVal] 374 if !ok { 375 panic(fmt.Sprintf("expected write options for: %s", secondTagVal)) 376 } 377 378 if !reflect.DeepEqual(expectedWriteOpts, writeOpts) { 379 panic(fmt.Sprintf("expected %v to equal %v for metric: %s", 380 expectedWriteOpts, writeOpts, secondTagVal)) 381 } 382 383 return nil 384 }). 385 AnyTimes() 386 387 byteConn := &byteConn{b: bytes.NewBuffer([]byte(test.input))} 388 389 session := client.NewMockSession(ctrl) 390 watcher := newTestWatcher(t, session, m3.AggregatedClusterNamespaceDefinition{ 391 NamespaceID: ident.StringID("10s:48h"), 392 Resolution: 10 * time.Second, 393 Retention: 48 * time.Hour, 394 Session: session, 395 }, m3.AggregatedClusterNamespaceDefinition{ 396 NamespaceID: ident.StringID("1m:24h"), 397 Resolution: 1 * time.Minute, 398 Retention: 24 * time.Hour, 399 Session: session, 400 }, m3.AggregatedClusterNamespaceDefinition{ 401 NamespaceID: ident.StringID("1h:168h"), 402 Resolution: 1 * time.Hour, 403 Retention: 168 * time.Hour, 404 Session: session, 405 }) 406 407 ingester, err := NewIngester(mockDownsamplerAndWriter, watcher, 408 newTestOpts(test.rules)) 409 require.NoError(t, err) 410 ingester.Handle(byteConn) 411 412 assertTestMetricsAreEqual(t, test.expectedMetrics, found) 413 }) 414 } 415 } 416 417 func TestIngesterNoStaticRules(t *testing.T) { 418 ctrl := xtest.NewController(t) 419 defer ctrl.Finish() 420 421 var expectationErr error 422 mockDownsamplerAndWriter, found := newMockDownsamplerAndWriter(ctrl, func(mappingRules []downsample.AutoMappingRule) { 423 if len(mappingRules) != 1 { 424 expectationErr = errors.New(fmt.Sprintf("expected: len(DownsampleMappingRules) == 1, got: %v", len(mappingRules))) 425 } 426 policies := mappingRules[0].Policies 427 428 if len(policies) != 1 { 429 panic(fmt.Sprintf("expected: len(policies) == 1, got: %v", len(policies))) 430 } 431 expectedPolicy := policy.NewStoragePolicy(10*time.Second, xtime.Second, 48*time.Hour) 432 if ok := expectedPolicy == policies[0]; !ok { 433 expectationErr = errors.New(fmt.Sprintf("expected storage policy: %+v, got: %+v", expectedPolicy, policies[0])) 434 } 435 }) 436 437 session := client.NewMockSession(ctrl) 438 watcher := newTestWatcher(t, session, m3.AggregatedClusterNamespaceDefinition{ 439 NamespaceID: ident.StringID("10s:48h"), 440 Resolution: 10 * time.Second, 441 Retention: 48 * time.Hour, 442 Session: session, 443 }) 444 445 conn := &byteConn{b: bytes.NewBuffer(testPacket)} 446 i, err := NewIngester(mockDownsamplerAndWriter, watcher, newTestOpts(CarbonIngesterRules{Rules: nil})) 447 require.NoError(t, err) 448 449 downcast, ok := i.(*ingester) 450 require.True(t, ok) 451 452 // Wait until rules are updated and store them for later comparison. 453 var origRules []ruleAndMatcher 454 require.True(t, clock.WaitUntil(func() bool { 455 downcast.RLock() 456 origRules = downcast.rules 457 downcast.RUnlock() 458 459 return len(origRules) > 0 460 }, time.Second)) 461 462 i.Handle(conn) 463 464 assertTestMetricsAreEqual(t, testMetrics, *found) 465 require.NoError(t, expectationErr) 466 467 // Simulate namespace changes while ingester exists. 468 clusterNamespaces := newClusterNamespaces(t, session, m3.AggregatedClusterNamespaceDefinition{ 469 NamespaceID: ident.StringID("10s:48h"), 470 Resolution: 10 * time.Second, 471 Retention: 48 * time.Hour, 472 Session: session, 473 }, m3.AggregatedClusterNamespaceDefinition{ 474 NamespaceID: ident.StringID("1m:7d"), 475 Resolution: 1 * time.Minute, 476 Retention: 168 * time.Hour, 477 Session: session, 478 }) 479 480 err = watcher.Update(clusterNamespaces) 481 require.NoError(t, err) 482 483 // Ensure storage policies on mapping rules have been updated now that new aggregated namespaces have 484 // been added. 485 expectationErr = nil 486 mockDownsamplerAndWriter, found = newMockDownsamplerAndWriter(ctrl, func(mappingRules []downsample.AutoMappingRule) { 487 // Use panics instead of require/assert because those don't behave properly when the assertion 488 // is run in a background goroutine. 489 if len(mappingRules) != 1 { 490 panic(fmt.Sprintf("expected: len(DownsampleMappingRules) == 1, got: %v", len(mappingRules))) 491 } 492 policies := mappingRules[0].Policies 493 494 if len(policies) != 2 { 495 panic(fmt.Sprintf("expected: len(policies) == 2, got: %v", len(policies))) 496 } 497 expectedPolicy := policy.NewStoragePolicy(10*time.Second, xtime.Second, 48*time.Hour) 498 if ok := expectedPolicy == policies[0]; !ok { 499 expectationErr = errors.New(fmt.Sprintf("expected storage policy: %+v, got: %+v", expectedPolicy, policies[0])) 500 } 501 expectedPolicy = policy.NewStoragePolicy(1*time.Minute, xtime.Second, 168*time.Hour) 502 if ok := expectedPolicy == policies[1]; !ok { 503 expectationErr = errors.New(fmt.Sprintf("expected storage policy: %+v, got: %+v", expectedPolicy, policies[1])) 504 } 505 }) 506 507 // Need to do this to update the mock to check for storage policy updates we expect to see. 508 downcast.downsamplerAndWriter = mockDownsamplerAndWriter 509 510 // Wait for rules to be updated again. 511 require.True(t, clock.WaitUntil(func() bool { 512 downcast.RLock() 513 defer downcast.RUnlock() 514 515 return !assert.ObjectsAreEqual(origRules, downcast.rules) 516 }, time.Second)) 517 518 conn = &byteConn{b: bytes.NewBuffer(testPacket)} 519 downcast.Handle(conn) 520 521 assertTestMetricsAreEqual(t, testMetrics, *found) 522 require.NoError(t, expectationErr) 523 } 524 525 func newMockDownsamplerAndWriter( 526 ctrl *gomock.Controller, 527 expectations func(mappingRules []downsample.AutoMappingRule), 528 ) (*ingest.MockDownsamplerAndWriter, *[]testMetric) { 529 mockDownsamplerAndWriter := ingest.NewMockDownsamplerAndWriter(ctrl) 530 531 var ( 532 lock sync.Mutex 533 metrics = make([]testMetric, 0, numLinesInTestPacket) 534 found = &metrics 535 idx = 0 536 ) 537 mockDownsamplerAndWriter.EXPECT(). 538 Write(gomock.Any(), gomock.Any(), gomock.Any(), xtime.Second, gomock.Any(), gomock.Any(), graphiteSource). 539 DoAndReturn(func( 540 _ context.Context, 541 tags models.Tags, 542 dp ts.Datapoints, 543 unit xtime.Unit, 544 annotation []byte, 545 writeOpts ingest.WriteOptions, 546 _ ts.SourceType, 547 ) interface{} { 548 lock.Lock() 549 // Clone tags because they (and their underlying bytes) are pooled. 550 *found = append(*found, testMetric{ 551 tags: tags.Clone(), 552 timestamp: int(dp[0].Timestamp.Seconds()), 553 value: dp[0].Value, 554 }) 555 556 // Make 1 in 10 writes fail to test those paths. 557 returnErr := idx%10 == 0 558 idx++ 559 lock.Unlock() 560 561 expectations(writeOpts.DownsampleMappingRules) 562 563 if returnErr { 564 return errors.New("some_error") 565 } 566 return nil 567 }).AnyTimes() 568 569 return mockDownsamplerAndWriter, found 570 } 571 572 func TestGenerateTagsFromName(t *testing.T) { 573 testCases := []struct { 574 name string 575 id string 576 expectedTags []models.Tag 577 expectedErr error 578 }{ 579 { 580 name: "foo", 581 id: "foo", 582 expectedTags: []models.Tag{ 583 {Name: graphite.TagName(0), Value: []byte("foo")}, 584 }, 585 }, 586 { 587 name: "foo.bar.baz", 588 id: "foo.bar.baz", 589 expectedTags: []models.Tag{ 590 {Name: graphite.TagName(0), Value: []byte("foo")}, 591 {Name: graphite.TagName(1), Value: []byte("bar")}, 592 {Name: graphite.TagName(2), Value: []byte("baz")}, 593 }, 594 }, 595 { 596 name: "foo.bar.baz.", 597 id: "foo.bar.baz", 598 expectedTags: []models.Tag{ 599 {Name: graphite.TagName(0), Value: []byte("foo")}, 600 {Name: graphite.TagName(1), Value: []byte("bar")}, 601 {Name: graphite.TagName(2), Value: []byte("baz")}, 602 }, 603 }, 604 { 605 name: "foo..bar..baz..", 606 expectedErr: fmt.Errorf("carbon metric: foo..bar..baz.. has duplicate separator"), 607 expectedTags: []models.Tag{}, 608 }, 609 { 610 name: "foo.bar.baz..", 611 expectedErr: fmt.Errorf("carbon metric: foo.bar.baz.. has duplicate separator"), 612 expectedTags: []models.Tag{}, 613 }, 614 } 615 616 opts := models.NewTagOptions().SetIDSchemeType(models.TypeGraphite) 617 for _, tc := range testCases { 618 tags, err := GenerateTagsFromName([]byte(tc.name), opts) 619 if tc.expectedErr != nil { 620 require.Equal(t, tc.expectedErr, err) 621 } else { 622 require.NoError(t, err) 623 assert.Equal(t, []byte(tc.id), tags.ID()) 624 } 625 require.Equal(t, tc.expectedTags, tags.Tags) 626 } 627 } 628 629 func newTestOpts(rules CarbonIngesterRules) Options { 630 cfg := config.CarbonIngesterConfiguration{Rules: rules.Rules} 631 opts := testOptions 632 opts.IngesterConfig = cfg 633 return opts 634 } 635 636 func newTestWatcher( 637 t *testing.T, 638 session client.Session, 639 aggNamespaces ...m3.AggregatedClusterNamespaceDefinition, 640 ) m3.ClusterNamespacesWatcher { 641 clusterNamespaces := newClusterNamespaces(t, session, aggNamespaces...) 642 watcher := m3.NewClusterNamespacesWatcher() 643 err := watcher.Update(clusterNamespaces) 644 require.NoError(t, err) 645 646 return watcher 647 } 648 649 func newClusterNamespaces( 650 t *testing.T, 651 session client.Session, 652 aggNamespaces ...m3.AggregatedClusterNamespaceDefinition, 653 ) m3.ClusterNamespaces { 654 clusters, err := m3.NewClusters(m3.UnaggregatedClusterNamespaceDefinition{ 655 NamespaceID: ident.StringID("default"), 656 Retention: 48 * time.Hour, 657 Session: session, 658 }, aggNamespaces...) 659 require.NoError(t, err) 660 661 return clusters.ClusterNamespaces() 662 } 663 664 // byteConn implements the net.Conn interface so that we can test the handler without 665 // going over the network. 666 type byteConn struct { 667 b io.Reader 668 closed bool 669 } 670 671 func (b *byteConn) Read(buf []byte) (n int, err error) { 672 if !b.closed { 673 return b.b.Read(buf) 674 } 675 676 return 0, io.EOF 677 } 678 679 func (b *byteConn) Write(buf []byte) (n int, err error) { 680 panic("not_implemented") 681 } 682 683 func (b *byteConn) Close() error { 684 b.closed = true 685 return nil 686 } 687 688 func (b *byteConn) LocalAddr() net.Addr { 689 panic("not_implemented") 690 } 691 692 func (b *byteConn) RemoteAddr() net.Addr { 693 panic("not_implemented") 694 } 695 696 func (b *byteConn) SetDeadline(t time.Time) error { 697 panic("not_implemented") 698 } 699 700 func (b *byteConn) SetReadDeadline(t time.Time) error { 701 panic("not_implemented") 702 } 703 704 func (b *byteConn) SetWriteDeadline(t time.Time) error { 705 panic("not_implemented") 706 } 707 708 type testMetric struct { 709 metric []byte 710 tags models.Tags 711 timestamp int 712 value float64 713 } 714 715 func assertTestMetricsAreEqual(t *testing.T, a, b []testMetric) { 716 require.Equal(t, len(a), len(b)) 717 718 sort.Slice(b, func(i, j int) bool { 719 return b[i].timestamp < b[j].timestamp 720 }) 721 722 for i, f := range b { 723 require.Equal(t, a[i].tags, f.tags) 724 require.Equal(t, a[i].timestamp, f.timestamp) 725 require.Equal(t, a[i].value, f.value) 726 } 727 } 728 729 func init() { 730 var err error 731 testOptions.WorkerPool, err = xsync.NewPooledWorkerPool(16, xsync.NewPooledWorkerPoolOptions()) 732 if err != nil { 733 panic(err) 734 } 735 testOptions.WorkerPool.Init() 736 737 for i := 0; i < numLinesInTestPacket; i++ { 738 var metric []byte 739 740 if i%10 == 0 { 741 // Make 1 in 10 lines invalid to test the error paths. 742 if rand.Intn(2) == 0 { 743 // Invalid line altogether. 744 line := []byte(fmt.Sprintf("garbage line %d \n", i)) 745 testPacket = append(testPacket, line...) 746 continue 747 } else { 748 // Valid line, but invalid name (too many separators). 749 line := []byte(fmt.Sprintf("test..metric..%d %d %d\n", i, i, i)) 750 testPacket = append(testPacket, line...) 751 continue 752 } 753 } 754 755 metric = []byte(fmt.Sprintf("test.metric.%d", i)) 756 757 opts := models.NewTagOptions().SetIDSchemeType(models.TypeGraphite) 758 tags, err := GenerateTagsFromName(metric, opts) 759 if err != nil { 760 panic(err) 761 } 762 testMetrics = append(testMetrics, testMetric{ 763 metric: metric, 764 tags: tags, 765 timestamp: i, 766 value: float64(i), 767 }) 768 769 line := []byte(fmt.Sprintf("%s %d %d\n", string(metric), i, i)) 770 testPacket = append(testPacket, line...) 771 } 772 } 773 774 func mustGenerateTagsFromName(t *testing.T, name []byte) models.Tags { 775 tags, err := GenerateTagsFromName(name, testTagOpts) 776 require.NoError(t, err) 777 return tags 778 } 779 780 var ( 781 // Boilerplate to deal with optional config value nonsense. 782 trueVar = true 783 truePtr = &trueVar 784 falseVar = false 785 falsePtr = &falseVar 786 aggregateMean = aggregation.Mean 787 aggregateLast = aggregation.Last 788 aggregateMeanPtr = &aggregateMean 789 aggregateLastPtr = &aggregateLast 790 )