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  }