consistent-hashing 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +2 -1
- data/Rakefile +1 -0
- data/benchmark/benchmark.rb +42 -0
- data/lib/consistent_hashing.rb +2 -1
- data/lib/consistent_hashing/avl_tree.rb +49 -0
- data/lib/consistent_hashing/ring.rb +5 -14
- data/test/consistent_hashing/test_avl_tree.rb +62 -0
- data/test/consistent_hashing/test_ring.rb +1 -31
- data/version.txt +1 -1
- metadata +23 -6
data/README.md
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
consistent-hashing
|
2
2
|
==================
|
3
3
|
|
4
|
-
A generic implementation of the Consistent Hashing algorithm.
|
4
|
+
A generic implementation of the Consistent Hashing algorithm using an AVL tree.
|
5
5
|
|
6
6
|
Features
|
7
7
|
--------
|
8
8
|
|
9
9
|
* set number of replicas to create multiple virtual points in the ring for each node
|
10
10
|
* nodes can be arbitrary data (e.g. a Memcache client instance)
|
11
|
+
* fast performance through using an AVL tree internally
|
11
12
|
|
12
13
|
Examples
|
13
14
|
--------
|
data/Rakefile
CHANGED
@@ -0,0 +1,42 @@
|
|
1
|
+
$:.unshift(File.join(File.dirname(__FILE__), %w{.. lib}))
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'ipaddr'
|
5
|
+
require 'consistent_hashing'
|
6
|
+
|
7
|
+
def ip(offset)
|
8
|
+
address = IPAddr.new('10.0.0.0').to_i + offset
|
9
|
+
[24, 16, 8, 0].collect {|b| (address >> b) & 255}.join('.')
|
10
|
+
end
|
11
|
+
|
12
|
+
def rand_client_id()
|
13
|
+
(rand * 1_000_000).to_int
|
14
|
+
end
|
15
|
+
|
16
|
+
def benchmark_insertions_lookups()
|
17
|
+
# The initial ring implementation using a combination of hash and sorted list
|
18
|
+
# had the following results when benchmarked:
|
19
|
+
# user system total real
|
20
|
+
# Insertions: 1.260000 0.000000 1.260000 ( 1.259346)
|
21
|
+
# Look ups: 20.080000 0.020000 20.100000 ( 20.111773)
|
22
|
+
#
|
23
|
+
# The ring implementation using an AVLTree has the following results
|
24
|
+
# when benchmarked on the same system:
|
25
|
+
# user system total real
|
26
|
+
# Insertions: 0.060000 0.000000 0.060000 ( 0.062302)
|
27
|
+
# Look ups: 1.020000 0.000000 1.020000 ( 1.028172)
|
28
|
+
#
|
29
|
+
# The performance improvement is ~20x for both insertions and lookups.
|
30
|
+
|
31
|
+
Benchmark.bm(10) do |x|
|
32
|
+
ring = ConsistentHashing::Ring.new(replicas: 200)
|
33
|
+
x.report("Insertions:") {for i in 1..1_000; ring << ip(i); end}
|
34
|
+
x.report("Look ups: ") do
|
35
|
+
for i in 1..100_000
|
36
|
+
ring.point_for(rand_client_id)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
benchmark_insertions_lookups
|
data/lib/consistent_hashing.rb
CHANGED
@@ -10,7 +10,8 @@ module ConsistentHashing
|
|
10
10
|
def self.load_lib
|
11
11
|
require File.join(LIBPATH, 'consistent_hashing', 'virtual_point')
|
12
12
|
require File.join(LIBPATH, 'consistent_hashing', 'ring')
|
13
|
+
require File.join(LIBPATH, 'consistent_hashing', 'avl_tree')
|
13
14
|
end
|
14
15
|
end
|
15
16
|
|
16
|
-
ConsistentHashing.load_lib
|
17
|
+
ConsistentHashing.load_lib
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'avl_tree'
|
2
|
+
|
3
|
+
module ConsistentHashing
|
4
|
+
class AVLTree < ::AVLTree
|
5
|
+
|
6
|
+
def minimum_pair()
|
7
|
+
# Return the key with the smallest key value.
|
8
|
+
return nil if @root.empty?
|
9
|
+
|
10
|
+
current_node = @root
|
11
|
+
while not current_node.left.empty?
|
12
|
+
current_node = current_node.left
|
13
|
+
end
|
14
|
+
|
15
|
+
[current_node.key, current_node.value]
|
16
|
+
end
|
17
|
+
|
18
|
+
def next_gte_pair(key)
|
19
|
+
# Returns the key/value pair with a key that follows the provided key in
|
20
|
+
# sorted order.
|
21
|
+
node = next_gte_node(@root, key)
|
22
|
+
[node.key, node.value] if not node.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def next_gte_node(node, key)
|
28
|
+
return AVLTree::Node::EMPTY if node.empty?
|
29
|
+
|
30
|
+
if key < node.key
|
31
|
+
# The current key qualifies as after the provided key. However, we need
|
32
|
+
# to check the tree on the left to see if there's a key in there also
|
33
|
+
# greater than the provided key but less than the current key.
|
34
|
+
after = next_gte_node(node.left, key)
|
35
|
+
after = node if after.empty?
|
36
|
+
elsif key > node.key
|
37
|
+
# The current key will not be after the provided key, but something
|
38
|
+
# in the right branch maybe. Check the right branch for the first key
|
39
|
+
# larger than our value.
|
40
|
+
after = next_gte_node(node.right, key)
|
41
|
+
elsif node.key == key
|
42
|
+
# An exact match qualifies as the next largest node.
|
43
|
+
after = node
|
44
|
+
end
|
45
|
+
|
46
|
+
return after
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -10,8 +10,7 @@ module ConsistentHashing
|
|
10
10
|
# Public: returns a new ring object
|
11
11
|
def initialize(nodes = [], replicas = 3)
|
12
12
|
@replicas = replicas
|
13
|
-
@
|
14
|
-
@ring = Hash.new
|
13
|
+
@ring = AVLTree.new
|
15
14
|
|
16
15
|
nodes.each { |node| add(node) }
|
17
16
|
end
|
@@ -31,11 +30,8 @@ module ConsistentHashing
|
|
31
30
|
key = hash_key(node, i)
|
32
31
|
|
33
32
|
@ring[key] = VirtualPoint.new(node, key)
|
34
|
-
@sorted_keys << key
|
35
33
|
end
|
36
34
|
|
37
|
-
@sorted_keys.sort!
|
38
|
-
|
39
35
|
self
|
40
36
|
end
|
41
37
|
alias :<< :add
|
@@ -47,7 +43,6 @@ module ConsistentHashing
|
|
47
43
|
key = hash_key(node, i)
|
48
44
|
|
49
45
|
@ring.delete key
|
50
|
-
@sorted_keys.delete key
|
51
46
|
end
|
52
47
|
|
53
48
|
self
|
@@ -58,14 +53,10 @@ module ConsistentHashing
|
|
58
53
|
#
|
59
54
|
def point_for(key)
|
60
55
|
return nil if @ring.empty?
|
61
|
-
|
62
56
|
key = hash_key(key)
|
63
|
-
|
64
|
-
@
|
65
|
-
|
66
|
-
end
|
67
|
-
|
68
|
-
@ring[@sorted_keys[0]]
|
57
|
+
_, value = @ring.next_gte_pair(key)
|
58
|
+
_, value = @ring.minimum_pair unless value
|
59
|
+
value
|
69
60
|
end
|
70
61
|
|
71
62
|
# Public: gets the node where to store the key
|
@@ -100,4 +91,4 @@ module ConsistentHashing
|
|
100
91
|
Digest::MD5.hexdigest(key.to_s)[0..16].hex
|
101
92
|
end
|
102
93
|
end
|
103
|
-
end
|
94
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w{ .. test_consistent_hashing})
|
2
|
+
|
3
|
+
class TestAVLTree < ConsistentHashing::TestCase
|
4
|
+
AVLTree = ConsistentHashing::AVLTree
|
5
|
+
|
6
|
+
def test_minimum_returns_nil_if_empty
|
7
|
+
tree = AVLTree.new
|
8
|
+
assert_nil tree.minimum_pair
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_minimum_returns_root_node_if_only_node
|
12
|
+
tree = AVLTree.new
|
13
|
+
tree[0]= 1
|
14
|
+
assert_equal(tree.minimum_pair, [0, 1])
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_minimum_returns_left_most_leaf
|
18
|
+
tree = AVLTree.new
|
19
|
+
1.upto(10) do |i|
|
20
|
+
tree[i] = i.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
assert_equal(tree.minimum_pair, [1, "1"])
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_next_gte_pair_returns_nil_if_empty
|
27
|
+
tree = AVLTree.new
|
28
|
+
assert_nil tree.next_gte_pair("some key")
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_next_gte_pair_finds_exact_match_key
|
32
|
+
tree = AVLTree.new
|
33
|
+
1.upto(10) do |i|
|
34
|
+
tree[i] = i.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
1.upto(10) do |i|
|
38
|
+
assert_equal(tree.next_gte_pair(i), [i, i.to_s])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_next_gte_pair_finds_slightly_larger_key
|
43
|
+
tree = AVLTree.new
|
44
|
+
(2..11).step(2) do |i|
|
45
|
+
tree[i] = i.to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
(1..10).step(2) do |i|
|
49
|
+
assert_equal(tree.next_gte_pair(i), [i + 1, (i + 1).to_s])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_next_gte_pair_returns_nil_if_no_larger_keys
|
54
|
+
tree = AVLTree.new
|
55
|
+
(2..11).step(2) do |i|
|
56
|
+
tree[i] = i.to_s
|
57
|
+
end
|
58
|
+
|
59
|
+
assert_nil tree.next_gte_pair(12)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
@@ -51,34 +51,4 @@ class TestRing < ConsistentHashing::TestCase
|
|
51
51
|
def test_point_for
|
52
52
|
assert_equal "C", @ring.node_for(@examples["C"])
|
53
53
|
end
|
54
|
-
|
55
|
-
def test_arbitrary_node_data
|
56
|
-
nodes = [
|
57
|
-
{"host" => "192.168.1.101"},
|
58
|
-
{"host" => "192.168.1.102"},
|
59
|
-
{"host" => "192.168.1.103"}
|
60
|
-
]
|
61
|
-
|
62
|
-
ring = ConsistentHashing::Ring.new(nodes)
|
63
|
-
assert_equal 9, ring.length
|
64
|
-
|
65
|
-
ring_nodes = ring.nodes
|
66
|
-
assert_equal 3, ring_nodes.length
|
67
|
-
assert_equal "192.168.1.102", ring_nodes[1]['host']
|
68
|
-
end
|
69
|
-
|
70
|
-
def test_points
|
71
|
-
nodes = [
|
72
|
-
{"host" => "192.168.1.101"},
|
73
|
-
{"host" => "192.168.1.102"},
|
74
|
-
{"host" => "192.168.1.103"}
|
75
|
-
]
|
76
|
-
|
77
|
-
ring = ConsistentHashing::Ring.new(nodes)
|
78
|
-
points = ring.points
|
79
|
-
|
80
|
-
assert_equal 9, points.length
|
81
|
-
assert_not_equal 0, points[0].index
|
82
|
-
assert_equal "192.168.1.101", points[0].node['host']
|
83
|
-
end
|
84
|
-
end
|
54
|
+
end
|
data/version.txt
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: consistent-hashing
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,22 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-09-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: avl_tree
|
16
|
+
requirement: &70236637210800 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.1.3
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70236637210800
|
14
25
|
- !ruby/object:Gem::Dependency
|
15
26
|
name: bones
|
16
|
-
requirement: &
|
27
|
+
requirement: &70236637209960 !ruby/object:Gem::Requirement
|
17
28
|
none: false
|
18
29
|
requirements:
|
19
30
|
- - ! '>='
|
@@ -21,8 +32,9 @@ dependencies:
|
|
21
32
|
version: 3.8.0
|
22
33
|
type: :development
|
23
34
|
prerelease: false
|
24
|
-
version_requirements: *
|
25
|
-
description: A generic implementation of the Consistent Hashing algorithm
|
35
|
+
version_requirements: *70236637209960
|
36
|
+
description: A generic implementation of the Consistent Hashing algorithm using an
|
37
|
+
AVL tree.
|
26
38
|
email: liebler.dominik@googlemail.com
|
27
39
|
executables: []
|
28
40
|
extensions: []
|
@@ -33,9 +45,12 @@ files:
|
|
33
45
|
- History.txt
|
34
46
|
- README.md
|
35
47
|
- Rakefile
|
48
|
+
- benchmark/benchmark.rb
|
36
49
|
- lib/consistent_hashing.rb
|
50
|
+
- lib/consistent_hashing/avl_tree.rb
|
37
51
|
- lib/consistent_hashing/ring.rb
|
38
52
|
- lib/consistent_hashing/virtual_point.rb
|
53
|
+
- test/consistent_hashing/test_avl_tree.rb
|
39
54
|
- test/consistent_hashing/test_ring.rb
|
40
55
|
- test/consistent_hashing/test_virtual_point.rb
|
41
56
|
- test/test_consistent_hashing.rb
|
@@ -65,8 +80,10 @@ rubyforge_project: consistent-hashing
|
|
65
80
|
rubygems_version: 1.8.16
|
66
81
|
signing_key:
|
67
82
|
specification_version: 3
|
68
|
-
summary: A generic implementation of the Consistent Hashing algorithm
|
83
|
+
summary: A generic implementation of the Consistent Hashing algorithm using an AVL
|
84
|
+
tree.
|
69
85
|
test_files:
|
86
|
+
- test/consistent_hashing/test_avl_tree.rb
|
70
87
|
- test/consistent_hashing/test_ring.rb
|
71
88
|
- test/consistent_hashing/test_virtual_point.rb
|
72
89
|
- test/test_consistent_hashing.rb
|