github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/environment/config_test.go (about)

     1  // Copyright (c) 2018 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 environment
    22  
    23  import (
    24  	"fmt"
    25  	"math/rand"
    26  	"testing"
    27  	"time"
    28  
    29  	etcdclient "github.com/m3db/m3/src/cluster/client/etcd"
    30  	"github.com/m3db/m3/src/cluster/services"
    31  	"github.com/m3db/m3/src/dbnode/namespace"
    32  	"github.com/m3db/m3/src/dbnode/retention"
    33  	"github.com/m3db/m3/src/dbnode/topology"
    34  	"github.com/m3db/m3/src/x/instrument"
    35  	"github.com/stretchr/testify/assert"
    36  	"github.com/stretchr/testify/require"
    37  	"gopkg.in/yaml.v2"
    38  )
    39  
    40  var initTimeout = time.Minute
    41  
    42  func TestConfigureStatic(t *testing.T) {
    43  	tests := []struct {
    44  		name       string
    45  		staticTopo *topology.StaticConfiguration
    46  		expectErr  bool
    47  	}{
    48  		{
    49  			name: "0 replicas get defaulted to 1",
    50  			staticTopo: &topology.StaticConfiguration{
    51  				Shards:   32,
    52  				Replicas: 0,
    53  				Hosts: []topology.HostShardConfig{
    54  					{
    55  						HostID:        "localhost",
    56  						ListenAddress: "0.0.0.0:1234",
    57  					},
    58  				},
    59  			},
    60  			expectErr: false,
    61  		},
    62  		{
    63  			name: "1 replica, 1 host",
    64  			staticTopo: &topology.StaticConfiguration{
    65  				Shards:   32,
    66  				Replicas: 1,
    67  				Hosts: []topology.HostShardConfig{
    68  					{
    69  						HostID:        "localhost",
    70  						ListenAddress: "0.0.0.0:1234",
    71  					},
    72  				},
    73  			},
    74  			expectErr: false,
    75  		},
    76  		{
    77  			name: "1 replica, 3 hosts",
    78  			staticTopo: &topology.StaticConfiguration{
    79  				Shards:   32,
    80  				Replicas: 1,
    81  				Hosts: []topology.HostShardConfig{
    82  					{
    83  						HostID:        "host0",
    84  						ListenAddress: "0.0.0.0:1000",
    85  					},
    86  					{
    87  						HostID:        "host1",
    88  						ListenAddress: "0.0.0.0:1001",
    89  					},
    90  					{
    91  						HostID:        "host2",
    92  						ListenAddress: "0.0.0.0:1002",
    93  					},
    94  				},
    95  			},
    96  			expectErr: false,
    97  		},
    98  		{
    99  			name: "3 replicas, 3 hosts",
   100  			staticTopo: &topology.StaticConfiguration{
   101  				Shards:   32,
   102  				Replicas: 3,
   103  				Hosts: []topology.HostShardConfig{
   104  					{
   105  						HostID:        "host0",
   106  						ListenAddress: "0.0.0.0:1000",
   107  					},
   108  					{
   109  						HostID:        "host1",
   110  						ListenAddress: "0.0.0.0:1001",
   111  					},
   112  					{
   113  						HostID:        "host2",
   114  						ListenAddress: "0.0.0.0:1002",
   115  					},
   116  				},
   117  			},
   118  			expectErr: false,
   119  		},
   120  		{
   121  			name: "3 replicas, 5 hosts",
   122  			staticTopo: &topology.StaticConfiguration{
   123  				Shards:   32,
   124  				Replicas: 3,
   125  				Hosts: []topology.HostShardConfig{
   126  					{
   127  						HostID:        "host0",
   128  						ListenAddress: "0.0.0.0:1000",
   129  					},
   130  					{
   131  						HostID:        "host1",
   132  						ListenAddress: "0.0.0.0:1001",
   133  					},
   134  					{
   135  						HostID:        "host2",
   136  						ListenAddress: "0.0.0.0:1002",
   137  					},
   138  					{
   139  						HostID:        "host3",
   140  						ListenAddress: "0.0.0.0:1003",
   141  					},
   142  					{
   143  						HostID:        "host4",
   144  						ListenAddress: "0.0.0.0:1004",
   145  					},
   146  				},
   147  			},
   148  			expectErr: false,
   149  		},
   150  		{
   151  			name: "invalid: replicas > hosts",
   152  			staticTopo: &topology.StaticConfiguration{
   153  				Shards:   32,
   154  				Replicas: 3,
   155  				Hosts: []topology.HostShardConfig{
   156  					{
   157  						HostID:        "host0",
   158  						ListenAddress: "0.0.0.0:1000",
   159  					},
   160  				},
   161  			},
   162  			expectErr: true,
   163  		},
   164  	}
   165  
   166  	for _, test := range tests {
   167  		t.Run(test.name, func(t *testing.T) {
   168  			config := Configuration{
   169  				Statics: StaticConfiguration{
   170  					&StaticCluster{
   171  						Namespaces: []namespace.MetadataConfiguration{
   172  							{
   173  								ID: "metrics",
   174  								Retention: retention.Configuration{
   175  									RetentionPeriod: 24 * time.Hour,
   176  									BlockSize:       time.Hour,
   177  								},
   178  							},
   179  							{
   180  								ID: "other-metrics",
   181  								Retention: retention.Configuration{
   182  									RetentionPeriod: 24 * time.Hour,
   183  									BlockSize:       time.Hour,
   184  								},
   185  							},
   186  						},
   187  						ListenAddress:  "0.0.0.0:9000",
   188  						TopologyConfig: test.staticTopo,
   189  					},
   190  				},
   191  			}
   192  
   193  			configRes, err := config.Configure(ConfigurationParameters{})
   194  			if test.expectErr {
   195  				require.Error(t, err)
   196  				return
   197  			}
   198  
   199  			require.NoError(t, err)
   200  			require.NotNil(t, configRes)
   201  		})
   202  	}
   203  }
   204  
   205  func TestGeneratePlacement(t *testing.T) {
   206  	tests := []struct {
   207  		name      string
   208  		numHosts  int
   209  		numShards int
   210  		rf        int
   211  		expectErr bool
   212  	}{
   213  		{
   214  			name:      "1 host, 1 rf",
   215  			numHosts:  1,
   216  			numShards: 16,
   217  			rf:        1,
   218  			expectErr: false,
   219  		},
   220  		{
   221  			name:      "3 hosts, 1 rf",
   222  			numHosts:  3,
   223  			numShards: 16,
   224  			rf:        1,
   225  			expectErr: false,
   226  		},
   227  		{
   228  			name:      "3 hosts, 1 rf with more shards",
   229  			numHosts:  3,
   230  			numShards: 32,
   231  			rf:        1,
   232  			expectErr: false,
   233  		},
   234  		{
   235  			name:      "3 hosts, 3 rf",
   236  			numHosts:  3,
   237  			numShards: 16,
   238  			rf:        3,
   239  			expectErr: false,
   240  		},
   241  		{
   242  			name:      "5 hosts, 3 rf",
   243  			numHosts:  5,
   244  			numShards: 16,
   245  			rf:        3,
   246  			expectErr: false,
   247  		},
   248  		{
   249  			name:      "prod-like cluster",
   250  			numHosts:  100,
   251  			numShards: 4096,
   252  			rf:        3,
   253  			expectErr: false,
   254  		},
   255  		{
   256  			name:      "invalid: hosts < rf",
   257  			numHosts:  2,
   258  			numShards: 16,
   259  			rf:        3,
   260  			expectErr: true,
   261  		},
   262  		{
   263  			name:      "invalid: hosts < rf 2",
   264  			numHosts:  10,
   265  			numShards: 16,
   266  			rf:        11,
   267  			expectErr: true,
   268  		},
   269  		{
   270  			name:      "invalid: no hosts",
   271  			numHosts:  0,
   272  			numShards: 16,
   273  			rf:        3,
   274  			expectErr: true,
   275  		},
   276  		{
   277  			name:      "invalid: 0 rf",
   278  			numHosts:  10,
   279  			numShards: 16,
   280  			rf:        0,
   281  			expectErr: true,
   282  		},
   283  		{
   284  			name:      "invalid: no shards",
   285  			numHosts:  10,
   286  			numShards: 0,
   287  			rf:        3,
   288  			expectErr: true,
   289  		},
   290  	}
   291  
   292  	for _, test := range tests {
   293  		t.Run(test.name, func(t *testing.T) {
   294  			var hosts []topology.HostShardConfig
   295  			for i := 0; i < test.numHosts; i++ {
   296  				hosts = append(hosts, topology.HostShardConfig{
   297  					HostID:        fmt.Sprintf("id%d", i),
   298  					ListenAddress: fmt.Sprintf("id%d", i),
   299  				})
   300  			}
   301  
   302  			hostShardSets, err := generatePlacement(hosts, test.numShards, test.rf)
   303  			if test.expectErr {
   304  				require.Error(t, err)
   305  				return
   306  			}
   307  			require.NoError(t, err)
   308  
   309  			var (
   310  				minShardCount = test.numShards + 1
   311  				maxShardCount = 0
   312  				shardCounts   = make([]int, test.numShards)
   313  			)
   314  			for _, hostShardSet := range hostShardSets {
   315  				ids := hostShardSet.ShardSet().AllIDs()
   316  				if len(ids) < minShardCount {
   317  					minShardCount = len(ids)
   318  				}
   319  				if len(ids) > maxShardCount {
   320  					maxShardCount = len(ids)
   321  				}
   322  
   323  				for _, id := range ids {
   324  					shardCounts[id]++
   325  				}
   326  			}
   327  
   328  			// Assert balanced shard distribution
   329  			assert.True(t, maxShardCount-minShardCount < 2)
   330  			// Assert each shard has `rf` replicas
   331  			for _, shardCount := range shardCounts {
   332  				assert.Equal(t, test.rf, shardCount)
   333  			}
   334  		})
   335  	}
   336  }
   337  
   338  func TestGeneratePlacementConsistency(t *testing.T) {
   339  	// Asserts that the placement generated by `generatePlacement` is
   340  	// deterministic even when the ordering of hosts passed in is in a
   341  	// different order.
   342  	var (
   343  		numHosts  = 123
   344  		numShards = 4096
   345  		rf        = 3
   346  		iters     = 10
   347  		hosts     = make([]topology.HostShardConfig, 0, numHosts)
   348  	)
   349  
   350  	for i := 0; i < numHosts; i++ {
   351  		hosts = append(hosts, topology.HostShardConfig{
   352  			HostID:        fmt.Sprintf("id%d", i),
   353  			ListenAddress: fmt.Sprintf("id%d", i),
   354  		})
   355  	}
   356  
   357  	var pl []topology.HostShardSet
   358  	for i := 0; i < iters; i++ {
   359  		rand.Shuffle(len(hosts), func(i, j int) {
   360  			hosts[i], hosts[j] = hosts[j], hosts[i]
   361  		})
   362  
   363  		hostShardSets, err := generatePlacement(hosts, numShards, rf)
   364  		require.NoError(t, err)
   365  
   366  		if i == 0 {
   367  			pl = hostShardSets
   368  		} else {
   369  			assertHostShardSetsEqual(t, pl, hostShardSets)
   370  		}
   371  	}
   372  }
   373  
   374  // assertHostShardSetsEqual asserts that two HostShardSets are semantically
   375  // equal. Mandates that the two HostShardSets are in the same order too.
   376  func assertHostShardSetsEqual(t *testing.T, one, two []topology.HostShardSet) {
   377  	require.Equal(t, len(one), len(two))
   378  
   379  	for i := range one {
   380  		oneHost := one[i].Host()
   381  		twoHost := two[i].Host()
   382  		assert.Equal(t, oneHost.ID(), twoHost.ID())
   383  		assert.Equal(t, oneHost.Address(), twoHost.Address())
   384  
   385  		oneIDs := one[i].ShardSet().All()
   386  		twoIDs := two[i].ShardSet().All()
   387  		require.Equal(t, len(oneIDs), len(twoIDs))
   388  		for j := range oneIDs {
   389  			assert.True(t, oneIDs[j].Equals(twoIDs[j]))
   390  		}
   391  	}
   392  }
   393  
   394  func TestConfigureDynamic(t *testing.T) {
   395  	config := Configuration{
   396  		Services: DynamicConfiguration{
   397  			&DynamicCluster{
   398  				Service: &etcdclient.Configuration{
   399  					Zone:     "local",
   400  					Env:      "test",
   401  					Service:  "m3dbnode_test",
   402  					CacheDir: "/",
   403  					ETCDClusters: []etcdclient.ClusterConfig{
   404  						{
   405  							Zone:      "local",
   406  							Endpoints: []string{"localhost:1111"},
   407  						},
   408  					},
   409  					SDConfig: services.Configuration{
   410  						InitTimeout: &initTimeout,
   411  					},
   412  				},
   413  			},
   414  		},
   415  	}
   416  
   417  	cfgParams := ConfigurationParameters{
   418  		InstrumentOpts: instrument.NewOptions(),
   419  	}
   420  
   421  	configRes, err := config.Configure(cfgParams)
   422  	assert.NotNil(t, configRes)
   423  	assert.NoError(t, err)
   424  }
   425  
   426  func TestUnmarshalDynamicSingle(t *testing.T) {
   427  	in := `
   428  service:
   429    zone: dca8
   430    env: test
   431  `
   432  
   433  	var cfg Configuration
   434  	err := yaml.Unmarshal([]byte(in), &cfg)
   435  	assert.NoError(t, err)
   436  	assert.NoError(t, cfg.Validate())
   437  	assert.Len(t, cfg.Services, 1)
   438  }
   439  
   440  func TestUnmarshalDynamicList(t *testing.T) {
   441  	in := `
   442  services:
   443    - service:
   444        zone: dca8
   445        env: test
   446    - service:
   447        zone: phx3
   448        env: test
   449      async: true
   450  `
   451  
   452  	var cfg Configuration
   453  	err := yaml.Unmarshal([]byte(in), &cfg)
   454  	assert.NoError(t, err)
   455  	assert.NoError(t, cfg.Validate())
   456  	assert.Len(t, cfg.Services, 2)
   457  }
   458  
   459  var configValidationTests = []struct {
   460  	name      string
   461  	in        string
   462  	expectErr error
   463  }{
   464  	{
   465  		name:      "empty config",
   466  		in:        ``,
   467  		expectErr: errInvalidConfig,
   468  	},
   469  	{
   470  		name: "static and dynamic",
   471  		in: `
   472  services:
   473    - service:
   474        zone: dca8
   475        env: test
   476  statics:
   477    - listenAddress: 0.0.0.0:9000`,
   478  		expectErr: errInvalidConfig,
   479  	},
   480  	{
   481  		name: "invalid dynamic config",
   482  		in: `
   483  services:
   484    - async: true`,
   485  		expectErr: errInvalidSyncCount,
   486  	},
   487  	{
   488  		name: "invalid static config",
   489  		in: `
   490  statics:
   491    - async: true`,
   492  		expectErr: errInvalidSyncCount,
   493  	},
   494  	{
   495  		name: "valid config",
   496  		in: `
   497  services:
   498    - service:
   499        zone: dca8
   500        env: test
   501    - service:
   502        zone: phx3
   503        env: test
   504      async: true`,
   505  		expectErr: nil,
   506  	},
   507  }
   508  
   509  func TestConfigValidation(t *testing.T) {
   510  	for _, tt := range configValidationTests {
   511  		var cfg Configuration
   512  		err := yaml.Unmarshal([]byte(tt.in), &cfg)
   513  		assert.NoError(t, err)
   514  		assert.Equal(t, tt.expectErr, cfg.Validate())
   515  	}
   516  }