Skip to content

feat: new method GetNodes() #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 86 additions & 15 deletions src/ConsistentHashing/HashRing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}


/// <summary>
/// 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.
/// </summary>
/// <param name="hash">The hash.</param>
/// <param name="n">How many nodes to return. May be fewer than n if n is greater than the number of nodes in the ring.</param>
/// <returns>The nodes that owns the hash, and the following n - 1 nodes.</returns>
public List<TNode> 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}");
Comment on lines +91 to +92
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think an ArgumentException would be better here.

}
else

var toReturn = new List<TNode>();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we can initialize this list with capacity n to avoid resizing.

var seen = new List<TNode>(); // Faster for small values of n, which is the expected use case.
Comment on lines +95 to +96
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is seen needed? Seems like it is the same as toReturn.


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;
}


/// <summary>
/// Removes all instances of the node from the hash ring.
/// </summary>
Expand Down Expand Up @@ -174,6 +220,31 @@ private IEnumerable<Partition<TNode>> GetPartitions()
yield return new Partition<TNode>(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)
Expand Down
14 changes: 14 additions & 0 deletions src/ConsistentHashing/IConsistentHashRing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,19 @@ public interface IConsistentHashRing<TNode> : IEnumerable<(TNode, uint)>
/// <param name="hash">The hash.</param>
/// <returns>The node that owns the hash.</returns>
TNode GetNode(uint hash);

/// <summary>
/// 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.
/// </summary>
/// <param name="hash">The hash.</param>
/// <param name="n">How many nodes to return. May be fewer than n if n is greater than the number of nodes in the ring.</param>
/// <returns>The nodes that owns the hash, and the following n - 1 nodes.</returns>
List<TNode> GetNodes(uint hash, int n);
}
}
5 changes: 5 additions & 0 deletions src/UnitTests/BstHashRing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ IEnumerator IEnumerable.GetEnumerator()
throw new NotImplementedException();
}

public List<TNode> GetNodes(uint hash, int n)
{
throw new NotImplementedException();
}

private class TreeNode
{
public TreeNode(TNode node, uint hashValue)
Expand Down
22 changes: 22 additions & 0 deletions src/UnitTests/HashRingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,28 @@ public void GetNodeForHash()
hashRing.GetNode(100).Should().Be(1);
}

[Fact]
public void GetNodesForHash()
{
IConsistentHashRing<int> 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()
{
Expand Down