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  )