add_node may over-count when multiple virtual nodes for the new node land in the same existing arc; worth verifying against the testsDate: 2026-05-29
Time: 12:58
add_node TransfersIn consistenthashing.py:27-44, addnode inserts virtual nodes one at a time in a loop and computes a transfer map — which arcs of the ring move from an existing owner to the new node:
for i in range(vnode_count):
pos = _hash(f"{node_id}:{i}")
idx = bisect.bisect_left(self._ring_positions, pos)
if self._ring_positions:
succ_idx = idx % len(self._ring_positions)
old_owner = self._ring_nodes[succ_idx]
if old_owner != node_id:
prev_idx = (idx - 1) % len(self._ring_positions)
arc_start = (self._ring_positions[prev_idx] + 1) % RING_SIZE
transfers[(arc_start, pos)] = (old_owner, node_id)
self._ring_positions.insert(idx, pos) # ring mutated HERE
self._ring_nodes.insert(idx, node_id)
The critical detail: the ring is mutated at the bottom of each iteration (lines 43-44). Each subsequent vnode sees the ring with all previously-inserted vnodes already in place.
Imagine node A owns the arc from position 100 to 500, and new node B gets two vnodes at positions 200 and 300 — both landing in that same arc. The worry: do we record two overlapping transfers that together claim more ring space than actually moved?
The incremental insertion prevents overlap. Walk through the two orderings:
If 200 is processed first:
1. Successor of 200 is 500 (A). Predecessor is 100 (A). Transfer: (101, 200) → A to B. Insert 200.
2. Successor of 300 is 500 (A). Predecessor is now 200 (B) — already in the ring. Transfer: (201, 300) → A to B. Insert 300.
Result: two non-overlapping arcs (101,200) and (201,300), total size 200. Correct.
If 300 is processed first:
1. Successor of 300 is 500 (A). Predecessor is 100 (A). Transfer: (101, 300) → A to B. Insert 300.
2. Successor of 200 is 300 (B). oldowner == nodeid → skipped by the guard on line 38. Insert 200.
Result: one arc (101,300), total size 200. Also correct — the first transfer already covered the full range.
The guard if oldowner != nodeid (line 38) is doing real work here: when a vnode's successor is another vnode of the same new node, that range was already claimed by the earlier insertion. No transfer is needed.
The relevant test at testconsistenthashing.py:139-146:
def test_add_node_returns_transfers():
ring = ConsistentHashRing(num_vnodes=10)
ring.add_node("A")
transfers = ring.add_node("B")
assert len(transfers) > 0
for (start, end), (from_node, to_node) in transfers.items():
assert from_node == "A"
assert to_node == "B"
This test checks that transfers exist and flow from A to B. It does not verify:
So the concern is right to say "worth verifying against the tests" — the code is correct, but the tests are too weak to prove it. A test that sums the arc sizes in the transfer map and compares against B's actual load (via getloaddistribution) would close this gap.
The testminimalredistribution test at line 63 provides indirect validation — it checks that adding node D moves roughly 25% of 10,000 keys — but it tests routing correctness, not transfer-map correctness.
consistent-hashing/consistenthashing.py:removenode — Uses reverse-index iteration to avoid index shifting; the transfer computation has the same arc-boundary logic but processes removals instead of insertionsconsistent-hashing/consistenthashing.py:getload_distribution — Computes actual ring ownership per node; useful as a ground-truth check against the transfer mapconsistent-hashing/consistenthashing.py:getnodes — The replication preference list walk skips duplicate physical nodes; understanding this is key to how RF>1 interacts with virtual nodestransfer-map-test-gap — Writing a test that asserts non-overlapping arcs and total-size correctness for the transfer dict would close the verification gap identified hereconsistent-hashing/testconsistenthashing.py — The test for weighted nodes (line 89) verifies key distribution ratios but doesn't test that weighted vnode counts produce correct transfersadd-node-incremental-ring-mutation — add_node mutates the ring after each vnode insertion, so subsequent vnodes in the same loop see predecessors from earlier iterations, preventing overlapping transfer arcsadd-node-same-owner-guard — The oldowner != nodeid guard at line 38 skips transfer recording when a new vnode's successor is another vnode of the same node, avoiding double-counting already-claimed arcstransfer-test-weak-assertions — testaddnodereturnstransfers only asserts transfer direction (A→B) and existence, not arc non-overlap or total size correctnesstransfer-dict-keyed-by-arc — The transfer dict uses (arcstart, arcend) tuples as keys, so two vnodes producing identical arc boundaries would silently overwrite rather than accumulate