Permalink
Browse files

valid and test consistent_hash

  • Loading branch information...
awetzel committed Nov 20, 2013
1 parent 9447a8b commit b64e16dffcca9d0c3bb32553a3fe31dbf6fefcc4
Showing with 33 additions and 2 deletions.
  1. +4 −2 lib/consistent_hash.ex
  2. +29 −0 test/chash_test.exs
View
@@ -1,15 +1,17 @@
defmodule ConsistentHash do
@docmodule "consistent hashing key/node mapping"
+ import Enum
@doc "Map a given key to a node of the ring in a consistent way (ring modifications move a minimum of keys)"
- def node_for_key(ring,key), do: bfind(key,ring)
+ def node_for_key(ring,key), do: bfind(key_as_int(key),ring)
@vnode_per_node 300
@doc "generate the node_for_key ring parameter according to a given node list"
def ring_for_nodes(nodes) do
#Place nodes at @vnode_per_node dots in the key hash space {hash(node++vnode_idx),node},
#then create a bst adapted to consistent hashing traversal, for a given hash, find the next vnode dot in the ring
vnodes = nodes |> flat_map(fn n -> (1..@vnode_per_node |> map &{key_as_int("#{n}#{&1}"),n}) end)
+ |> sort(fn {h1,_},{h2,_}->h2>h1 end)
vnodes |> bsplit({0,trunc(:math.pow(2,160)-1)},vnodes|>first)
end
@@ -23,7 +25,7 @@ defmodule ConsistentHash do
defp bsplit([{h,n}],{_,_},{_,next}), do: {h,n,next} # interval contains a vnode split
defp bsplit(list,{lbound,rbound},next) do # interval contains multiple vnode, recursivly middle split allows easy tree balancing
center = lbound + (rbound - lbound)/2
- {left,right} = list |> partition(fn {h,_n}->h<center end)
+ {left,right} = list |> split_while(fn {h,_n}->h<center end)
{center,bsplit(left,{lbound,center},(right|>first)||next),bsplit(right,{center,rbound},next)}
end
# bsplit is designed to allow standard btree traversing to associate node to hash
View
@@ -0,0 +1,29 @@
+Code.require_file "test_helper.exs", __DIR__
+
+defmodule ChashTest do
+ use ExUnit.Case
+ import Enum
+ import ConsistentHash
+
+ @nb_key 1000
+ @test_set 1..@nb_key |> Enum.map fn _ -> :crypto.rand_bytes(100) end
+ @nodes [:n1,:n2,:n3,:n4,:n5,:n6,:n7]
+
+ test "each node should be assign to roughly the same nb of keys" do
+ ring = ring_for_nodes(@nodes)
+ res_set = @test_set |> map(fn k -> node_for_key(ring,k) end)
+ counts = @nodes |> Enum.map fn n -> (res_set |> Enum.count &(&1==n)) end
+ mean = @nb_key/length(@nodes)
+ assert(counts|>Enum.all?(&(abs(&1-mean) < mean*0.3)))
+ end
+
+ test "if we remove a node, only ~nb_keys/nb_node keys have to move" do
+ ring = ring_for_nodes(@nodes)
+ ring2 = ring_for_nodes(@nodes ++ [:n8])
+ res1_set = @test_set |> map(fn k -> {k,node_for_key(ring,k)} end)
+ res2_set = @test_set |> map(fn k -> {k,node_for_key(ring2,k)} end)
+ diff = Set.difference(res1_set|>HashSet.new,res2_set|>HashSet.new) |> Set.size
+ mean_per_node = Enum.count(@test_set)/length(@nodes)
+ assert(diff < mean_per_node*1.1)
+ end
+end

0 comments on commit b64e16d

Please sign in to comment.