github.com/MetalBlockchain/metalgo@v1.11.9/utils/hashing/consistent/ring_test.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package consistent
     5  
     6  import (
     7  	"testing"
     8  
     9  	"github.com/stretchr/testify/require"
    10  	"go.uber.org/mock/gomock"
    11  
    12  	"github.com/MetalBlockchain/metalgo/utils/hashing"
    13  )
    14  
    15  var (
    16  	_ Hashable = (*testKey)(nil)
    17  
    18  	// nodes
    19  	node1 = testKey{key: "node-1", hash: 1}
    20  	node2 = testKey{key: "node-2", hash: 2}
    21  	node3 = testKey{key: "node-3", hash: 3}
    22  )
    23  
    24  // testKey is a simple wrapper around a key and its mocked hash for testing.
    25  type testKey struct {
    26  	// key
    27  	key string
    28  	// mocked hash value of the key
    29  	hash uint64
    30  }
    31  
    32  func (t testKey) ConsistentHashKey() []byte {
    33  	return []byte(t.key)
    34  }
    35  
    36  // Tests that a key routes to its closest clockwise node.
    37  // Test cases are described in greater detail below; see diagrams for Ring.
    38  func TestGetMapsToClockwiseNode(t *testing.T) {
    39  	tests := []struct {
    40  		// name of the test
    41  		name string
    42  		// nodes that exist in the ring
    43  		ringNodes []testKey
    44  		// key to try to route
    45  		key testKey
    46  		// expected key to be routed to
    47  		expectedNode testKey
    48  	}{
    49  		{
    50  			// If we're left of a node in the ring, we should route to it.
    51  			//
    52  			// Ring:
    53  			// ... -> foo -> node-1 -> ...
    54  			name: "key with right node",
    55  			ringNodes: []testKey{
    56  				node1,
    57  			},
    58  			key: testKey{
    59  				key:  "foo",
    60  				hash: 0,
    61  			},
    62  			expectedNode: node1,
    63  		},
    64  		{
    65  			// If we occupy the same hash as the only ring node, we should route to it.
    66  			//
    67  			// Ring:
    68  			// ... -> foo, node-1 -> ...
    69  			name: "key with equal node",
    70  			ringNodes: []testKey{
    71  				node1,
    72  			},
    73  			key: testKey{
    74  				key:  "foo",
    75  				hash: 1,
    76  			},
    77  			expectedNode: node1,
    78  		},
    79  		{
    80  			// If we're clockwise of the only node, we should wrap around and route to that node.
    81  			//
    82  			// Ring:
    83  			// ... -> node-1 -> foo -> ...
    84  			name: "key wraps around to left-most node",
    85  			ringNodes: []testKey{
    86  				node1,
    87  			},
    88  			key: testKey{
    89  				key:  "foo",
    90  				hash: 2,
    91  			},
    92  			expectedNode: node1,
    93  		},
    94  
    95  		{
    96  			// If we're left of multiple nodes in the ring, we should route to the first clockwise node.
    97  			//
    98  			// Ring:
    99  			// ... -> foo -> node-1 -> node-2 -> ...
   100  			name: "key with two right nodes",
   101  			ringNodes: []testKey{
   102  				node1,
   103  				node2,
   104  			},
   105  			key: testKey{
   106  				key:  "foo",
   107  				hash: 0,
   108  			},
   109  			expectedNode: node1,
   110  		},
   111  		{
   112  			// If we occupy the same hash as a node, we should route to the node clockwise of it.
   113  			//
   114  			// Ring:
   115  			// ... -> foo, node-1 -> node-2 -> ...
   116  			name: "key with one equal node and one right node",
   117  			ringNodes: []testKey{
   118  				node2,
   119  				node1,
   120  			},
   121  			key: testKey{
   122  				key:  "foo",
   123  				hash: 1,
   124  			},
   125  			expectedNode: node2,
   126  		},
   127  		{
   128  			// If we're in between two nodes, we should route to the clockwise node.
   129  			//
   130  			// Ring:
   131  			// ... -> node-1 -> foo -> node-3 -> ...
   132  			name: "key between two nodes",
   133  			ringNodes: []testKey{
   134  				node3,
   135  				node1,
   136  			},
   137  			key: testKey{
   138  				key:  "foo",
   139  				hash: 2,
   140  			},
   141  			expectedNode: node3,
   142  		},
   143  		{
   144  			// If we're clockwise of all ring keys, we should wrap around and route to the left-most node.
   145  			//
   146  			// Ring:
   147  			// ... -> node-1 -> node-2 -> foo -> ...
   148  			name: "key with two left nodes and no right neighbors",
   149  			ringNodes: []testKey{
   150  				node2,
   151  				node1,
   152  			},
   153  			key: testKey{
   154  				key:  "foo",
   155  				hash: 3,
   156  			},
   157  			expectedNode: node1,
   158  		},
   159  		{
   160  			// If we occupy the same hash as a node, we should wrap around to the clockwise node.
   161  			//
   162  			// Ring:
   163  			// ... -> node-1 -> node-2, foo -> ...
   164  			name: "key with equal neighbor and no right node wraps around to left-most node",
   165  			ringNodes: []testKey{
   166  				node2,
   167  				node1,
   168  			},
   169  			key: testKey{
   170  				key:  "foo",
   171  				hash: 2,
   172  			},
   173  			expectedNode: node1,
   174  		},
   175  	}
   176  
   177  	for _, test := range tests {
   178  		t.Run(test.name, func(t *testing.T) {
   179  			require := require.New(t)
   180  			ring, hasher := setupTest(t, 1)
   181  
   182  			// setup expected calls
   183  			calls := make([]any, len(test.ringNodes)+1)
   184  
   185  			for i, key := range test.ringNodes {
   186  				calls[i] = hasher.EXPECT().Hash(getHashKey(key.ConsistentHashKey(), 0)).Return(key.hash).Times(1)
   187  			}
   188  
   189  			calls[len(test.ringNodes)] = hasher.EXPECT().Hash(test.key.ConsistentHashKey()).Return(test.key.hash).Times(1)
   190  			gomock.InOrder(calls...)
   191  
   192  			// execute test
   193  			for _, key := range test.ringNodes {
   194  				ring.Add(key)
   195  			}
   196  
   197  			node, err := ring.Get(test.key)
   198  			require.NoError(err)
   199  			require.Equal(test.expectedNode, node)
   200  		})
   201  	}
   202  }
   203  
   204  // Tests that if we have an empty ring, trying to call Get results in an error, as there is no node to route to.
   205  func TestGetOnEmptyRingReturnsError(t *testing.T) {
   206  	ring, _ := setupTest(t, 1)
   207  
   208  	foo := testKey{
   209  		key:  "foo",
   210  		hash: 0,
   211  	}
   212  	_, err := ring.Get(foo)
   213  	require.ErrorIs(t, err, errEmptyRing)
   214  }
   215  
   216  // Tests that trying to call Remove on a node that doesn't exist should return false.
   217  func TestRemoveNonExistentKeyReturnsFalse(t *testing.T) {
   218  	ring, hasher := setupTest(t, 1)
   219  
   220  	gomock.InOrder(
   221  		hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(uint64(1)).Times(1),
   222  	)
   223  
   224  	// try to remove something from an empty ring.
   225  	require.False(t, ring.Remove(node1))
   226  }
   227  
   228  // Tests that trying to call Remove on a node that doesn't exist should return true.
   229  func TestRemoveExistingKeyReturnsTrue(t *testing.T) {
   230  	ring, hasher := setupTest(t, 1)
   231  
   232  	gomock.InOrder(
   233  		hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(uint64(1)).Times(1),
   234  	)
   235  
   236  	// Add a node into the ring.
   237  	//
   238  	// Ring:
   239  	// ... -> node-1 -> ...
   240  	ring.Add(node1)
   241  
   242  	gomock.InOrder(
   243  		hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(uint64(1)).Times(1),
   244  	)
   245  
   246  	// Try to remove it.
   247  	//
   248  	// Ring:
   249  	// ... -> (empty) -> ...
   250  	require.True(t, ring.Remove(node1))
   251  }
   252  
   253  // Tests that if we have a collision, the node is replaced.
   254  func TestAddCollisionReplacement(t *testing.T) {
   255  	require := require.New(t)
   256  	ring, hasher := setupTest(t, 1)
   257  
   258  	foo := testKey{
   259  		key:  "foo",
   260  		hash: 2,
   261  	}
   262  
   263  	gomock.InOrder(
   264  		// node-1 and node-2 occupy the same hash
   265  		hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(uint64(1)).Times(1),
   266  		hasher.EXPECT().Hash(getHashKey(node2.ConsistentHashKey(), 0)).Return(uint64(1)).Times(1),
   267  		hasher.EXPECT().Hash(foo.ConsistentHashKey()).Return(uint64(1)).Times(1),
   268  	)
   269  
   270  	// Ring:
   271  	// ... -> node-1 -> ...
   272  	ring.Add(node1)
   273  
   274  	// Ring:
   275  	// ... -> node-2 -> ...
   276  	ring.Add(node2)
   277  
   278  	ringMember, err := ring.Get(foo)
   279  	require.NoError(err)
   280  	require.Equal(node2, ringMember)
   281  }
   282  
   283  // Tests that virtual nodes are replicated on Add.
   284  func TestAddVirtualNodes(t *testing.T) {
   285  	require := require.New(t)
   286  	ring, hasher := setupTest(t, 3)
   287  
   288  	gomock.InOrder(
   289  		// we should see 3 virtual nodes created (0, 1, 2) when we insert a node into the ring.
   290  
   291  		// insert node-1
   292  		hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(uint64(0)).Times(1),
   293  		hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 1)).Return(uint64(2)).Times(1),
   294  		hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 2)).Return(uint64(4)).Times(1),
   295  
   296  		// insert node-2
   297  		hasher.EXPECT().Hash(getHashKey(node2.ConsistentHashKey(), 0)).Return(uint64(1)).Times(1),
   298  		hasher.EXPECT().Hash(getHashKey(node2.ConsistentHashKey(), 1)).Return(uint64(3)).Times(1),
   299  		hasher.EXPECT().Hash(getHashKey(node2.ConsistentHashKey(), 2)).Return(uint64(5)).Times(1),
   300  
   301  		// gets that should route to node-1
   302  		hasher.EXPECT().Hash([]byte("foo1")).Return(uint64(1)).Times(1),
   303  		hasher.EXPECT().Hash([]byte("foo3")).Return(uint64(3)).Times(1),
   304  		hasher.EXPECT().Hash([]byte("foo5")).Return(uint64(5)).Times(1),
   305  
   306  		// gets that should route to node-2
   307  		hasher.EXPECT().Hash([]byte("foo0")).Return(uint64(0)).Times(1),
   308  		hasher.EXPECT().Hash([]byte("foo2")).Return(uint64(2)).Times(1),
   309  		hasher.EXPECT().Hash([]byte("foo4")).Return(uint64(4)).Times(1),
   310  	)
   311  
   312  	// Add node 1.
   313  	//
   314  	// Ring:
   315  	// ... -> node-1-v0 -> node-1-v1 -> node-1-v2 -> ...
   316  	ring.Add(node1)
   317  
   318  	// Add node 2.
   319  	//
   320  	// Ring:
   321  	// ... -> node-1-v0 -> node-2-v0 -> node-1-v1 -> node-2-v1 -> node-1-v2 -> node-2-v2 -> ...
   322  	ring.Add(node2)
   323  
   324  	// Gets that should route to node-1
   325  	node, err := ring.Get(testKey{key: "foo1"})
   326  	require.NoError(err)
   327  	require.Equal(node1, node)
   328  	node, err = ring.Get(testKey{key: "foo3"})
   329  	require.NoError(err)
   330  	require.Equal(node1, node)
   331  	node, err = ring.Get(testKey{key: "foo5"})
   332  	require.NoError(err)
   333  	require.Equal(node1, node)
   334  
   335  	// Gets that should route to node-2
   336  	node, err = ring.Get(testKey{key: "foo0"})
   337  	require.NoError(err)
   338  	require.Equal(node2, node)
   339  	node, err = ring.Get(testKey{key: "foo2"})
   340  	require.NoError(err)
   341  	require.Equal(node2, node)
   342  	node, err = ring.Get(testKey{key: "foo4"})
   343  	require.NoError(err)
   344  	require.Equal(node2, node)
   345  }
   346  
   347  // Tests that the node routed to changes if an Add results in a key shuffle.
   348  func TestGetShuffleOnAdd(t *testing.T) {
   349  	require := require.New(t)
   350  	ring, hasher := setupTest(t, 1)
   351  
   352  	foo := testKey{
   353  		key:  "foo",
   354  		hash: 1,
   355  	}
   356  
   357  	gomock.InOrder(
   358  		hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(uint64(0)).Times(1),
   359  		hasher.EXPECT().Hash(foo.ConsistentHashKey()).Return(foo.hash).Times(1),
   360  
   361  		hasher.EXPECT().Hash(getHashKey(node2.ConsistentHashKey(), 0)).Return(uint64(2)).Times(1),
   362  		hasher.EXPECT().Hash(foo.ConsistentHashKey()).Return(foo.hash).Times(1),
   363  	)
   364  
   365  	// Add node-1 into the ring
   366  	//
   367  	// Ring:
   368  	// ... -> node-1 -> ...
   369  	ring.Add(node1)
   370  
   371  	// node-1 is the closest clockwise node (when we wrap around), so we route to it.
   372  	//
   373  	// Ring:
   374  	// ... -> node-1 -> foo -> ...
   375  	node, err := ring.Get(foo)
   376  	require.NoError(err)
   377  	require.Equal(node1, node)
   378  
   379  	// Add node-2, which results in foo being shuffled from node-1 to node-2.
   380  	//
   381  	// Ring:
   382  	// ... -> node-1 -> node-2 -> ...
   383  	ring.Add(node2)
   384  
   385  	// Now node-2 is our closest clockwise node, so we should route to it.
   386  	//
   387  	// Ring:
   388  	// ... -> node-1 -> foo -> node-2 -> ...
   389  	node, err = ring.Get(foo)
   390  	require.NoError(err)
   391  	require.Equal(node2, node)
   392  }
   393  
   394  // Tests that we can iterate around the ring.
   395  func TestIteration(t *testing.T) {
   396  	require := require.New(t)
   397  	ring, hasher := setupTest(t, 1)
   398  
   399  	foo := testKey{
   400  		key:  "foo",
   401  		hash: 0,
   402  	}
   403  
   404  	gomock.InOrder(
   405  		hasher.EXPECT().Hash(getHashKey(node1.ConsistentHashKey(), 0)).Return(node1.hash).Times(1),
   406  		hasher.EXPECT().Hash(getHashKey(node2.ConsistentHashKey(), 0)).Return(node2.hash).Times(1),
   407  
   408  		hasher.EXPECT().Hash(foo.ConsistentHashKey()).Return(foo.hash).Times(1),
   409  		hasher.EXPECT().Hash(node1.ConsistentHashKey()).Return(node1.hash).Times(1),
   410  	)
   411  
   412  	// add node-1 into the ring
   413  	//
   414  	// Ring:
   415  	// ... -> node-1 -> ...
   416  	ring.Add(node1)
   417  
   418  	// add node-2 into the ring
   419  	//
   420  	// Ring:
   421  	// ... -> node-1 -> node-2 -> ...
   422  	ring.Add(node2)
   423  
   424  	// Get the neighbor of foo
   425  	//
   426  	// Ring:
   427  	// ... -> foo -> node-1 -> node-2 -> ...
   428  	node, err := ring.Get(foo)
   429  	require.NoError(err)
   430  	require.Equal(node1, node)
   431  
   432  	// iterate by re-using node-1 to get node-2
   433  	node, err = ring.Get(node)
   434  	require.NoError(err)
   435  	require.Equal(node2, node)
   436  }
   437  
   438  func setupTest(t *testing.T, virtualNodes int) (Ring, *hashing.MockHasher) {
   439  	ctrl := gomock.NewController(t)
   440  	hasher := hashing.NewMockHasher(ctrl)
   441  
   442  	return NewHashRing(RingConfig{
   443  		VirtualNodes: virtualNodes,
   444  		Hasher:       hasher,
   445  		Degree:       2,
   446  	}), hasher
   447  }