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 }