github.com/weaviate/weaviate@v1.24.6/usecases/sharding/state_test.go (about) 1 // _ _ 2 // __ _____ __ ___ ___ __ _| |_ ___ 3 // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \ 4 // \ V V / __/ (_| |\ V /| | (_| | || __/ 5 // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___| 6 // 7 // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved. 8 // 9 // CONTACT: hello@weaviate.io 10 // 11 12 package sharding 13 14 import ( 15 "crypto/rand" 16 "encoding/json" 17 "fmt" 18 "testing" 19 20 "github.com/stretchr/testify/assert" 21 "github.com/stretchr/testify/require" 22 "github.com/weaviate/weaviate/entities/models" 23 ) 24 25 func TestState(t *testing.T) { 26 size := 1000 27 28 cfg, err := ParseConfig(map[string]interface{}{"desiredCount": float64(4)}, 14) 29 require.Nil(t, err) 30 31 nodes := fakeNodes{[]string{"node1", "node2"}} 32 state, err := InitState("my-index", cfg, nodes, 1, false) 33 require.Nil(t, err) 34 35 physicalCount := map[string]int{} 36 var names [][]byte 37 38 for i := 0; i < size; i++ { 39 name := make([]byte, 16) 40 rand.Read(name) 41 names = append(names, name) 42 43 phid := state.PhysicalShard(name) 44 physicalCount[phid]++ 45 } 46 47 // verify each shard contains at least 15% of data. The expected value would 48 // be 25%, but since this is random, we should take a lower value to reduce 49 // flakyness 50 51 for name, count := range physicalCount { 52 if owns := float64(count) / float64(size); owns < 0.15 { 53 t.Errorf("expected shard %q to own at least 15%%, but it only owns %f", name, owns) 54 } 55 } 56 57 // Marshal and recreate, verify results 58 bytes, err := state.JSON() 59 require.Nil(t, err) 60 61 // destroy old version 62 state = nil 63 64 stateReloaded, err := StateFromJSON(bytes, nodes) 65 require.Nil(t, err) 66 67 physicalCountReloaded := map[string]int{} 68 69 // hash the same values again and verify the counts are exactly the same 70 for _, name := range names { 71 phid := stateReloaded.PhysicalShard(name) 72 physicalCountReloaded[phid]++ 73 } 74 75 assert.Equal(t, physicalCount, physicalCountReloaded) 76 } 77 78 type fakeNodes struct { 79 nodes []string 80 } 81 82 func (f fakeNodes) Candidates() []string { 83 return f.nodes 84 } 85 86 func (f fakeNodes) LocalName() string { 87 return f.nodes[0] 88 } 89 90 func TestInitState(t *testing.T) { 91 type test struct { 92 nodes []string 93 replicationFactor int 94 shards int 95 ok bool 96 } 97 98 // this tests asserts that nodes are assigned evenly with various 99 // combinations. 100 101 tests := []test{ 102 { 103 nodes: []string{"node1", "node2", "node3"}, 104 replicationFactor: 1, 105 shards: 3, 106 ok: true, 107 }, 108 { 109 nodes: []string{"node1", "node2", "node3"}, 110 replicationFactor: 2, 111 shards: 3, 112 ok: true, 113 }, 114 { 115 nodes: []string{"node1", "node2", "node3"}, 116 replicationFactor: 3, 117 shards: 1, 118 ok: true, 119 }, 120 { 121 nodes: []string{"node1", "node2", "node3"}, 122 replicationFactor: 3, 123 shards: 3, 124 ok: true, 125 }, 126 { 127 nodes: []string{"node1", "node2", "node3"}, 128 replicationFactor: 3, 129 shards: 2, 130 ok: true, 131 }, 132 { 133 nodes: []string{"node1", "node2", "node3", "node4", "node5", "node6"}, 134 replicationFactor: 4, 135 shards: 6, 136 ok: true, 137 }, 138 { 139 nodes: []string{"node1", "node2"}, 140 replicationFactor: 4, 141 shards: 4, 142 ok: false, 143 }, 144 { 145 nodes: []string{"node1", "node2", "node3", "node4", "node5", "node6", "node7", "node8", "node9", "node10", "node11", "node12"}, 146 replicationFactor: 3, 147 shards: 4, 148 ok: true, 149 }, 150 } 151 152 for _, test := range tests { 153 t.Run(fmt.Sprintf("Shards=%d_RF=%d", test.shards, test.replicationFactor), 154 func(t *testing.T) { 155 nodes := fakeNodes{test.nodes} 156 cfg, err := ParseConfig(map[string]interface{}{ 157 "desiredCount": float64(test.shards), 158 "replicas": float64(test.replicationFactor), 159 }, 3) 160 require.Nil(t, err) 161 162 state, err := InitState("my-index", cfg, nodes, int64(test.replicationFactor), false) 163 if !test.ok { 164 require.NotNil(t, err) 165 return 166 } 167 require.Nil(t, err) 168 169 nodeCounter := map[string]int{} 170 actual := 0 171 for _, shard := range state.Physical { 172 for _, node := range shard.BelongsToNodes { 173 nodeCounter[node]++ 174 actual++ 175 } 176 } 177 178 assert.Equal(t, len(nodeCounter), len(test.nodes)) 179 180 // assert that total no of associations is correct 181 desired := test.shards * test.replicationFactor 182 assert.Equal(t, desired, actual, "correct number of node associations") 183 184 // assert that shards are hit evenly 185 expectedAssociations := test.shards * test.replicationFactor / len(test.nodes) 186 for _, count := range nodeCounter { 187 assert.Equal(t, expectedAssociations, count) 188 } 189 }) 190 } 191 } 192 193 func TestAdjustReplicas(t *testing.T) { 194 t.Run("1->3", func(t *testing.T) { 195 nodes := fakeNodes{nodes: []string{"N1", "N2", "N3", "N4", "N5"}} 196 shard := Physical{BelongsToNodes: []string{"N1"}} 197 require.Nil(t, shard.AdjustReplicas(3, nodes)) 198 assert.ElementsMatch(t, []string{"N1", "N2", "N3"}, shard.BelongsToNodes) 199 }) 200 201 t.Run("2->3", func(t *testing.T) { 202 nodes := fakeNodes{nodes: []string{"N1", "N2", "N3", "N4", "N5"}} 203 shard := Physical{BelongsToNodes: []string{"N2", "N3"}} 204 require.Nil(t, shard.AdjustReplicas(3, nodes)) 205 assert.ElementsMatch(t, []string{"N1", "N2", "N3"}, shard.BelongsToNodes) 206 }) 207 208 t.Run("3->3", func(t *testing.T) { 209 nodes := fakeNodes{nodes: []string{"N1", "N2", "N3", "N4", "N5"}} 210 shard := Physical{BelongsToNodes: []string{"N1", "N2", "N3"}} 211 require.Nil(t, shard.AdjustReplicas(3, nodes)) 212 assert.ElementsMatch(t, []string{"N1", "N2", "N3"}, shard.BelongsToNodes) 213 }) 214 215 t.Run("3->2", func(t *testing.T) { 216 nodes := fakeNodes{nodes: []string{"N1", "N2", "N3"}} 217 shard := Physical{BelongsToNodes: []string{"N1", "N2", "N3"}} 218 require.Nil(t, shard.AdjustReplicas(2, nodes)) 219 assert.ElementsMatch(t, []string{"N1", "N2"}, shard.BelongsToNodes) 220 }) 221 222 t.Run("Min", func(t *testing.T) { 223 nodes := fakeNodes{nodes: []string{"N1", "N2", "N3"}} 224 shard := Physical{BelongsToNodes: []string{"N1", "N2", "N3"}} 225 require.NotNil(t, shard.AdjustReplicas(-1, nodes)) 226 }) 227 t.Run("Max", func(t *testing.T) { 228 nodes := fakeNodes{nodes: []string{"N1", "N2", "N3"}} 229 shard := Physical{BelongsToNodes: []string{"N1", "N2", "N3"}} 230 require.NotNil(t, shard.AdjustReplicas(4, nodes)) 231 }) 232 t.Run("Bug", func(t *testing.T) { 233 names := []string{"N1", "N2", "N3", "N4"} 234 nodes := fakeNodes{nodes: names} // bug 235 shard := Physical{BelongsToNodes: []string{"N1", "N1", "N1", "N2", "N2"}} 236 require.Nil(t, shard.AdjustReplicas(4, nodes)) // correct 237 require.ElementsMatch(t, names, shard.BelongsToNodes) 238 }) 239 } 240 241 func TestGetPartitions(t *testing.T) { 242 t.Run("EmptyCandidatesList", func(t *testing.T) { 243 // nodes := fakeNodes{nodes: []string{"N1", "N2", "N3", "N4", "N5"}} 244 shards := []string{"H1"} 245 state := State{} 246 partitions, err := state.GetPartitions(fakeNodes{}, shards, 1) 247 require.Nil(t, partitions) 248 require.ErrorContains(t, err, "empty") 249 }) 250 t.Run("NotEnoughReplicas", func(t *testing.T) { 251 shards := []string{"H1"} 252 state := State{} 253 partitions, err := state.GetPartitions(fakeNodes{nodes: []string{"N1"}}, shards, 2) 254 require.Nil(t, partitions) 255 require.ErrorContains(t, err, "not enough replicas") 256 }) 257 t.Run("Success/RF3", func(t *testing.T) { 258 nodes := fakeNodes{nodes: []string{"N1", "N2", "N3"}} 259 shards := []string{"H1", "H2", "H3", "H4", "H5"} 260 state := State{} 261 got, err := state.GetPartitions(nodes, shards, 3) 262 require.Nil(t, err) 263 want := map[string][]string{ 264 "H1": {"N1", "N2", "N3"}, 265 "H2": {"N2", "N3", "N1"}, 266 "H3": {"N3", "N1", "N2"}, 267 "H4": {"N3", "N1", "N2"}, 268 "H5": {"N1", "N2", "N3"}, 269 } 270 require.Equal(t, want, got) 271 }) 272 273 t.Run("Success/RF2", func(t *testing.T) { 274 nodes := fakeNodes{nodes: []string{"N1", "N2", "N3", "N4", "N5", "N6", "N7"}} 275 shards := []string{"H1", "H2", "H3", "H4", "H5"} 276 state := State{} 277 got, err := state.GetPartitions(nodes, shards, 2) 278 require.Nil(t, err) 279 want := map[string][]string{ 280 "H1": {"N1", "N2"}, 281 "H2": {"N3", "N4"}, 282 "H3": {"N5", "N6"}, 283 "H4": {"N7", "N1"}, 284 "H5": {"N2", "N3"}, 285 } 286 require.Equal(t, want, got) 287 }) 288 } 289 290 func TestAddPartition(t *testing.T) { 291 var ( 292 nodes1 = []string{"N", "M"} 293 nodes2 = []string{"L", "M", "O"} 294 ) 295 cfg, err := ParseConfig(map[string]interface{}{"desiredCount": float64(4)}, 14) 296 require.Nil(t, err) 297 298 nodes := fakeNodes{[]string{"node1", "node2"}} 299 s, err := InitState("my-index", cfg, nodes, 1, true) 300 require.Nil(t, err) 301 302 s.AddPartition("A", nodes1, models.TenantActivityStatusHOT) 303 s.AddPartition("B", nodes2, models.TenantActivityStatusCOLD) 304 305 want := map[string]Physical{ 306 "A": {Name: "A", BelongsToNodes: nodes1, OwnsPercentage: 1, Status: models.TenantActivityStatusHOT}, 307 "B": {Name: "B", BelongsToNodes: nodes2, OwnsPercentage: 1, Status: models.TenantActivityStatusCOLD}, 308 } 309 require.Equal(t, want, s.Physical) 310 } 311 312 func TestStateDeepCopy(t *testing.T) { 313 original := State{ 314 IndexID: "original", 315 Config: Config{ 316 VirtualPerPhysical: 1, 317 DesiredCount: 2, 318 ActualCount: 3, 319 DesiredVirtualCount: 4, 320 ActualVirtualCount: 5, 321 Key: "original", 322 Strategy: "original", 323 Function: "original", 324 }, 325 localNodeName: "original", 326 Physical: map[string]Physical{ 327 "physical1": { 328 Name: "original", 329 OwnsVirtual: []string{"original"}, 330 OwnsPercentage: 7, 331 BelongsToNodes: []string{"original"}, 332 Status: models.TenantActivityStatusHOT, 333 }, 334 }, 335 Virtual: []Virtual{ 336 { 337 Name: "original", 338 Upper: 8, 339 OwnsPercentage: 9, 340 AssignedToPhysical: "original", 341 }, 342 }, 343 } 344 345 control := State{ 346 IndexID: "original", 347 Config: Config{ 348 VirtualPerPhysical: 1, 349 DesiredCount: 2, 350 ActualCount: 3, 351 DesiredVirtualCount: 4, 352 ActualVirtualCount: 5, 353 Key: "original", 354 Strategy: "original", 355 Function: "original", 356 }, 357 localNodeName: "original", 358 Physical: map[string]Physical{ 359 "physical1": { 360 Name: "original", 361 OwnsVirtual: []string{"original"}, 362 OwnsPercentage: 7, 363 BelongsToNodes: []string{"original"}, 364 Status: models.TenantActivityStatusHOT, 365 }, 366 }, 367 Virtual: []Virtual{ 368 { 369 Name: "original", 370 Upper: 8, 371 OwnsPercentage: 9, 372 AssignedToPhysical: "original", 373 }, 374 }, 375 } 376 377 assert.Equal(t, control, original, "control matches initially") 378 379 copied := original.DeepCopy() 380 assert.Equal(t, control, copied, "copy matches original") 381 382 // modify literally every field 383 copied.localNodeName = "changed" 384 copied.IndexID = "changed" 385 copied.Config.VirtualPerPhysical = 11 386 copied.Config.DesiredCount = 22 387 copied.Config.ActualCount = 33 388 copied.Config.DesiredVirtualCount = 44 389 copied.Config.ActualVirtualCount = 55 390 copied.Config.Key = "changed" 391 copied.Config.Strategy = "changed" 392 copied.Config.Function = "changed" 393 physical1 := copied.Physical["physical1"] 394 physical1.Name = "changed" 395 physical1.BelongsToNodes = append(physical1.BelongsToNodes, "changed") 396 physical1.OwnsPercentage = 100 397 physical1.OwnsVirtual = append(physical1.OwnsVirtual, "changed") 398 physical1.Status = models.TenantActivityStatusCOLD 399 copied.Physical["physical1"] = physical1 400 copied.Physical["physical2"] = Physical{} 401 copied.Virtual[0].Name = "original" 402 copied.Virtual[0].Upper = 8 403 copied.Virtual[0].OwnsPercentage = 9 404 copied.Virtual[0].AssignedToPhysical = "original" 405 copied.Virtual = append(copied.Virtual, Virtual{}) 406 407 assert.Equal(t, control, original, "original still matches control even with changes in copy") 408 } 409 410 func TestBackwardCompatibilityBefore1_17(t *testing.T) { 411 // As part of v1.17, replication is introduced and the structure of the 412 // physical shard is slightly changed. Instead of `belongsToNode string`, the 413 // association is now `belongsToNodes []string`. A migration helper was 414 // introduced to make sure we're backward compatible. 415 416 oldVersion := State{ 417 Physical: map[string]Physical{ 418 "hello-replication": { 419 Name: "hello-replication", 420 LegacyBelongsToNodeForBackwardCompat: "the-best-node", 421 }, 422 }, 423 } 424 oldVersionJSON, err := json.Marshal(oldVersion) 425 require.Nil(t, err) 426 427 var newVersion State 428 err = json.Unmarshal(oldVersionJSON, &newVersion) 429 require.Nil(t, err) 430 431 newVersion.MigrateFromOldFormat() 432 433 assert.Equal(t, []string{"the-best-node"}, 434 newVersion.Physical["hello-replication"].BelongsToNodes) 435 } 436 437 func TestApplyNodeMapping(t *testing.T) { 438 type test struct { 439 name string 440 state State 441 control State 442 nodeMapping map[string]string 443 } 444 445 tests := []test{ 446 { 447 name: "no mapping", 448 state: State{ 449 Physical: map[string]Physical{ 450 "hello-node-mapping": { 451 Name: "hello-node-mapping", 452 LegacyBelongsToNodeForBackwardCompat: "node1", 453 BelongsToNodes: []string{"node1"}, 454 }, 455 }, 456 }, 457 control: State{ 458 Physical: map[string]Physical{ 459 "hello-node-mapping": { 460 Name: "hello-node-mapping", 461 LegacyBelongsToNodeForBackwardCompat: "node1", 462 BelongsToNodes: []string{"node1"}, 463 }, 464 }, 465 }, 466 }, 467 { 468 name: "map one node", 469 state: State{ 470 Physical: map[string]Physical{ 471 "hello-node-mapping": { 472 Name: "hello-node-mapping", 473 LegacyBelongsToNodeForBackwardCompat: "node1", 474 BelongsToNodes: []string{"node1"}, 475 }, 476 }, 477 }, 478 control: State{ 479 Physical: map[string]Physical{ 480 "hello-node-mapping": { 481 Name: "hello-node-mapping", 482 LegacyBelongsToNodeForBackwardCompat: "new-node1", 483 BelongsToNodes: []string{"new-node1"}, 484 }, 485 }, 486 }, 487 nodeMapping: map[string]string{"node1": "new-node1"}, 488 }, 489 { 490 name: "map multiple nodes", 491 state: State{ 492 Physical: map[string]Physical{ 493 "hello-node-mapping": { 494 Name: "hello-node-mapping", 495 LegacyBelongsToNodeForBackwardCompat: "node1", 496 BelongsToNodes: []string{"node1", "node2"}, 497 }, 498 }, 499 }, 500 control: State{ 501 Physical: map[string]Physical{ 502 "hello-node-mapping": { 503 Name: "hello-node-mapping", 504 LegacyBelongsToNodeForBackwardCompat: "new-node1", 505 BelongsToNodes: []string{"new-node1", "new-node2"}, 506 }, 507 }, 508 }, 509 nodeMapping: map[string]string{"node1": "new-node1", "node2": "new-node2"}, 510 }, 511 { 512 name: "map multiple nodes with exceptions", 513 state: State{ 514 Physical: map[string]Physical{ 515 "hello-node-mapping": { 516 Name: "hello-node-mapping", 517 LegacyBelongsToNodeForBackwardCompat: "node1", 518 BelongsToNodes: []string{"node1", "node2", "node3"}, 519 }, 520 }, 521 }, 522 control: State{ 523 Physical: map[string]Physical{ 524 "hello-node-mapping": { 525 Name: "hello-node-mapping", 526 LegacyBelongsToNodeForBackwardCompat: "new-node1", 527 BelongsToNodes: []string{"new-node1", "new-node2", "node3"}, 528 }, 529 }, 530 }, 531 nodeMapping: map[string]string{"node1": "new-node1", "node2": "new-node2"}, 532 }, 533 { 534 name: "map multiple nodes with legacy exception", 535 state: State{ 536 Physical: map[string]Physical{ 537 "hello-node-mapping": { 538 Name: "hello-node-mapping", 539 LegacyBelongsToNodeForBackwardCompat: "node3", 540 BelongsToNodes: []string{"node1", "node2", "node3"}, 541 }, 542 }, 543 }, 544 control: State{ 545 Physical: map[string]Physical{ 546 "hello-node-mapping": { 547 Name: "hello-node-mapping", 548 LegacyBelongsToNodeForBackwardCompat: "node3", 549 BelongsToNodes: []string{"new-node1", "new-node2", "node3"}, 550 }, 551 }, 552 }, 553 nodeMapping: map[string]string{"node1": "new-node1", "node2": "new-node2"}, 554 }, 555 } 556 557 for _, tc := range tests { 558 t.Run(tc.name, func(t *testing.T) { 559 tc.state.ApplyNodeMapping(tc.nodeMapping) 560 assert.Equal(t, tc.control, tc.state) 561 }) 562 } 563 }