github.com/thanos-io/thanos@v0.32.5/pkg/receive/hashring_test.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package receive
     5  
     6  import (
     7  	"fmt"
     8  	"math"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/efficientgo/core/testutil"
    13  	"github.com/stretchr/testify/require"
    14  
    15  	"github.com/prometheus/prometheus/model/labels"
    16  
    17  	"github.com/thanos-io/thanos/pkg/store/labelpb"
    18  	"github.com/thanos-io/thanos/pkg/store/storepb/prompb"
    19  )
    20  
    21  func TestHashringGet(t *testing.T) {
    22  	ts := &prompb.TimeSeries{
    23  		Labels: []labelpb.ZLabel{
    24  			{
    25  				Name:  "foo",
    26  				Value: "bar",
    27  			},
    28  			{
    29  				Name:  "baz",
    30  				Value: "qux",
    31  			},
    32  		},
    33  	}
    34  
    35  	for _, tc := range []struct {
    36  		name   string
    37  		cfg    []HashringConfig
    38  		nodes  map[string]struct{}
    39  		tenant string
    40  	}{
    41  		{
    42  			name:   "empty",
    43  			cfg:    nil,
    44  			tenant: "tenant1",
    45  		},
    46  		{
    47  			name: "simple",
    48  			cfg: []HashringConfig{
    49  				{
    50  					Endpoints: []Endpoint{{Address: "node1"}},
    51  				},
    52  			},
    53  			nodes: map[string]struct{}{"node1": {}},
    54  		},
    55  		{
    56  			name: "specific",
    57  			cfg: []HashringConfig{
    58  				{
    59  					Endpoints: []Endpoint{{Address: "node2"}},
    60  					Tenants:   []string{"tenant2"},
    61  				},
    62  				{
    63  					Endpoints: []Endpoint{{Address: "node1"}},
    64  				},
    65  			},
    66  			nodes:  map[string]struct{}{"node2": {}},
    67  			tenant: "tenant2",
    68  		},
    69  		{
    70  			name: "many tenants",
    71  			cfg: []HashringConfig{
    72  				{
    73  					Endpoints: []Endpoint{{Address: "node1"}},
    74  					Tenants:   []string{"tenant1"},
    75  				},
    76  				{
    77  					Endpoints: []Endpoint{{Address: "node2"}},
    78  					Tenants:   []string{"tenant2"},
    79  				},
    80  				{
    81  					Endpoints: []Endpoint{{Address: "node3"}},
    82  					Tenants:   []string{"tenant3"},
    83  				},
    84  			},
    85  			nodes:  map[string]struct{}{"node1": {}},
    86  			tenant: "tenant1",
    87  		},
    88  		{
    89  			name: "many tenants error",
    90  			cfg: []HashringConfig{
    91  				{
    92  					Endpoints: []Endpoint{{Address: "node1"}},
    93  					Tenants:   []string{"tenant1"},
    94  				},
    95  				{
    96  					Endpoints: []Endpoint{{Address: "node2"}},
    97  					Tenants:   []string{"tenant2"},
    98  				},
    99  				{
   100  					Endpoints: []Endpoint{{Address: "node3"}},
   101  					Tenants:   []string{"tenant3"},
   102  				},
   103  			},
   104  			tenant: "tenant4",
   105  		},
   106  		{
   107  			name: "many nodes",
   108  			cfg: []HashringConfig{
   109  				{
   110  					Endpoints: []Endpoint{{Address: "node1"}, {Address: "node2"}, {Address: "node3"}},
   111  					Tenants:   []string{"tenant1"},
   112  				},
   113  				{
   114  					Endpoints: []Endpoint{{Address: "node4"}, {Address: "node5"}, {Address: "node6"}},
   115  				},
   116  			},
   117  			nodes: map[string]struct{}{
   118  				"node1": {},
   119  				"node2": {},
   120  				"node3": {},
   121  			},
   122  			tenant: "tenant1",
   123  		},
   124  		{
   125  			name: "many nodes default",
   126  			cfg: []HashringConfig{
   127  				{
   128  					Endpoints: []Endpoint{{Address: "node1"}, {Address: "node2"}, {Address: "node3"}},
   129  					Tenants:   []string{"tenant1"},
   130  				},
   131  				{
   132  					Endpoints: []Endpoint{{Address: "node4"}, {Address: "node5"}, {Address: "node6"}},
   133  				},
   134  			},
   135  			nodes: map[string]struct{}{
   136  				"node4": {},
   137  				"node5": {},
   138  				"node6": {},
   139  			},
   140  		},
   141  	} {
   142  		hs, err := NewMultiHashring(AlgorithmHashmod, 3, tc.cfg)
   143  		require.NoError(t, err)
   144  
   145  		h, err := hs.Get(tc.tenant, ts)
   146  		if tc.nodes != nil {
   147  			if err != nil {
   148  				t.Errorf("case %q: got unexpected error: %v", tc.name, err)
   149  				continue
   150  			}
   151  			if _, ok := tc.nodes[h]; !ok {
   152  				t.Errorf("case %q: got unexpected node %q", tc.name, h)
   153  			}
   154  			continue
   155  		}
   156  		if err == nil {
   157  			t.Errorf("case %q: expected error", tc.name)
   158  		}
   159  	}
   160  }
   161  
   162  func TestKetamaHashringGet(t *testing.T) {
   163  	baseTS := &prompb.TimeSeries{
   164  		Labels: []labelpb.ZLabel{
   165  			{
   166  				Name:  "pod",
   167  				Value: "nginx",
   168  			},
   169  		},
   170  	}
   171  	tests := []struct {
   172  		name         string
   173  		endpoints    []Endpoint
   174  		expectedNode string
   175  		ts           *prompb.TimeSeries
   176  		n            uint64
   177  	}{
   178  		{
   179  			name:         "base case",
   180  			endpoints:    []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}},
   181  			ts:           baseTS,
   182  			expectedNode: "node-2",
   183  		},
   184  		{
   185  			name:         "base case with replication",
   186  			endpoints:    []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}},
   187  			ts:           baseTS,
   188  			n:            1,
   189  			expectedNode: "node-1",
   190  		},
   191  		{
   192  			name:         "base case with replication",
   193  			endpoints:    []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}},
   194  			ts:           baseTS,
   195  			n:            2,
   196  			expectedNode: "node-3",
   197  		},
   198  		{
   199  			name:         "base case with replication and reordered nodes",
   200  			endpoints:    []Endpoint{{Address: "node-1"}, {Address: "node-3"}, {Address: "node-2"}},
   201  			ts:           baseTS,
   202  			n:            2,
   203  			expectedNode: "node-3",
   204  		},
   205  		{
   206  			name:         "base case with new node at beginning of ring",
   207  			endpoints:    []Endpoint{{Address: "node-0"}, {Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}},
   208  			ts:           baseTS,
   209  			expectedNode: "node-2",
   210  		},
   211  		{
   212  			name:         "base case with new node at end of ring",
   213  			endpoints:    []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}, {Address: "node-4"}},
   214  			ts:           baseTS,
   215  			expectedNode: "node-2",
   216  		},
   217  		{
   218  			name:      "base case with different timeseries",
   219  			endpoints: []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}},
   220  			ts: &prompb.TimeSeries{
   221  				Labels: []labelpb.ZLabel{
   222  					{
   223  						Name:  "pod",
   224  						Value: "thanos",
   225  					},
   226  				},
   227  			},
   228  			expectedNode: "node-3",
   229  		},
   230  	}
   231  
   232  	for _, test := range tests {
   233  		t.Run(test.name, func(t *testing.T) {
   234  			hashRing, err := newKetamaHashring(test.endpoints, 10, test.n+1)
   235  			require.NoError(t, err)
   236  
   237  			result, err := hashRing.GetN("tenant", test.ts, test.n)
   238  			require.NoError(t, err)
   239  			require.Equal(t, test.expectedNode, result)
   240  		})
   241  	}
   242  }
   243  
   244  func TestKetamaHashringBadConfigIsRejected(t *testing.T) {
   245  	_, err := newKetamaHashring([]Endpoint{{Address: "node-1"}}, 1, 2)
   246  	require.Error(t, err)
   247  }
   248  
   249  func TestKetamaHashringConsistency(t *testing.T) {
   250  	series := makeSeries()
   251  
   252  	ringA := []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}}
   253  	a1, err := assignSeries(series, ringA)
   254  	require.NoError(t, err)
   255  
   256  	ringB := []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}}
   257  	a2, err := assignSeries(series, ringB)
   258  	require.NoError(t, err)
   259  
   260  	for node, ts := range a1 {
   261  		require.Len(t, a2[node], len(ts), "node %s has an inconsistent number of series", node)
   262  	}
   263  
   264  	for node, ts := range a2 {
   265  		require.Len(t, a1[node], len(ts), "node %s has an inconsistent number of series", node)
   266  	}
   267  }
   268  
   269  func TestKetamaHashringIncreaseAtEnd(t *testing.T) {
   270  	series := makeSeries()
   271  
   272  	initialRing := []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}}
   273  	initialAssignments, err := assignSeries(series, initialRing)
   274  	require.NoError(t, err)
   275  
   276  	resizedRing := []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}, {Address: "node-4"}, {Address: "node-5"}}
   277  	reassignments, err := assignSeries(series, resizedRing)
   278  	require.NoError(t, err)
   279  
   280  	// Assert that the initial nodes have no new keys after increasing the ring size
   281  	for _, node := range initialRing {
   282  		for _, ts := range reassignments[node.Address] {
   283  			foundInInitialAssignment := findSeries(initialAssignments, node.Address, ts)
   284  			require.True(t, foundInInitialAssignment, "node %s contains new series after resizing", node)
   285  		}
   286  	}
   287  }
   288  
   289  func TestKetamaHashringIncreaseInMiddle(t *testing.T) {
   290  	series := makeSeries()
   291  
   292  	initialRing := []Endpoint{{Address: "node-1"}, {Address: "node-3"}}
   293  	initialAssignments, err := assignSeries(series, initialRing)
   294  	require.NoError(t, err)
   295  
   296  	resizedRing := []Endpoint{{Address: "node-1"}, {Address: "node-2"}, {Address: "node-3"}}
   297  	reassignments, err := assignSeries(series, resizedRing)
   298  	require.NoError(t, err)
   299  
   300  	// Assert that the initial nodes have no new keys after increasing the ring size
   301  	for _, node := range initialRing {
   302  		for _, ts := range reassignments[node.Address] {
   303  			foundInInitialAssignment := findSeries(initialAssignments, node.Address, ts)
   304  			require.True(t, foundInInitialAssignment, "node %s contains new series after resizing", node)
   305  		}
   306  	}
   307  }
   308  
   309  func TestKetamaHashringReplicationConsistency(t *testing.T) {
   310  	series := makeSeries()
   311  
   312  	initialRing := []Endpoint{{Address: "node-1"}, {Address: "node-4"}, {Address: "node-5"}}
   313  	initialAssignments, err := assignReplicatedSeries(series, initialRing, 2)
   314  	require.NoError(t, err)
   315  
   316  	resizedRing := []Endpoint{{Address: "node-4"}, {Address: "node-3"}, {Address: "node-1"}, {Address: "node-2"}, {Address: "node-5"}}
   317  	reassignments, err := assignReplicatedSeries(series, resizedRing, 2)
   318  	require.NoError(t, err)
   319  
   320  	// Assert that the initial nodes have no new keys after increasing the ring size
   321  	for _, node := range initialRing {
   322  		for _, ts := range reassignments[node.Address] {
   323  			foundInInitialAssignment := findSeries(initialAssignments, node.Address, ts)
   324  			require.True(t, foundInInitialAssignment, "node %s contains new series after resizing", node)
   325  		}
   326  	}
   327  }
   328  
   329  func TestKetamaHashringReplicationConsistencyWithAZs(t *testing.T) {
   330  	for _, tt := range []struct {
   331  		initialRing []Endpoint
   332  		resizedRing []Endpoint
   333  		replicas    uint64
   334  	}{
   335  		{
   336  			initialRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}},
   337  			resizedRing: []Endpoint{{Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}, {Address: "a", AZ: "1"}, {Address: "d", AZ: "2"}, {Address: "e", AZ: "4"}},
   338  			replicas:    3,
   339  		},
   340  		{
   341  			initialRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}},
   342  			resizedRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}, {Address: "d", AZ: "1"}, {Address: "e", AZ: "2"}, {Address: "f", AZ: "3"}},
   343  			replicas:    3,
   344  		},
   345  		{
   346  			initialRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}},
   347  			resizedRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}, {Address: "d", AZ: "4"}, {Address: "e", AZ: "5"}, {Address: "f", AZ: "6"}},
   348  			replicas:    3,
   349  		},
   350  		{
   351  			initialRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}},
   352  			resizedRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}, {Address: "c", AZ: "3"}, {Address: "d", AZ: "4"}, {Address: "e", AZ: "5"}, {Address: "f", AZ: "6"}},
   353  			replicas:    2,
   354  		},
   355  		{
   356  			initialRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "c", AZ: "2"}, {Address: "f", AZ: "3"}},
   357  			resizedRing: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "1"}, {Address: "c", AZ: "2"}, {Address: "d", AZ: "2"}, {Address: "f", AZ: "3"}},
   358  			replicas:    2,
   359  		},
   360  	} {
   361  		t.Run("", func(t *testing.T) {
   362  			series := makeSeries()
   363  
   364  			initialAssignments, err := assignReplicatedSeries(series, tt.initialRing, tt.replicas)
   365  			require.NoError(t, err)
   366  
   367  			reassignments, err := assignReplicatedSeries(series, tt.resizedRing, tt.replicas)
   368  			require.NoError(t, err)
   369  
   370  			// Assert that the initial nodes have no new keys after increasing the ring size
   371  			for _, node := range tt.initialRing {
   372  				for _, ts := range reassignments[node.Address] {
   373  					foundInInitialAssignment := findSeries(initialAssignments, node.Address, ts)
   374  					require.True(t, foundInInitialAssignment, "node %s contains new series after resizing", node)
   375  				}
   376  			}
   377  		})
   378  	}
   379  }
   380  
   381  func TestKetamaHashringEvenAZSpread(t *testing.T) {
   382  	tenant := "default-tenant"
   383  	ts := &prompb.TimeSeries{
   384  		Labels:  labelpb.ZLabelsFromPromLabels(labels.FromStrings("foo", "bar")),
   385  		Samples: []prompb.Sample{{Value: 1, Timestamp: 0}},
   386  	}
   387  
   388  	for _, tt := range []struct {
   389  		nodes    []Endpoint
   390  		replicas uint64
   391  	}{
   392  		{
   393  			nodes: []Endpoint{
   394  				{Address: "a", AZ: "1"},
   395  				{Address: "b", AZ: "2"},
   396  				{Address: "c", AZ: "1"},
   397  				{Address: "d", AZ: "2"},
   398  			},
   399  			replicas: 1,
   400  		},
   401  		{
   402  			nodes:    []Endpoint{{Address: "a"}, {Address: "b"}, {Address: "c"}, {Address: "d"}},
   403  			replicas: 1,
   404  		},
   405  		{
   406  			nodes: []Endpoint{
   407  				{Address: "a", AZ: "1"},
   408  				{Address: "b", AZ: "2"},
   409  				{Address: "c", AZ: "1"},
   410  				{Address: "d", AZ: "2"},
   411  			},
   412  			replicas: 2,
   413  		},
   414  		{
   415  			nodes: []Endpoint{
   416  				{Address: "a", AZ: "1"},
   417  				{Address: "b", AZ: "2"},
   418  				{Address: "c", AZ: "3"},
   419  				{Address: "d", AZ: "1"},
   420  				{Address: "e", AZ: "2"},
   421  				{Address: "f", AZ: "3"},
   422  			},
   423  			replicas: 3,
   424  		},
   425  		{
   426  			nodes:    []Endpoint{{Address: "a"}, {Address: "b"}, {Address: "c"}, {Address: "d"}, {Address: "e"}, {Address: "f"}, {Address: "g"}},
   427  			replicas: 3,
   428  		},
   429  		{
   430  			nodes: []Endpoint{
   431  				{Address: "a", AZ: "1"},
   432  				{Address: "b", AZ: "2"},
   433  				{Address: "c", AZ: "3"},
   434  				{Address: "d", AZ: "1"},
   435  				{Address: "e", AZ: "2"},
   436  				{Address: "f", AZ: "3"},
   437  				{Address: "g", AZ: "4"},
   438  				{Address: "h", AZ: "4"},
   439  				{Address: "i", AZ: "4"},
   440  				{Address: "j", AZ: "5"},
   441  				{Address: "k", AZ: "5"},
   442  				{Address: "l", AZ: "5"},
   443  			},
   444  			replicas: 10,
   445  		},
   446  	} {
   447  		t.Run("", func(t *testing.T) {
   448  			hashRing, err := newKetamaHashring(tt.nodes, SectionsPerNode, tt.replicas)
   449  			testutil.Ok(t, err)
   450  
   451  			availableAzs := make(map[string]int64)
   452  			for _, endpoint := range tt.nodes {
   453  				availableAzs[endpoint.AZ] = 0
   454  			}
   455  
   456  			azSpread := make(map[string]int64)
   457  			for i := 0; i < int(tt.replicas); i++ {
   458  				r, err := hashRing.GetN(tenant, ts, uint64(i))
   459  				testutil.Ok(t, err)
   460  
   461  				for _, n := range tt.nodes {
   462  					if !strings.HasPrefix(n.Address, r) {
   463  						continue
   464  					}
   465  					azSpread[n.AZ]++
   466  				}
   467  
   468  			}
   469  
   470  			expectedAzSpreadLength := int(tt.replicas)
   471  			if int(tt.replicas) > len(availableAzs) {
   472  				expectedAzSpreadLength = len(availableAzs)
   473  			}
   474  			testutil.Equals(t, len(azSpread), expectedAzSpreadLength)
   475  
   476  			for _, writeToAz := range azSpread {
   477  				minAz := sizeOfLeastOccupiedAZ(azSpread)
   478  				testutil.Assert(t, math.Abs(float64(writeToAz-minAz)) <= 1.0)
   479  			}
   480  		})
   481  	}
   482  }
   483  
   484  func TestKetamaHashringEvenNodeSpread(t *testing.T) {
   485  	tenant := "default-tenant"
   486  
   487  	for _, tt := range []struct {
   488  		nodes     []Endpoint
   489  		replicas  uint64
   490  		numSeries uint64
   491  	}{
   492  		{
   493  			nodes: []Endpoint{
   494  				{Address: "a", AZ: "1"},
   495  				{Address: "b", AZ: "2"},
   496  				{Address: "c", AZ: "1"},
   497  				{Address: "d", AZ: "2"},
   498  			},
   499  			replicas:  2,
   500  			numSeries: 1000,
   501  		},
   502  		{
   503  			nodes:     []Endpoint{{Address: "a"}, {Address: "b"}, {Address: "c"}, {Address: "d"}},
   504  			replicas:  2,
   505  			numSeries: 1000,
   506  		},
   507  		{
   508  			nodes: []Endpoint{
   509  				{Address: "a", AZ: "1"},
   510  				{Address: "b", AZ: "2"},
   511  				{Address: "c", AZ: "3"},
   512  				{Address: "d", AZ: "2"},
   513  				{Address: "e", AZ: "1"},
   514  				{Address: "f", AZ: "3"},
   515  			},
   516  			replicas:  3,
   517  			numSeries: 10000,
   518  		},
   519  		{
   520  			nodes: []Endpoint{
   521  				{Address: "a", AZ: "1"},
   522  				{Address: "b", AZ: "2"},
   523  				{Address: "c", AZ: "3"},
   524  				{Address: "d", AZ: "2"},
   525  				{Address: "e", AZ: "1"},
   526  				{Address: "f", AZ: "3"},
   527  				{Address: "g", AZ: "1"},
   528  				{Address: "h", AZ: "2"},
   529  				{Address: "i", AZ: "3"},
   530  			},
   531  			replicas:  2,
   532  			numSeries: 10000,
   533  		},
   534  		{
   535  			nodes: []Endpoint{
   536  				{Address: "a", AZ: "1"},
   537  				{Address: "b", AZ: "2"},
   538  				{Address: "c", AZ: "3"},
   539  				{Address: "d", AZ: "2"},
   540  				{Address: "e", AZ: "1"},
   541  				{Address: "f", AZ: "3"},
   542  				{Address: "g", AZ: "1"},
   543  				{Address: "h", AZ: "2"},
   544  				{Address: "i", AZ: "3"},
   545  			},
   546  			replicas:  9,
   547  			numSeries: 10000,
   548  		},
   549  	} {
   550  		t.Run("", func(t *testing.T) {
   551  			hashRing, err := newKetamaHashring(tt.nodes, SectionsPerNode, tt.replicas)
   552  			testutil.Ok(t, err)
   553  			optimalSpread := int(tt.numSeries*tt.replicas) / len(tt.nodes)
   554  			nodeSpread := make(map[string]int)
   555  			for i := 0; i < int(tt.numSeries); i++ {
   556  				ts := &prompb.TimeSeries{
   557  					Labels:  labelpb.ZLabelsFromPromLabels(labels.FromStrings("foo", fmt.Sprintf("%d", i))),
   558  					Samples: []prompb.Sample{{Value: 1, Timestamp: 0}},
   559  				}
   560  				for j := 0; j < int(tt.replicas); j++ {
   561  					r, err := hashRing.GetN(tenant, ts, uint64(j))
   562  					testutil.Ok(t, err)
   563  
   564  					nodeSpread[r]++
   565  				}
   566  			}
   567  			for _, node := range nodeSpread {
   568  				diff := math.Abs(float64(node) - float64(optimalSpread))
   569  				testutil.Assert(t, diff/float64(optimalSpread) < 0.1)
   570  			}
   571  		})
   572  	}
   573  }
   574  
   575  func TestInvalidAZHashringCfg(t *testing.T) {
   576  	for _, tt := range []struct {
   577  		cfg           []HashringConfig
   578  		replicas      uint64
   579  		algorithm     HashringAlgorithm
   580  		expectedError string
   581  	}{
   582  		{
   583  			cfg:           []HashringConfig{{Endpoints: []Endpoint{{Address: "a", AZ: "1"}, {Address: "b", AZ: "2"}}}},
   584  			replicas:      2,
   585  			expectedError: "Hashmod algorithm does not support AZ aware hashring configuration. Either use Ketama or remove AZ configuration.",
   586  		},
   587  	} {
   588  		t.Run("", func(t *testing.T) {
   589  			_, err := NewMultiHashring(tt.algorithm, tt.replicas, tt.cfg)
   590  			require.EqualError(t, err, tt.expectedError)
   591  		})
   592  	}
   593  }
   594  
   595  func makeSeries() []prompb.TimeSeries {
   596  	numSeries := 10000
   597  	series := make([]prompb.TimeSeries, numSeries)
   598  	for i := 0; i < numSeries; i++ {
   599  		series[i] = prompb.TimeSeries{
   600  			Labels: []labelpb.ZLabel{
   601  				{
   602  					Name:  "pod",
   603  					Value: fmt.Sprintf("nginx-%d", i),
   604  				},
   605  			},
   606  		}
   607  	}
   608  	return series
   609  }
   610  
   611  func findSeries(initialAssignments map[string][]prompb.TimeSeries, node string, newSeries prompb.TimeSeries) bool {
   612  	for _, oldSeries := range initialAssignments[node] {
   613  		l1 := labelpb.ZLabelsToPromLabels(newSeries.Labels)
   614  		l2 := labelpb.ZLabelsToPromLabels(oldSeries.Labels)
   615  		if labels.Equal(l1, l2) {
   616  			return true
   617  		}
   618  	}
   619  
   620  	return false
   621  }
   622  
   623  func assignSeries(series []prompb.TimeSeries, nodes []Endpoint) (map[string][]prompb.TimeSeries, error) {
   624  	return assignReplicatedSeries(series, nodes, 0)
   625  }
   626  
   627  func assignReplicatedSeries(series []prompb.TimeSeries, nodes []Endpoint, replicas uint64) (map[string][]prompb.TimeSeries, error) {
   628  	hashRing, err := newKetamaHashring(nodes, SectionsPerNode, replicas)
   629  	if err != nil {
   630  		return nil, err
   631  	}
   632  	assignments := make(map[string][]prompb.TimeSeries)
   633  	for i := uint64(0); i < replicas; i++ {
   634  		for _, ts := range series {
   635  			result, err := hashRing.GetN("tenant", &ts, i)
   636  			if err != nil {
   637  				return nil, err
   638  			}
   639  			assignments[result] = append(assignments[result], ts)
   640  
   641  		}
   642  	}
   643  
   644  	return assignments, nil
   645  }