diff --git a/clandestined.gemspec b/clandestined.gemspec index d9adfeb..a1d7ca6 100644 --- a/clandestined.gemspec +++ b/clandestined.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'clandestined' - s.version = '1.0.0' + s.version = '1.0.1' s.licenses = ['MIT'] s.date = Time.now.strftime('%Y-%m-%d') s.summary = 'rendezvous hashing implementation based on murmur3 hash' @@ -13,6 +13,8 @@ Gem::Specification.new do |s| s.require_paths = ['lib', 'ext'] s.extensions = ['ext/murmur3_native/extconf.rb'] + %w{digest-siphash}.each { |gem| s.add_dependency gem } + if RUBY_VERSION < "1.9" s.add_development_dependency 'rake', '0.8.7' s.add_development_dependency 'rake-compiler', '0.8.3' diff --git a/lib/clandestined/rendezvous_hash.rb b/lib/clandestined/rendezvous_hash.rb index bfa65a9..115d9f5 100644 --- a/lib/clandestined/rendezvous_hash.rb +++ b/lib/clandestined/rendezvous_hash.rb @@ -1,4 +1,6 @@ require 'murmur3' +require 'string' +require 'digest/siphash' module Clandestined class RendezvousHash @@ -9,11 +11,23 @@ class RendezvousHash attr_reader :seed attr_reader :hash_function - def initialize(nodes=nil, seed=0) + def initialize(nodes=nil, seed=0, hash_type=:murmur) @nodes = nodes || [] @seed = seed - @hash_function = lambda { |key| murmur3_32(key, seed) } + case hash_type + when :murmur + @hash_function = lambda { |key| murmur3_32(key, seed) } + when :siphash + if seed == 0 + # siphash requires 128bit char + seed = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + end + + @hash_function = lambda { |key| + Digest::SipHash.digest(key, seed).reverse.hexencode.to_i(16) + } + end end def add_node(node) diff --git a/lib/string.rb b/lib/string.rb new file mode 100644 index 0000000..b5dc3a8 --- /dev/null +++ b/lib/string.rb @@ -0,0 +1,9 @@ +class String + def hexencode + self.unpack('H*').first + end + + def hexdecode + [self].pack('H*') + end +end diff --git a/test/test_rendezvous_hash.rb b/test/test_rendezvous_hash.rb index 866cb70..ddf92b8 100644 --- a/test/test_rendezvous_hash.rb +++ b/test/test_rendezvous_hash.rb @@ -7,7 +7,7 @@ include Clandestined -class RendezvousHashTestCase < Test::Unit::TestCase +class RendezvousHashMurMurTestCase < Test::Unit::TestCase def test_init_no_options rendezvous = RendezvousHash.new() @@ -86,6 +86,85 @@ def test_find_node_after_addition end +class RendezvousHashSipHashTestCase < Test::Unit::TestCase + + def test_init_no_options + rendezvous = RendezvousHash.new(nil, 0, :siphash) + assert_equal(0, rendezvous.nodes.length) + assert_equal(12268983219697955183, rendezvous.hash_function.call('6666')) + end + + def test_init + nodes = ['0', '1', '2'] + rendezvous = RendezvousHash.new(nodes, 0, :siphash) + assert_equal(3, rendezvous.nodes.length) + assert_equal(12268983219697955183, rendezvous.hash_function.call('6666')) + end + + def test_seed + rendezvous = RendezvousHash.new(nil, "8ed18c1098ec29a2", :siphash) + assert_equal("8ed18c1098ec29a2", rendezvous.seed) + assert_equal(9634808532907877200, rendezvous.hash_function.call('6666')) + end + + def test_add_node + rendezvous = RendezvousHash.new(nil, 0, :siphash) + rendezvous.add_node('1') + assert_equal(1, rendezvous.nodes.length) + rendezvous.add_node('1') + assert_equal(1, rendezvous.nodes.length) + rendezvous.add_node('2') + assert_equal(2, rendezvous.nodes.length) + rendezvous.add_node('1') + assert_equal(2, rendezvous.nodes.length) + end + + def test_remove_node + nodes = ['0', '1', '2'] + rendezvous = RendezvousHash.new(nodes, 0, :siphash) + rendezvous.remove_node('2') + assert_equal(2, rendezvous.nodes.length) + assert_raises(ArgumentError) { rendezvous.remove_node(2, rendezvous.nodes.length) } + assert_equal(2, rendezvous.nodes.length) + rendezvous.remove_node('1') + assert_equal(1, rendezvous.nodes.length) + rendezvous.remove_node('0') + assert_equal(0, rendezvous.nodes.length) + end + + def test_find_node + nodes = ['0', '1', '2'] + rendezvous = RendezvousHash.new(nodes, 0, :siphash) + assert_equal('1', rendezvous.find_node('ok')) + assert_equal('0', rendezvous.find_node('mykey')) + assert_equal('0', rendezvous.find_node('wat')) + end + + def test_find_node_after_removal + nodes = ['0', '1', '2'] + rendezvous = RendezvousHash.new(nodes, 0, :siphash) + rendezvous.remove_node('1') + assert_equal('2', rendezvous.find_node('ok')) + assert_equal('0', rendezvous.find_node('mykey')) + assert_equal('0', rendezvous.find_node('wat')) + end + + def test_find_node_after_addition + nodes = ['0', '1', '2'] + rendezvous = RendezvousHash.new(nodes, 0, :siphash) + assert_equal('1', rendezvous.find_node('ok')) + assert_equal('0', rendezvous.find_node('mykey')) + assert_equal('0', rendezvous.find_node('wat')) + assert_equal('2', rendezvous.find_node('lol')) + rendezvous.add_node('3') + assert_equal('1', rendezvous.find_node('ok')) + assert_equal('3', rendezvous.find_node('mykey')) + assert_equal('3', rendezvous.find_node('wat')) + assert_equal('2', rendezvous.find_node('lol')) + end + +end + class RendezvousHashIntegrationTestCase < Test::Unit::TestCase def test_grow