diff --git a/src/ConsistentHashing/HashRing.cs b/src/ConsistentHashing/HashRing.cs index d20c062..39d1f91 100644 --- a/src/ConsistentHashing/HashRing.cs +++ b/src/ConsistentHashing/HashRing.cs @@ -61,33 +61,79 @@ public TNode GetNode(uint hash) throw new InvalidOperationException("Ring is empty"); } - int index = this.BinarySearch(hash, false, default(TNode)); - - if (index >= 0) + int index = this.GetNodeIndex(hash); + + return this.ring[index].Node; + } + + + /// + /// Gets the node that owns the hash, and the next n - 1 unique nodes + /// on the ring. This method is useful for implementing the concept of + /// replicas. + /// + /// If a node appears on the ring multiple times as virtual nodes, the + /// first instance will be returned and the remaining appearances will + /// be ignored. toward the limit. + /// + /// The hash. + /// How many nodes to return. May be fewer than n if n is greater than the number of nodes in the ring. + /// The nodes that owns the hash, and the following n - 1 nodes. + public List GetNodes(uint hash, int n) + { + if (this.IsEmpty) { - int prev = index - 1; - while (prev >= 0 && this.ring[prev].Hash == hash) - { - index = prev; - prev--; - } + throw new InvalidOperationException("Ring is empty"); + } - return this.ring[index].Node; + if (n < 1) + { + throw new InvalidOperationException( + $"GetNodes() parameter n must be greater or equal to 1, but it was {n}"); } - else + + var toReturn = new List(); + var seen = new List(); // Faster for small values of n, which is the expected use case. + + int curIndex = this.GetNodeIndex(hash); + n = Math.Min(n, ring.Count); + + // Loop over the ring, reading the hash's node and following nodes + for (int tries = 0; tries < ring.Count; tries++) { - index = ~index; - if (index == this.ring.Count) + // We need to take the entry's ID. + var curNode = ring[curIndex].Node; + if (!seen.Contains(curNode)) { - return this.ring[0].Node; + seen.Add(curNode); + toReturn.Add(curNode); + n--; } else { - return this.ring[index].Node; + // We've already seen curNode node and added it. It's a + // virtual node. Don't re-add it. + } + + if (n == 0) + { + // We've found all of the nodes the caller asked for. + // Return. + break; + } + + if (++curIndex == ring.Count) + { + // Wrap around. We're a ring, aren't we? Faster than + // using modulo every loop. + curIndex = 0; } } + + return toReturn; } + /// /// Removes all instances of the node from the hash ring. /// @@ -174,6 +220,31 @@ private IEnumerable> GetPartitions() yield return new Partition(first.Node, new HashRange(last.Hash, first.Hash)); } + private int GetNodeIndex(uint hash) + { + int index = this.BinarySearch(hash, false, default(TNode)); + + if (index >= 0) + { + int prev = index - 1; + while (prev >= 0 && this.ring[prev].Hash == hash) + { + index = prev; + prev--; + } + } + else + { + index = ~index; + if (index == this.ring.Count) + { + index = 0; + } + } + + return index; + } + struct RingItem { public RingItem(TNode node, uint hash) diff --git a/src/ConsistentHashing/IConsistentHashRing.cs b/src/ConsistentHashing/IConsistentHashRing.cs index 44a9885..8caa7db 100644 --- a/src/ConsistentHashing/IConsistentHashRing.cs +++ b/src/ConsistentHashing/IConsistentHashRing.cs @@ -41,5 +41,19 @@ public interface IConsistentHashRing : IEnumerable<(TNode, uint)> /// The hash. /// The node that owns the hash. TNode GetNode(uint hash); + + /// + /// Gets the node that owns the hash, and the next n - 1 unique nodes + /// on the ring. This method is useful for implementing the concept of + /// replicas. + /// + /// If a node appears on the ring multiple times as virtual nodes, the + /// first instance will be returned and the remaining appearances will + /// be ignored. toward the limit. + /// + /// The hash. + /// How many nodes to return. May be fewer than n if n is greater than the number of nodes in the ring. + /// The nodes that owns the hash, and the following n - 1 nodes. + List GetNodes(uint hash, int n); } } diff --git a/src/UnitTests/BstHashRing.cs b/src/UnitTests/BstHashRing.cs index 137d61b..3ffe0c9 100644 --- a/src/UnitTests/BstHashRing.cs +++ b/src/UnitTests/BstHashRing.cs @@ -207,6 +207,11 @@ IEnumerator IEnumerable.GetEnumerator() throw new NotImplementedException(); } + public List GetNodes(uint hash, int n) + { + throw new NotImplementedException(); + } + private class TreeNode { public TreeNode(TNode node, uint hashValue) diff --git a/src/UnitTests/HashRingTests.cs b/src/UnitTests/HashRingTests.cs index 54aff5e..88b85af 100644 --- a/src/UnitTests/HashRingTests.cs +++ b/src/UnitTests/HashRingTests.cs @@ -257,6 +257,28 @@ public void GetNodeForHash() hashRing.GetNode(100).Should().Be(1); } + [Fact] + public void GetNodesForHash() + { + IConsistentHashRing hashRing = this.CreateRing(); + + hashRing.AddVirtualNodes(1, new uint[] { 100, 300, 500 }); + hashRing.AddVirtualNodes(2, new uint[] { 200, 400, 600 }); + + hashRing.GetNodes(101, 1).Should().Equal(new int[] {2}); + hashRing.GetNodes(101, 2).Should().Equal(new int[] {2, 1}); + hashRing.GetNodes(101, 3).Should().Equal(new int[] {2, 1}); + + hashRing.GetNodes(501, 1).Should().Equal(new int[] {2}); + hashRing.GetNodes(501, 2).Should().Equal(new int[] {2, 1}); + hashRing.GetNodes(501, 3).Should().Equal(new int[] {2, 1}); + + hashRing.GetNodes(601, 1).Should().Equal(new int[] {1}); + hashRing.GetNodes(601, 2).Should().Equal(new int[] {1, 2}); + hashRing.GetNodes(601, 3).Should().Equal(new int[] {1, 2}); + hashRing.GetNodes(601, 100).Should().Equal(new int[] {1, 2}); + } + [Fact] public void VerifyAllHashesInRange() {