tooz/tooz/tests/test_hashring.py

260 lines
10 KiB
Python

# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import hashlib
from unittest import mock
from testtools import matchers
from testtools import testcase
from tooz import hashring
class HashRingTestCase(testcase.TestCase):
# NOTE(deva): the mapping used in these tests is as follows:
# if nodes = [foo, bar]:
# fake -> foo, bar
# if nodes = [foo, bar, baz]:
# fake -> foo, bar, baz
# fake-again -> bar, baz, foo
try:
_ = hashlib.md5(usedforsecurity=False)
_fips_enabled = True
except TypeError:
_fips_enabled = False
@mock.patch.object(hashlib, 'md5', autospec=True)
def test_hash2int_returns_int(self, mock_md5):
r1 = 32 * 'a'
r2 = 32 * 'b'
# 2**PARTITION_EXPONENT calls to md5.update per node
# PARTITION_EXPONENT is currently always 5, so 32 calls each here
mock_md5.return_value.hexdigest.side_effect = [r1] * 32 + [r2] * 32
nodes = ['foo', 'bar']
ring = hashring.HashRing(nodes)
self.assertIn(int(r1, 16), ring._ring)
self.assertIn(int(r2, 16), ring._ring)
if self._fips_enabled:
mock_md5.assert_called_with(mock.ANY, usedforsecurity=False)
else:
mock_md5.assert_called_with(mock.ANY)
def test_create_ring(self):
nodes = {'foo', 'bar'}
ring = hashring.HashRing(nodes)
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * 2, len(ring))
def test_wrong_hash_function(self):
nodes = {'foo', 'bar'}
self.assertRaisesRegex(ValueError, 'is not supported',
hashring.HashRing, nodes,
hash_function='fold twice and leave to dry')
def test_add_node(self):
nodes = {'foo', 'bar'}
ring = hashring.HashRing(nodes)
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
nodes.add('baz')
ring.add_node('baz')
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
def test_add_node_bytes(self):
nodes = {'foo', 'bar'}
ring = hashring.HashRing(nodes)
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
nodes.add(b'Z\xe2\xfa\x90\x17EC\xac\xae\x88\xa7[\xa1}:E')
ring.add_node(b'Z\xe2\xfa\x90\x17EC\xac\xae\x88\xa7[\xa1}:E')
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
def test_add_node_unicode(self):
nodes = {'foo', 'bar'}
ring = hashring.HashRing(nodes)
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
nodes.add(u'\u0634\u0628\u06a9\u0647')
ring.add_node(u'\u0634\u0628\u06a9\u0647')
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
def test_add_node_weight(self):
nodes = {'foo', 'bar'}
ring = hashring.HashRing(nodes)
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
nodes.add('baz')
ring.add_node('baz', weight=10)
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * 12, len(ring))
def test_add_nodes_weight(self):
nodes = {'foo', 'bar'}
ring = hashring.HashRing(nodes)
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
nodes.add('baz')
nodes.add('baz2')
ring.add_nodes(set(['baz', 'baz2']), weight=10)
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * 22, len(ring))
def test_remove_node(self):
nodes = {'foo', 'bar'}
ring = hashring.HashRing(nodes)
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
nodes.discard('bar')
ring.remove_node('bar')
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
def test_remove_node_bytes(self):
nodes = {'foo', b'Z\xe2\xfa\x90\x17EC\xac\xae\x88\xa7[\xa1}:E'}
ring = hashring.HashRing(nodes)
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
nodes.discard(b'Z\xe2\xfa\x90\x17EC\xac\xae\x88\xa7[\xa1}:E')
ring.remove_node(b'Z\xe2\xfa\x90\x17EC\xac\xae\x88\xa7[\xa1}:E')
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
def test_remove_node_unknown(self):
nodes = ['foo', 'bar']
ring = hashring.HashRing(nodes)
self.assertRaises(
hashring.UnknownNode,
ring.remove_node, 'biz')
def test_add_then_removenode(self):
nodes = {'foo', 'bar'}
ring = hashring.HashRing(nodes)
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
nodes.add('baz')
ring.add_node('baz')
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
nodes.discard('bar')
ring.remove_node('bar')
self.assertEqual(nodes, set(ring.nodes.keys()))
self.assertEqual(2 ** 5 * len(nodes), len(ring))
def test_distribution_one_replica(self):
nodes = ['foo', 'bar', 'baz']
ring = hashring.HashRing(nodes)
fake_1_nodes = ring.get_nodes(b'fake')
fake_2_nodes = ring.get_nodes(b'fake-again')
# We should have one nodes for each thing
self.assertEqual(1, len(fake_1_nodes))
self.assertEqual(1, len(fake_2_nodes))
# And they must not be the same answers even on this simple data.
self.assertNotEqual(fake_1_nodes, fake_2_nodes)
def test_distribution_more_replica(self):
nodes = ['foo', 'bar', 'baz']
ring = hashring.HashRing(nodes)
fake_1_nodes = ring.get_nodes(b'fake', replicas=2)
fake_2_nodes = ring.get_nodes(b'fake-again', replicas=2)
# We should have one nodes for each thing
self.assertEqual(2, len(fake_1_nodes))
self.assertEqual(2, len(fake_2_nodes))
fake_1_nodes = ring.get_nodes(b'fake', replicas=3)
fake_2_nodes = ring.get_nodes(b'fake-again', replicas=3)
# We should have one nodes for each thing
self.assertEqual(3, len(fake_1_nodes))
self.assertEqual(3, len(fake_2_nodes))
self.assertEqual(fake_1_nodes, fake_2_nodes)
def test_ignore_nodes(self):
nodes = ['foo', 'bar', 'baz']
ring = hashring.HashRing(nodes)
equals_bar_or_baz = matchers.MatchesAny(
matchers.Equals({'bar'}),
matchers.Equals({'baz'}))
self.assertThat(
ring.get_nodes(b'fake', ignore_nodes=['foo']),
equals_bar_or_baz)
self.assertThat(
ring.get_nodes(b'fake', ignore_nodes=['foo', 'bar']),
equals_bar_or_baz)
self.assertEqual(set(), ring.get_nodes(b'fake', ignore_nodes=nodes))
@staticmethod
def _compare_rings(nodes, conductors, ring, new_conductors, new_ring):
delta = {}
mapping = {
'node': list(ring.get_nodes(node.encode('ascii')))[0]
for node in nodes
}
new_mapping = {
'node': list(new_ring.get_nodes(node.encode('ascii')))[0]
for node in nodes
}
for key, old in mapping.items():
new = new_mapping.get(key, None)
if new != old:
delta[key] = (old, new)
return delta
def test_rebalance_stability_join(self):
num_services = 10
num_nodes = 10000
# Adding 1 service to a set of N should move 1/(N+1) of all nodes
# Eg, for a cluster of 10 nodes, adding one should move 1/11, or 9%
# We allow for 1/N to allow for rounding in tests.
redistribution_factor = 1.0 / num_services
nodes = [str(x) for x in range(num_nodes)]
services = [str(x) for x in range(num_services)]
new_services = services + ['new']
delta = self._compare_rings(
nodes, services, hashring.HashRing(services),
new_services, hashring.HashRing(new_services))
self.assertLess(len(delta), num_nodes * redistribution_factor)
def test_rebalance_stability_leave(self):
num_services = 10
num_nodes = 10000
# Removing 1 service from a set of N should move 1/(N) of all nodes
# Eg, for a cluster of 10 nodes, removing one should move 1/10, or 10%
# We allow for 1/(N-1) to allow for rounding in tests.
redistribution_factor = 1.0 / (num_services - 1)
nodes = [str(x) for x in range(num_nodes)]
services = [str(x) for x in range(num_services)]
new_services = services[:]
new_services.pop()
delta = self._compare_rings(
nodes, services, hashring.HashRing(services),
new_services, hashring.HashRing(new_services))
self.assertLess(len(delta), num_nodes * redistribution_factor)
def test_ignore_non_existent_node(self):
nodes = ['foo', 'bar']
ring = hashring.HashRing(nodes)
self.assertEqual({'foo'}, ring.get_nodes(b'fake',
ignore_nodes=['baz']))