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()
{