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 }