Build an in-memory consistent-hashing router and process a sequence of operations. Use virtual nodes for each server. The hash function is fixed for this problem: H(s) = sum((i + 1) * ord(s[i]) for i in range(len(s))). Each serverId creates virtual nodes with keys serverId + '#' + str(vnodeIndex) for vnodeIndex from 0 to virtual_nodes - 1. Put all virtual nodes on a ring sorted by (hash, serverId, vnodeIndex). To route a request, compute H(requestId), find the first virtual node whose tuple hash is at least that value, and return its serverId. If none exists, wrap around to the first virtual node. Adding an already active server should do nothing, and removing a missing server should do nothing.
Examples
Input: ([('addServer', 'A'), ('addServer', 'B'), ('route', 'Ne'), ('route', 'Oe'), ('route', 'Qe'), ('route', 'zz')], 2)
Expected Output: ['B', 'A', 'B', 'A']
Explanation: With 2 virtual nodes each, the ring positions are A#0=279, B#0=280, A#1=282, B#1=283. The request hashes are Ne=280, Oe=281, Qe=283, zz=366, so routing is B, A, B, then wrap to A.
Input: ([('addServer', 'A'), ('addServer', 'B'), ('route', 'Ne'), ('removeServer', 'B'), ('route', 'Ne'), ('route', 'Oe')], 2)
Expected Output: ['B', 'A', 'A']
Explanation: Before removal, Ne hashes to B. After removing B, both Ne=280 and Oe=281 route to A's next virtual node at 282.
Input: ([('addServer', 'A'), ('route', 'Ne'), ('addServer', 'B'), ('route', 'Ne'), ('route', 'Pe')], 2)
Expected Output: ['A', 'B', 'A']
Explanation: With only A active, Ne routes to A. After adding B, Ne remaps to B's virtual node at 280, while Pe=282 still maps to A.
Input: ([('route', 'x'), ('addServer', 'A'), ('removeServer', 'A'), ('route', 'y')], 3)
Expected Output: [None, None]
Explanation: There are no active servers at either route call.
Solution
def solution(operations, virtual_nodes):
from bisect import bisect_left, insort
def ring_hash(s):
total = 0
for i, ch in enumerate(s):
total += (i + 1) * ord(ch)
return total
ring = []
server_to_nodes = {}
result = []
for op in operations:
action = op[0]
if action == 'addServer':
sid = op[1]
if sid in server_to_nodes:
continue
nodes = []
for i in range(virtual_nodes):
entry = (ring_hash(f'{sid}#{i}'), sid, i)
insort(ring, entry)
nodes.append(entry)
server_to_nodes[sid] = nodes
elif action == 'removeServer':
sid = op[1]
nodes = server_to_nodes.pop(sid, None)
if not nodes:
continue
for entry in nodes:
idx = bisect_left(ring, entry)
if idx < len(ring) and ring[idx] == entry:
ring.pop(idx)
else: # route
request_id = op[1]
if not ring:
result.append(None)
continue
key = (ring_hash(request_id), '', -1)
idx = bisect_left(ring, key)
if idx == len(ring):
idx = 0
result.append(ring[idx][1])
return resultTime complexity: Routing: O(log M). Adding or removing one server: O(V * M) worst-case with Python list insertions/removals, where V is virtual_nodes and M is the current number of virtual nodes on the ring.. Space complexity: O(M), where M is the number of active virtual nodes.