From 9772914a7ff763b81cea0d099e0ae4804ab2782c Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Wed, 3 Mar 2021 14:49:48 -0500 Subject: [PATCH 1/6] checkpoint: initial add of GetNodes(). Not tested. --- src/ConsistentHashing/HashRing.cs | 81 +++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 19 deletions(-) diff --git a/src/ConsistentHashing/HashRing.cs b/src/ConsistentHashing/HashRing.cs index d20c062..c72fdae 100644 --- a/src/ConsistentHashing/HashRing.cs +++ b/src/ConsistentHashing/HashRing.cs @@ -1,4 +1,6 @@ -namespace ConsistentHashing +using System.Runtime.InteropServices; + +namespace ConsistentHashing { using System; using System.Collections; @@ -61,33 +63,49 @@ 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 nodes in the ring. + /// + /// The hash. + /// How many nodes to return. May be less than n if n is greater than the number of nodes in the ring. + /// The node that owns the hash. + 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 nodes = new List(); + + int curIndex = this.GetNodeIndex(hash); + n = Math.Min(n, ring.Count); + while (n-- > 0) { - index = ~index; - if (index == this.ring.Count) - { - return this.ring[0].Node; - } - else + nodes.Add(ring[curIndex].Node); + + if (++curIndex == ring.Count) { - return this.ring[index].Node; + curIndex = 0; } } + + return nodes; } + /// /// Removes all instances of the node from the hash ring. /// @@ -174,6 +192,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) From babbb5006b3b4a4622255d66d55fd5b10350d8d0 Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Wed, 3 Mar 2021 15:24:24 -0500 Subject: [PATCH 2/6] checkpoint: refine GetNodes(), add tests --- src/ConsistentHashing/HashRing.cs | 37 ++- src/ConsistentHashing/IConsistentHashRing.cs | 11 + src/UnitTests/BstHashRing.cs | 232 ------------------- src/UnitTests/HashRingTests.cs | 22 ++ 4 files changed, 65 insertions(+), 237 deletions(-) delete mode 100644 src/UnitTests/BstHashRing.cs diff --git a/src/ConsistentHashing/HashRing.cs b/src/ConsistentHashing/HashRing.cs index c72fdae..e9ab95b 100644 --- a/src/ConsistentHashing/HashRing.cs +++ b/src/ConsistentHashing/HashRing.cs @@ -70,7 +70,10 @@ public TNode GetNode(uint hash) /// - /// Gets the node that owns the hash, and the next n - 1 nodes in the ring. + /// Gets the node that owns the hash, and the next n - 1 unique nodes + /// in the ring. If a node appears on the ring multiple times as + /// virtual nodes, only a single instance will be be returned and count + /// toward the limit. /// /// The hash. /// How many nodes to return. May be less than n if n is greater than the number of nodes in the ring. @@ -88,21 +91,45 @@ public List GetNodes(uint hash, int n) $"GetNodes() parameter n must be greater or equal to 1, but it was {n}"); } - var nodes = new List(); + 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); - while (n-- > 0) + + // Loop over the ring, reading the hash's node and following nodes + for (int tries = 0; tries < ring.Count; tries++) { - nodes.Add(ring[curIndex].Node); + // We need to take the entry's ID. + var curNode = ring[curIndex].Node; + if (!seen.Contains(curNode)) + { + seen.Add(curNode); + toReturn.Add(curNode); + n--; + } + else + { + // 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 nodes; + return toReturn; } diff --git a/src/ConsistentHashing/IConsistentHashRing.cs b/src/ConsistentHashing/IConsistentHashRing.cs index 44a9885..f58dfb2 100644 --- a/src/ConsistentHashing/IConsistentHashRing.cs +++ b/src/ConsistentHashing/IConsistentHashRing.cs @@ -41,5 +41,16 @@ 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 + /// in the ring. If a node appears on the ring multiple times as + /// virtual nodes, only a single instance will be be returned and count + /// toward the limit. + /// + /// The hash. + /// How many nodes to return. May be less than n if n is greater than the number of nodes in the ring. + /// The node that owns the hash. + List GetNodes(uint hash, int n); } } diff --git a/src/UnitTests/BstHashRing.cs b/src/UnitTests/BstHashRing.cs deleted file mode 100644 index 137d61b..0000000 --- a/src/UnitTests/BstHashRing.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace ConsistentHashing -{ - public class BstHashRing : IConsistentHashRing - where TNode : IComparable - { - private TreeNode root; - - public BstHashRing() - { - } - - public IEnumerable> Partitions => - EnumerateRangeAssignments(this.root); - - public bool IsEmpty => this.root == null; - - public void AddNode(TNode node, uint point) - { - AddNode(ref this.root, new TreeNode(node, point)); - } - - public void RemoveNode(TNode node) - { - throw new NotImplementedException(); - } - - public TNode GetNode(uint hash) - { - if (this.root == null) - { - throw new InvalidOperationException("Hash ring is empty"); - } - - TreeNode n = GetNode(this.root); - return n == null ? GetMin(this.root).Node : n.Node; - - TreeNode GetNode(TreeNode root) - { - TreeNode result = null; - - while (root != null) - { - if (root.HashValue > hash) - { - result = root; - root = root.Left; - } - else if (root.HashValue < hash) - { - root = root.Right; - } - else - { - return root; - } - } - - return result; - } - } - - private static TreeNode GetMin(TreeNode root) - { - if (root == null) - { - return null; - } - - while (root.Left != null) - { - root = root.Left; - } - - return root; - } - - private static IEnumerable> EnumerateRangeAssignments(TreeNode root) - { - if (root == null) - { - yield break; - } - - foreach ((TreeNode first, TreeNode second) in Pairs(InOrderTraversal(root))) - { - yield return new Partition(second.Node, new HashRange(first.HashValue, second.HashValue)); - } - } - - private static IEnumerable<(TreeNode, TreeNode)> Pairs(IEnumerable nodes) - { - TreeNode first = null; - TreeNode curr = null; - - foreach (TreeNode n in nodes) - { - if (first == null) - { - first = n; - } - - if (curr == null) - { - curr = n; - continue; - } - - yield return (curr, n); - curr = n; - } - - yield return (curr, first); - } - - private static IEnumerable InOrderTraversal(TreeNode root) - { - if (root == null) - { - yield break; - } - - // Invariant: all items in the stack except - // for the top item are non-null. The top item - // _may_ be null. - Stack stack = new Stack(); - stack.Push(root); - - while (stack.Count != 0) - { - var n = stack.Peek(); - - if (n == null) - { - // Pop the null element off - stack.Pop(); - - if (stack.Count == 0) - { - break; - } - - // This is non-null - n = stack.Pop(); - yield return n; - stack.Push(n.Right); - continue; - } - - stack.Push(n.Left); - } - } - - private static void AddNode(ref TreeNode root, TreeNode newNode) - { - if (root == null) - { - root = newNode; - return; - } - - TreeNode pointer = root; - - while (pointer != null) - { - if (pointer.HashValue > newNode.HashValue) - { - if (pointer.Left != null) - { - pointer = pointer.Left; - } - else - { - pointer.Left = newNode; - return; - } - } - else if (pointer.HashValue < newNode.HashValue) - { - if (pointer.Right != null) - { - pointer = pointer.Right; - } - else - { - pointer.Right = newNode; - return; - } - } - else - { - throw new ArgumentException("Two nodes with same hash value"); - } - } - } - - public IEnumerator<(TNode, uint)> GetEnumerator() - { - throw new NotImplementedException(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - throw new NotImplementedException(); - } - - private class TreeNode - { - public TreeNode(TNode node, uint hashValue) - { - this.Node = node; - this.HashValue = hashValue; - } - - public TNode Node { get; } - - public uint HashValue { get; } - - public TreeNode Left { get; set; } - - public TreeNode Right { get; set; } - - public override string ToString() - { - return $"{this.Node} - {this.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() { From db7a53d9a46a437e527326b1f4069e78d7670bdb Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Wed, 3 Mar 2021 15:28:53 -0500 Subject: [PATCH 3/6] chore: clean up diff --- src/ConsistentHashing/HashRing.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ConsistentHashing/HashRing.cs b/src/ConsistentHashing/HashRing.cs index e9ab95b..5fc87ae 100644 --- a/src/ConsistentHashing/HashRing.cs +++ b/src/ConsistentHashing/HashRing.cs @@ -1,6 +1,4 @@ -using System.Runtime.InteropServices; - -namespace ConsistentHashing +namespace ConsistentHashing { using System; using System.Collections; From ee597f201bb0e23954ed840beed367985de970ee Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Wed, 3 Mar 2021 15:32:01 -0500 Subject: [PATCH 4/6] chore: add back deleted file --- src/UnitTests/BstHashRing.cs | 232 +++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 src/UnitTests/BstHashRing.cs diff --git a/src/UnitTests/BstHashRing.cs b/src/UnitTests/BstHashRing.cs new file mode 100644 index 0000000..137d61b --- /dev/null +++ b/src/UnitTests/BstHashRing.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace ConsistentHashing +{ + public class BstHashRing : IConsistentHashRing + where TNode : IComparable + { + private TreeNode root; + + public BstHashRing() + { + } + + public IEnumerable> Partitions => + EnumerateRangeAssignments(this.root); + + public bool IsEmpty => this.root == null; + + public void AddNode(TNode node, uint point) + { + AddNode(ref this.root, new TreeNode(node, point)); + } + + public void RemoveNode(TNode node) + { + throw new NotImplementedException(); + } + + public TNode GetNode(uint hash) + { + if (this.root == null) + { + throw new InvalidOperationException("Hash ring is empty"); + } + + TreeNode n = GetNode(this.root); + return n == null ? GetMin(this.root).Node : n.Node; + + TreeNode GetNode(TreeNode root) + { + TreeNode result = null; + + while (root != null) + { + if (root.HashValue > hash) + { + result = root; + root = root.Left; + } + else if (root.HashValue < hash) + { + root = root.Right; + } + else + { + return root; + } + } + + return result; + } + } + + private static TreeNode GetMin(TreeNode root) + { + if (root == null) + { + return null; + } + + while (root.Left != null) + { + root = root.Left; + } + + return root; + } + + private static IEnumerable> EnumerateRangeAssignments(TreeNode root) + { + if (root == null) + { + yield break; + } + + foreach ((TreeNode first, TreeNode second) in Pairs(InOrderTraversal(root))) + { + yield return new Partition(second.Node, new HashRange(first.HashValue, second.HashValue)); + } + } + + private static IEnumerable<(TreeNode, TreeNode)> Pairs(IEnumerable nodes) + { + TreeNode first = null; + TreeNode curr = null; + + foreach (TreeNode n in nodes) + { + if (first == null) + { + first = n; + } + + if (curr == null) + { + curr = n; + continue; + } + + yield return (curr, n); + curr = n; + } + + yield return (curr, first); + } + + private static IEnumerable InOrderTraversal(TreeNode root) + { + if (root == null) + { + yield break; + } + + // Invariant: all items in the stack except + // for the top item are non-null. The top item + // _may_ be null. + Stack stack = new Stack(); + stack.Push(root); + + while (stack.Count != 0) + { + var n = stack.Peek(); + + if (n == null) + { + // Pop the null element off + stack.Pop(); + + if (stack.Count == 0) + { + break; + } + + // This is non-null + n = stack.Pop(); + yield return n; + stack.Push(n.Right); + continue; + } + + stack.Push(n.Left); + } + } + + private static void AddNode(ref TreeNode root, TreeNode newNode) + { + if (root == null) + { + root = newNode; + return; + } + + TreeNode pointer = root; + + while (pointer != null) + { + if (pointer.HashValue > newNode.HashValue) + { + if (pointer.Left != null) + { + pointer = pointer.Left; + } + else + { + pointer.Left = newNode; + return; + } + } + else if (pointer.HashValue < newNode.HashValue) + { + if (pointer.Right != null) + { + pointer = pointer.Right; + } + else + { + pointer.Right = newNode; + return; + } + } + else + { + throw new ArgumentException("Two nodes with same hash value"); + } + } + } + + public IEnumerator<(TNode, uint)> GetEnumerator() + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + + private class TreeNode + { + public TreeNode(TNode node, uint hashValue) + { + this.Node = node; + this.HashValue = hashValue; + } + + public TNode Node { get; } + + public uint HashValue { get; } + + public TreeNode Left { get; set; } + + public TreeNode Right { get; set; } + + public override string ToString() + { + return $"{this.Node} - {this.HashValue}"; + } + } + } +} From c3fae26997d9c80b07b38fa88f666cc9c12a181a Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Wed, 3 Mar 2021 15:32:57 -0500 Subject: [PATCH 5/6] build: fix unimplemented error --- src/UnitTests/BstHashRing.cs | 5 +++++ 1 file changed, 5 insertions(+) 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) From 3791e3077aa21baeb6ed94863d4f1ebc65076660 Mon Sep 17 00:00:00 2001 From: Tristan Pratt Date: Wed, 3 Mar 2021 15:36:24 -0500 Subject: [PATCH 6/6] docs: improve function doc wording --- src/ConsistentHashing/HashRing.cs | 13 ++++++++----- src/ConsistentHashing/IConsistentHashRing.cs | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/ConsistentHashing/HashRing.cs b/src/ConsistentHashing/HashRing.cs index 5fc87ae..39d1f91 100644 --- a/src/ConsistentHashing/HashRing.cs +++ b/src/ConsistentHashing/HashRing.cs @@ -69,13 +69,16 @@ public TNode GetNode(uint hash) /// /// Gets the node that owns the hash, and the next n - 1 unique nodes - /// in the ring. If a node appears on the ring multiple times as - /// virtual nodes, only a single instance will be be returned and count - /// toward the limit. + /// 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 less than n if n is greater than the number of nodes in the ring. - /// The node that owns 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) diff --git a/src/ConsistentHashing/IConsistentHashRing.cs b/src/ConsistentHashing/IConsistentHashRing.cs index f58dfb2..8caa7db 100644 --- a/src/ConsistentHashing/IConsistentHashRing.cs +++ b/src/ConsistentHashing/IConsistentHashRing.cs @@ -44,13 +44,16 @@ public interface IConsistentHashRing : IEnumerable<(TNode, uint)> /// /// Gets the node that owns the hash, and the next n - 1 unique nodes - /// in the ring. If a node appears on the ring multiple times as - /// virtual nodes, only a single instance will be be returned and count - /// toward the limit. + /// 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 less than n if n is greater than the number of nodes in the ring. - /// The node that owns 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); } }