consistent-hashing 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|