To implement consistent hashing with virtual nodes and O(log N) lookups, we keep a sorted list of virtual-node positions on the ring and a map from each position to its real node. We create multiple virtual nodes per real node (replicas proportional to weight) to improve balance. Lookups use binary search (bisect) for O(log N).python
import hashlib
import bisect
class ConsistentHashRing:
def __init__(self, replicas=100):
# base replicas per weight unit; total virtual nodes = replicas * weight
self.replicas = replicas
self._ring = [] # sorted list of positions (ints)
self._pos2node = {} # position -> node_id
self._nodes = {} # node_id -> set(positions)
def _hash(self, key):
# 128-bit hash mapped to int
h = hashlib.md5(key.encode('utf-8')).hexdigest()
return int(h, 16)
def add_node(self, node_id, weight=1):
if node_id in self._nodes:
raise ValueError("node already exists")
positions = set()
total = self.replicas * max(1, int(weight))
for i in range(total):
vnode_key = f"{node_id}#{i}"
pos = self._hash(vnode_key)
# avoid collision (extremely unlikely)
while pos in self._pos2node:
vnode_key += "_"
pos = self._hash(vnode_key)
bisect.insort(self._ring, pos)
self._pos2node[pos] = node_id
positions.add(pos)
self._nodes[node_id] = positions
def remove_node(self, node_id):
if node_id not in self._nodes:
raise KeyError("node not found")
for pos in self._nodes[node_id]:
idx = bisect.bisect_left(self._ring, pos)
if idx < len(self._ring) and self._ring[idx] == pos:
self._ring.pop(idx)
del self._pos2node[pos]
del self._nodes[node_id]
def get_node(self, key):
if not self._ring:
return None
pos = self._hash(key)
idx = bisect.bisect_right(self._ring, pos)
if idx == len(self._ring):
idx = 0
vnode_pos = self._ring[idx]
return self._pos2node[vnode_pos]
Key points:- Use MD5 to produce a stable integer hash.- Virtual nodes (replicas * weight) improve distribution.- get_node uses bisect on a sorted list -> O(log N) where N = number of virtual nodes.Time complexity: add_node O(R log N) (R = replicas*weight), remove_node O(R log N), get_node O(log N). Space: O(N).Edge cases: empty ring returns None; avoid duplicate adds; handle hash collisions (rare) by rehashing. Alternative: use sha256, or store ring in a balanced tree (e.g., bisect + array is simple and efficient).