Spaces:
Sleeping
Sleeping
| // _ _ | |
| // __ _____ __ ___ ___ __ _| |_ ___ | |
| // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \ | |
| // \ V V / __/ (_| |\ V /| | (_| | || __/ | |
| // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___| | |
| // | |
| // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved. | |
| // | |
| // CONTACT: [email protected] | |
| // | |
| package sharding | |
| import ( | |
| "crypto/rand" | |
| "encoding/json" | |
| "fmt" | |
| "testing" | |
| "github.com/stretchr/testify/assert" | |
| "github.com/stretchr/testify/require" | |
| "github.com/weaviate/weaviate/entities/models" | |
| ) | |
| func TestState(t *testing.T) { | |
| size := 1000 | |
| cfg, err := ParseConfig(map[string]interface{}{"desiredCount": float64(4)}, 14) | |
| require.Nil(t, err) | |
| nodes := fakeNodes{[]string{"node1", "node2"}} | |
| state, err := InitState("my-index", cfg, nodes, 1, false) | |
| require.Nil(t, err) | |
| physicalCount := map[string]int{} | |
| var names [][]byte | |
| for i := 0; i < size; i++ { | |
| name := make([]byte, 16) | |
| rand.Read(name) | |
| names = append(names, name) | |
| phid := state.PhysicalShard(name) | |
| physicalCount[phid]++ | |
| } | |
| // verify each shard contains at least 15% of data. The expected value would | |
| // be 25%, but since this is random, we should take a lower value to reduce | |
| // flakyness | |
| for name, count := range physicalCount { | |
| if owns := float64(count) / float64(size); owns < 0.15 { | |
| t.Errorf("expected shard %q to own at least 15%%, but it only owns %f", name, owns) | |
| } | |
| } | |
| // Marshal and recreate, verify results | |
| bytes, err := state.JSON() | |
| require.Nil(t, err) | |
| // destroy old version | |
| state = nil | |
| stateReloaded, err := StateFromJSON(bytes, nodes) | |
| require.Nil(t, err) | |
| physicalCountReloaded := map[string]int{} | |
| // hash the same values again and verify the counts are exactly the same | |
| for _, name := range names { | |
| phid := stateReloaded.PhysicalShard(name) | |
| physicalCountReloaded[phid]++ | |
| } | |
| assert.Equal(t, physicalCount, physicalCountReloaded) | |
| } | |
| type fakeNodes struct { | |
| nodes []string | |
| } | |
| func (f fakeNodes) Candidates() []string { | |
| return f.nodes | |
| } | |
| func (f fakeNodes) LocalName() string { | |
| return f.nodes[0] | |
| } | |
| func TestInitState(t *testing.T) { | |
| type test struct { | |
| nodes []string | |
| replicationFactor int | |
| shards int | |
| ok bool | |
| } | |
| // this tests asserts that nodes are assigned evenly with various | |
| // combinations. | |
| tests := []test{ | |
| { | |
| nodes: []string{"node1", "node2", "node3"}, | |
| replicationFactor: 1, | |
| shards: 3, | |
| ok: true, | |
| }, | |
| { | |
| nodes: []string{"node1", "node2", "node3"}, | |
| replicationFactor: 2, | |
| shards: 3, | |
| ok: true, | |
| }, | |
| { | |
| nodes: []string{"node1", "node2", "node3"}, | |
| replicationFactor: 3, | |
| shards: 1, | |
| ok: true, | |
| }, | |
| { | |
| nodes: []string{"node1", "node2", "node3"}, | |
| replicationFactor: 3, | |
| shards: 3, | |
| ok: true, | |
| }, | |
| { | |
| nodes: []string{"node1", "node2", "node3"}, | |
| replicationFactor: 3, | |
| shards: 2, | |
| ok: true, | |
| }, | |
| { | |
| nodes: []string{"node1", "node2", "node3", "node4", "node5", "node6"}, | |
| replicationFactor: 4, | |
| shards: 6, | |
| ok: true, | |
| }, | |
| { | |
| nodes: []string{"node1", "node2"}, | |
| replicationFactor: 4, | |
| shards: 4, | |
| ok: false, | |
| }, | |
| } | |
| for _, test := range tests { | |
| t.Run(fmt.Sprintf("Shards=%d_RF=%d", test.shards, test.replicationFactor), | |
| func(t *testing.T) { | |
| nodes := fakeNodes{test.nodes} | |
| cfg, err := ParseConfig(map[string]interface{}{ | |
| "desiredCount": float64(test.shards), | |
| "replicas": float64(test.replicationFactor), | |
| }, 3) | |
| require.Nil(t, err) | |
| state, err := InitState("my-index", cfg, nodes, int64(test.replicationFactor), false) | |
| if !test.ok { | |
| require.NotNil(t, err) | |
| return | |
| } | |
| require.Nil(t, err) | |
| nodeCounter := map[string]int{} | |
| actual := 0 | |
| for _, shard := range state.Physical { | |
| for _, node := range shard.BelongsToNodes { | |
| nodeCounter[node]++ | |
| actual++ | |
| } | |
| } | |
| // assert that total no of associations is correct | |
| desired := test.shards * test.replicationFactor | |
| assert.Equal(t, desired, actual, "correct number of node associations") | |
| // assert that shards are hit evenly | |
| expectedAssociations := test.shards * test.replicationFactor / len(test.nodes) | |
| for _, count := range nodeCounter { | |
| assert.Equal(t, expectedAssociations, count) | |
| } | |
| }) | |
| } | |
| } | |
| func TestAdjustReplicas(t *testing.T) { | |
| t.Run("1->3", func(t *testing.T) { | |
| nodes := fakeNodes{nodes: []string{"N1", "N2", "N3", "N4", "N5"}} | |
| shard := Physical{BelongsToNodes: []string{"N1"}} | |
| require.Nil(t, shard.AdjustReplicas(3, nodes)) | |
| assert.ElementsMatch(t, []string{"N1", "N2", "N3"}, shard.BelongsToNodes) | |
| }) | |
| t.Run("2->3", func(t *testing.T) { | |
| nodes := fakeNodes{nodes: []string{"N1", "N2", "N3", "N4", "N5"}} | |
| shard := Physical{BelongsToNodes: []string{"N2", "N3"}} | |
| require.Nil(t, shard.AdjustReplicas(3, nodes)) | |
| assert.ElementsMatch(t, []string{"N1", "N2", "N3"}, shard.BelongsToNodes) | |
| }) | |
| t.Run("3->3", func(t *testing.T) { | |
| nodes := fakeNodes{nodes: []string{"N1", "N2", "N3", "N4", "N5"}} | |
| shard := Physical{BelongsToNodes: []string{"N1", "N2", "N3"}} | |
| require.Nil(t, shard.AdjustReplicas(3, nodes)) | |
| assert.ElementsMatch(t, []string{"N1", "N2", "N3"}, shard.BelongsToNodes) | |
| }) | |
| t.Run("3->2", func(t *testing.T) { | |
| nodes := fakeNodes{nodes: []string{"N1", "N2", "N3"}} | |
| shard := Physical{BelongsToNodes: []string{"N1", "N2", "N3"}} | |
| require.Nil(t, shard.AdjustReplicas(2, nodes)) | |
| assert.ElementsMatch(t, []string{"N1", "N2"}, shard.BelongsToNodes) | |
| }) | |
| t.Run("Min", func(t *testing.T) { | |
| nodes := fakeNodes{nodes: []string{"N1", "N2", "N3"}} | |
| shard := Physical{BelongsToNodes: []string{"N1", "N2", "N3"}} | |
| require.NotNil(t, shard.AdjustReplicas(-1, nodes)) | |
| }) | |
| t.Run("Max", func(t *testing.T) { | |
| nodes := fakeNodes{nodes: []string{"N1", "N2", "N3"}} | |
| shard := Physical{BelongsToNodes: []string{"N1", "N2", "N3"}} | |
| require.NotNil(t, shard.AdjustReplicas(4, nodes)) | |
| }) | |
| t.Run("Bug", func(t *testing.T) { | |
| names := []string{"N1", "N2", "N3", "N4"} | |
| nodes := fakeNodes{nodes: names} // bug | |
| shard := Physical{BelongsToNodes: []string{"N1", "N1", "N1", "N2", "N2"}} | |
| require.Nil(t, shard.AdjustReplicas(4, nodes)) // correct | |
| require.ElementsMatch(t, names, shard.BelongsToNodes) | |
| }) | |
| } | |
| func TestGetPartitions(t *testing.T) { | |
| t.Run("EmptyCandidatesList", func(t *testing.T) { | |
| // nodes := fakeNodes{nodes: []string{"N1", "N2", "N3", "N4", "N5"}} | |
| shards := []string{"H1"} | |
| state := State{} | |
| partitions, err := state.GetPartitions(fakeNodes{}, shards, 1) | |
| require.Nil(t, partitions) | |
| require.ErrorContains(t, err, "empty") | |
| }) | |
| t.Run("NotEnoughReplicas", func(t *testing.T) { | |
| shards := []string{"H1"} | |
| state := State{} | |
| partitions, err := state.GetPartitions(fakeNodes{nodes: []string{"N1"}}, shards, 2) | |
| require.Nil(t, partitions) | |
| require.ErrorContains(t, err, "not enough replicas") | |
| }) | |
| t.Run("Success", func(t *testing.T) { | |
| nodes := fakeNodes{nodes: []string{"N1", "N2", "N3"}} | |
| shards := []string{"H1", "H2", "H3"} | |
| state := State{} | |
| got, err := state.GetPartitions(nodes, shards, 3) | |
| require.Nil(t, err) | |
| want := map[string][]string{ | |
| "H1": {"N1", "N2", "N3"}, | |
| "H2": {"N2", "N3", "N1"}, | |
| "H3": {"N3", "N1", "N2"}, | |
| } | |
| require.Equal(t, want, got) | |
| }) | |
| } | |
| func TestAddPartition(t *testing.T) { | |
| var ( | |
| nodes1 = []string{"N", "M"} | |
| nodes2 = []string{"L", "M", "O"} | |
| ) | |
| cfg, err := ParseConfig(map[string]interface{}{"desiredCount": float64(4)}, 14) | |
| require.Nil(t, err) | |
| nodes := fakeNodes{[]string{"node1", "node2"}} | |
| s, err := InitState("my-index", cfg, nodes, 1, true) | |
| require.Nil(t, err) | |
| s.AddPartition("A", nodes1, models.TenantActivityStatusHOT) | |
| s.AddPartition("B", nodes2, models.TenantActivityStatusCOLD) | |
| want := map[string]Physical{ | |
| "A": {Name: "A", BelongsToNodes: nodes1, OwnsPercentage: 1, Status: models.TenantActivityStatusHOT}, | |
| "B": {Name: "B", BelongsToNodes: nodes2, OwnsPercentage: 1, Status: models.TenantActivityStatusCOLD}, | |
| } | |
| require.Equal(t, want, s.Physical) | |
| } | |
| func TestStateDeepCopy(t *testing.T) { | |
| original := State{ | |
| IndexID: "original", | |
| Config: Config{ | |
| VirtualPerPhysical: 1, | |
| DesiredCount: 2, | |
| ActualCount: 3, | |
| DesiredVirtualCount: 4, | |
| ActualVirtualCount: 5, | |
| Key: "original", | |
| Strategy: "original", | |
| Function: "original", | |
| }, | |
| localNodeName: "original", | |
| Physical: map[string]Physical{ | |
| "physical1": { | |
| Name: "original", | |
| OwnsVirtual: []string{"original"}, | |
| OwnsPercentage: 7, | |
| BelongsToNodes: []string{"original"}, | |
| Status: models.TenantActivityStatusHOT, | |
| }, | |
| }, | |
| Virtual: []Virtual{ | |
| { | |
| Name: "original", | |
| Upper: 8, | |
| OwnsPercentage: 9, | |
| AssignedToPhysical: "original", | |
| }, | |
| }, | |
| } | |
| control := State{ | |
| IndexID: "original", | |
| Config: Config{ | |
| VirtualPerPhysical: 1, | |
| DesiredCount: 2, | |
| ActualCount: 3, | |
| DesiredVirtualCount: 4, | |
| ActualVirtualCount: 5, | |
| Key: "original", | |
| Strategy: "original", | |
| Function: "original", | |
| }, | |
| localNodeName: "original", | |
| Physical: map[string]Physical{ | |
| "physical1": { | |
| Name: "original", | |
| OwnsVirtual: []string{"original"}, | |
| OwnsPercentage: 7, | |
| BelongsToNodes: []string{"original"}, | |
| Status: models.TenantActivityStatusHOT, | |
| }, | |
| }, | |
| Virtual: []Virtual{ | |
| { | |
| Name: "original", | |
| Upper: 8, | |
| OwnsPercentage: 9, | |
| AssignedToPhysical: "original", | |
| }, | |
| }, | |
| } | |
| assert.Equal(t, control, original, "control matches initially") | |
| copied := original.DeepCopy() | |
| assert.Equal(t, control, copied, "copy matches original") | |
| // modify literally every field | |
| copied.localNodeName = "changed" | |
| copied.IndexID = "changed" | |
| copied.Config.VirtualPerPhysical = 11 | |
| copied.Config.DesiredCount = 22 | |
| copied.Config.ActualCount = 33 | |
| copied.Config.DesiredVirtualCount = 44 | |
| copied.Config.ActualVirtualCount = 55 | |
| copied.Config.Key = "changed" | |
| copied.Config.Strategy = "changed" | |
| copied.Config.Function = "changed" | |
| physical1 := copied.Physical["physical1"] | |
| physical1.Name = "changed" | |
| physical1.BelongsToNodes = append(physical1.BelongsToNodes, "changed") | |
| physical1.OwnsPercentage = 100 | |
| physical1.OwnsVirtual = append(physical1.OwnsVirtual, "changed") | |
| physical1.Status = models.TenantActivityStatusCOLD | |
| copied.Physical["physical1"] = physical1 | |
| copied.Physical["physical2"] = Physical{} | |
| copied.Virtual[0].Name = "original" | |
| copied.Virtual[0].Upper = 8 | |
| copied.Virtual[0].OwnsPercentage = 9 | |
| copied.Virtual[0].AssignedToPhysical = "original" | |
| copied.Virtual = append(copied.Virtual, Virtual{}) | |
| assert.Equal(t, control, original, "original still matches control even with changes in copy") | |
| } | |
| func TestBackwardCompatibilityBefore1_17(t *testing.T) { | |
| // As part of v1.17, replication is introduced and the structure of the | |
| // physical shard is slightly changed. Instead of `belongsToNode string`, the | |
| // association is now `belongsToNodes []string`. A migration helper was | |
| // introduced to make sure we're backward compatible. | |
| oldVersion := State{ | |
| Physical: map[string]Physical{ | |
| "hello-replication": { | |
| Name: "hello-replication", | |
| LegacyBelongsToNodeForBackwardCompat: "the-best-node", | |
| }, | |
| }, | |
| } | |
| oldVersionJSON, err := json.Marshal(oldVersion) | |
| require.Nil(t, err) | |
| var newVersion State | |
| err = json.Unmarshal(oldVersionJSON, &newVersion) | |
| require.Nil(t, err) | |
| newVersion.MigrateFromOldFormat() | |
| assert.Equal(t, []string{"the-best-node"}, | |
| newVersion.Physical["hello-replication"].BelongsToNodes) | |
| } | |
| func TestApplyNodeMapping(t *testing.T) { | |
| type test struct { | |
| name string | |
| state State | |
| control State | |
| nodeMapping map[string]string | |
| } | |
| tests := []test{ | |
| { | |
| name: "no mapping", | |
| state: State{ | |
| Physical: map[string]Physical{ | |
| "hello-node-mapping": { | |
| Name: "hello-node-mapping", | |
| LegacyBelongsToNodeForBackwardCompat: "node1", | |
| BelongsToNodes: []string{"node1"}, | |
| }, | |
| }, | |
| }, | |
| control: State{ | |
| Physical: map[string]Physical{ | |
| "hello-node-mapping": { | |
| Name: "hello-node-mapping", | |
| LegacyBelongsToNodeForBackwardCompat: "node1", | |
| BelongsToNodes: []string{"node1"}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "map one node", | |
| state: State{ | |
| Physical: map[string]Physical{ | |
| "hello-node-mapping": { | |
| Name: "hello-node-mapping", | |
| LegacyBelongsToNodeForBackwardCompat: "node1", | |
| BelongsToNodes: []string{"node1"}, | |
| }, | |
| }, | |
| }, | |
| control: State{ | |
| Physical: map[string]Physical{ | |
| "hello-node-mapping": { | |
| Name: "hello-node-mapping", | |
| LegacyBelongsToNodeForBackwardCompat: "new-node1", | |
| BelongsToNodes: []string{"new-node1"}, | |
| }, | |
| }, | |
| }, | |
| nodeMapping: map[string]string{"node1": "new-node1"}, | |
| }, | |
| { | |
| name: "map multiple nodes", | |
| state: State{ | |
| Physical: map[string]Physical{ | |
| "hello-node-mapping": { | |
| Name: "hello-node-mapping", | |
| LegacyBelongsToNodeForBackwardCompat: "node1", | |
| BelongsToNodes: []string{"node1", "node2"}, | |
| }, | |
| }, | |
| }, | |
| control: State{ | |
| Physical: map[string]Physical{ | |
| "hello-node-mapping": { | |
| Name: "hello-node-mapping", | |
| LegacyBelongsToNodeForBackwardCompat: "new-node1", | |
| BelongsToNodes: []string{"new-node1", "new-node2"}, | |
| }, | |
| }, | |
| }, | |
| nodeMapping: map[string]string{"node1": "new-node1", "node2": "new-node2"}, | |
| }, | |
| { | |
| name: "map multiple nodes with exceptions", | |
| state: State{ | |
| Physical: map[string]Physical{ | |
| "hello-node-mapping": { | |
| Name: "hello-node-mapping", | |
| LegacyBelongsToNodeForBackwardCompat: "node1", | |
| BelongsToNodes: []string{"node1", "node2", "node3"}, | |
| }, | |
| }, | |
| }, | |
| control: State{ | |
| Physical: map[string]Physical{ | |
| "hello-node-mapping": { | |
| Name: "hello-node-mapping", | |
| LegacyBelongsToNodeForBackwardCompat: "new-node1", | |
| BelongsToNodes: []string{"new-node1", "new-node2", "node3"}, | |
| }, | |
| }, | |
| }, | |
| nodeMapping: map[string]string{"node1": "new-node1", "node2": "new-node2"}, | |
| }, | |
| { | |
| name: "map multiple nodes with legacy exception", | |
| state: State{ | |
| Physical: map[string]Physical{ | |
| "hello-node-mapping": { | |
| Name: "hello-node-mapping", | |
| LegacyBelongsToNodeForBackwardCompat: "node3", | |
| BelongsToNodes: []string{"node1", "node2", "node3"}, | |
| }, | |
| }, | |
| }, | |
| control: State{ | |
| Physical: map[string]Physical{ | |
| "hello-node-mapping": { | |
| Name: "hello-node-mapping", | |
| LegacyBelongsToNodeForBackwardCompat: "node3", | |
| BelongsToNodes: []string{"new-node1", "new-node2", "node3"}, | |
| }, | |
| }, | |
| }, | |
| nodeMapping: map[string]string{"node1": "new-node1", "node2": "new-node2"}, | |
| }, | |
| } | |
| for _, tc := range tests { | |
| t.Run(tc.name, func(t *testing.T) { | |
| tc.state.ApplyNodeMapping(tc.nodeMapping) | |
| assert.Equal(t, tc.control, tc.state) | |
| }) | |
| } | |
| } | |