KevinStephenson
Adding in weaviate code
b110593
raw
history blame
16.2 kB
// _ _
// __ _____ __ ___ ___ __ _| |_ ___
// \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
// \ 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)
})
}
}