clandestined 1.0.0a
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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +21 -0
- data/README.md +147 -0
- data/Rakefile +33 -0
- data/clandestined.gemspec +24 -0
- data/ext/murmur3_native/extconf.rb +22 -0
- data/ext/murmur3_native/murmur3_native.c +77 -0
- data/lib/clandestined/cluster.rb +111 -0
- data/lib/clandestined/rendezvous_hash.rb +37 -0
- data/lib/murmur3.rb +9 -0
- data/lib/murmur3_ruby.rb +50 -0
- data/test/test_cluster.rb +462 -0
- data/test/test_rendezvous_hash.rb +180 -0
- metadata +104 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 47d9cefad2b6c0c74a35295e37f64aa81f8bca19
|
4
|
+
data.tar.gz: ce0099265bc7b8b3929228550a657edc1057e027
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cd241d8e5896d6608f81477ea23ec575bc9f55e71ca3917705a4217cf7d84a22e5c916271413d6d42a4bd2e81a73af34c4b1bd9ae57ae72a10346dbb84804b26
|
7
|
+
data.tar.gz: f343ef64e349cc3805b1f8e994fc9bc95865e150fad80e15f75840511f90ff7e3250bf66d3e01da69594e413f236097f78b63a57f31242672a43c2c5c8c8cf9d
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
clandestined (1.0.0a)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
rake (0.9.6)
|
10
|
+
rake-compiler (0.8.3)
|
11
|
+
rake
|
12
|
+
rubydoctest (1.1.3)
|
13
|
+
|
14
|
+
PLATFORMS
|
15
|
+
ruby
|
16
|
+
|
17
|
+
DEPENDENCIES
|
18
|
+
clandestined!
|
19
|
+
rake (= 0.9.6)
|
20
|
+
rake-compiler (= 0.8.3)
|
21
|
+
rubydoctest (= 1.1.3)
|
data/README.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
clandestined
|
2
|
+
===========
|
3
|
+
|
4
|
+
rendezvous hashing implementation based on murmur3 hash
|
5
|
+
|
6
|
+
|
7
|
+
## motiviation
|
8
|
+
|
9
|
+
in distributed systems, the need often arises to locate objects amongst a
|
10
|
+
cluster of machines. consistent hashing and rendezvous hashing are methods of
|
11
|
+
performing this task, while minimizing data movement on cluster topology
|
12
|
+
changes.
|
13
|
+
|
14
|
+
clandestined is a library for rendezvous hashing which has the goal of simple
|
15
|
+
clients and ease of use.
|
16
|
+
|
17
|
+
Currently targetting for support:
|
18
|
+
- Ruby 1.8.6 through Ruby 2.1.x
|
19
|
+
|
20
|
+
[](https://travis-ci.org/ewdurbin/clandestined-ruby)
|
21
|
+
|
22
|
+
## example usage
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
>> require 'clandestined/cluster'
|
26
|
+
>>
|
27
|
+
>> nodes = Hash[
|
28
|
+
'1' => Hash['name' => 'node1.example.com', 'zone' => 'us-east-1a'],
|
29
|
+
'2' => Hash['name' => 'node2.example.com', 'zone' => 'us-east-1a'],
|
30
|
+
'3' => Hash['name' => 'node3.example.com', 'zone' => 'us-east-1a'],
|
31
|
+
'4' => Hash['name' => 'node4.example.com', 'zone' => 'us-east-1b'],
|
32
|
+
'5' => Hash['name' => 'node5.example.com', 'zone' => 'us-east-1b'],
|
33
|
+
'6' => Hash['name' => 'node6.example.com', 'zone' => 'us-east-1b'],
|
34
|
+
'7' => Hash['name' => 'node7.example.com', 'zone' => 'us-east-1c'],
|
35
|
+
'8' => Hash['name' => 'node8.example.com', 'zone' => 'us-east-1c'],
|
36
|
+
'9' => Hash['name' => 'node9.example.com', 'zone' => 'us-east-1c'],
|
37
|
+
]
|
38
|
+
>>
|
39
|
+
>> cluster = Cluster.new(nodes)
|
40
|
+
>> cluster.find_nodes('mykey')
|
41
|
+
=> ["4", "8"]
|
42
|
+
```
|
43
|
+
|
44
|
+
by default, `Cluster` will place 2 replicas around the cluster taking care to
|
45
|
+
place the second replica in a separate zone from the first.
|
46
|
+
|
47
|
+
in the event that your cluster doesn't need zone awareness, you can either
|
48
|
+
invoke the `RendezvousHash` class directly, or use a `Cluster` with replicas
|
49
|
+
set to 1
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
>> require 'clandestined/cluster'
|
53
|
+
>> require 'clandestined/rendezvous_hash'
|
54
|
+
>>
|
55
|
+
>> nodes = Hash[
|
56
|
+
'1' => Hash['name' => 'node1.example.com'],
|
57
|
+
'2' => Hash['name' => 'node2.example.com'],
|
58
|
+
'3' => Hash['name' => 'node3.example.com'],
|
59
|
+
'4' => Hash['name' => 'node4.example.com'],
|
60
|
+
'5' => Hash['name' => 'node5.example.com'],
|
61
|
+
'6' => Hash['name' => 'node6.example.com'],
|
62
|
+
'7' => Hash['name' => 'node7.example.com'],
|
63
|
+
'8' => Hash['name' => 'node8.example.com'],
|
64
|
+
'9' => Hash['name' => 'node9.example.com'],
|
65
|
+
]
|
66
|
+
>>
|
67
|
+
>> cluster = Cluster.new(nodes, 1)
|
68
|
+
>> rendezvous = RendezvousHash.new(nodes.keys)
|
69
|
+
>>
|
70
|
+
>> cluster.find_nodes('mykey')
|
71
|
+
=> ["4"]
|
72
|
+
>> rendezvous.find_node('mykey')
|
73
|
+
=> "4"
|
74
|
+
```
|
75
|
+
|
76
|
+
## advanced usage
|
77
|
+
|
78
|
+
### murmur3 seeding
|
79
|
+
|
80
|
+
if you plan to use keys based on untrusted input (not really supported, but go
|
81
|
+
ahead), it would be best to use a custom seed for hashing. although this
|
82
|
+
technique is by no means a way to fully mitigate a DoS attack using crafted
|
83
|
+
keys, it may make you sleep better at night.
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
>> require 'clandestined/cluster'
|
87
|
+
>> require 'clandestined/rendezvous_hash'
|
88
|
+
>>
|
89
|
+
>> nodes = Hash[
|
90
|
+
'1' => Hash['name' => 'node1.example.com'],
|
91
|
+
'2' => Hash['name' => 'node2.example.com'],
|
92
|
+
'3' => Hash['name' => 'node3.example.com'],
|
93
|
+
'4' => Hash['name' => 'node4.example.com'],
|
94
|
+
'5' => Hash['name' => 'node5.example.com'],
|
95
|
+
'6' => Hash['name' => 'node6.example.com'],
|
96
|
+
'7' => Hash['name' => 'node7.example.com'],
|
97
|
+
'8' => Hash['name' => 'node8.example.com'],
|
98
|
+
'9' => Hash['name' => 'node9.example.com'],
|
99
|
+
]
|
100
|
+
>>
|
101
|
+
>> cluster = Cluster.new(nodes, 1, 1337)
|
102
|
+
>> rendezvous = RendezvousHash.new(nodes.keys, 1337)
|
103
|
+
>>
|
104
|
+
>> cluster.find_nodes('mykey')
|
105
|
+
=> ["7"]
|
106
|
+
>> rendezvous.find_node('mykey')
|
107
|
+
=> "7"
|
108
|
+
```
|
109
|
+
|
110
|
+
### supplying your own hash function
|
111
|
+
|
112
|
+
a more robust, but possibly slower solution to mitigate DoS vulnerability by
|
113
|
+
crafted key might be to supply your own cryptograpic hash function.
|
114
|
+
|
115
|
+
in order for this to work, your method must be supplied to the `RendezvousHash`
|
116
|
+
or `Cluster` object as a callable which takes a byte string `key` and returns
|
117
|
+
an integer.
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
>> require 'digest'
|
121
|
+
>> require 'clandestined/cluster'
|
122
|
+
>> require 'clandestined/rendezvous_hash'
|
123
|
+
>>
|
124
|
+
>> nodes = Hash[
|
125
|
+
'1' => Hash['name' => 'node1.example.com'],
|
126
|
+
'2' => Hash['name' => 'node2.example.com'],
|
127
|
+
'3' => Hash['name' => 'node3.example.com'],
|
128
|
+
'4' => Hash['name' => 'node4.example.com'],
|
129
|
+
'5' => Hash['name' => 'node5.example.com'],
|
130
|
+
'6' => Hash['name' => 'node6.example.com'],
|
131
|
+
'7' => Hash['name' => 'node7.example.com'],
|
132
|
+
'8' => Hash['name' => 'node8.example.com'],
|
133
|
+
'9' => Hash['name' => 'node9.example.com'],
|
134
|
+
]
|
135
|
+
>>
|
136
|
+
>> def my_hash_function(key)
|
137
|
+
Digest::SHA1.hexdigest(key).to_i(16)
|
138
|
+
end
|
139
|
+
>>
|
140
|
+
>> cluster = Cluster.new(nodes, 1, 0, method(:my_hash_function))
|
141
|
+
>> rendezvous = RendezvousHash.new(nodes.keys, 0, method(:my_hash_function))
|
142
|
+
>>
|
143
|
+
>> cluster.find_nodes('mykey')
|
144
|
+
=> ["1"]
|
145
|
+
>> rendezvous.find_node('mykey')
|
146
|
+
=> "1"
|
147
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/extensiontask'
|
4
|
+
|
5
|
+
def can_compile_extensions
|
6
|
+
if defined? RUBY_DESCRIPTION
|
7
|
+
if RUBY_DESCRIPTION =~ /jruby/
|
8
|
+
false
|
9
|
+
else
|
10
|
+
true
|
11
|
+
end
|
12
|
+
else
|
13
|
+
true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Rake::ExtensionTask.new('murmur3_native')
|
18
|
+
|
19
|
+
Rake::TestTask.new do |t|
|
20
|
+
t.libs << "ext"
|
21
|
+
t.libs << "test"
|
22
|
+
t.test_files = FileList['test/test*.rb']
|
23
|
+
end
|
24
|
+
|
25
|
+
task :test_docs do
|
26
|
+
system("rubydoctest README.md") or exit!(1)
|
27
|
+
end
|
28
|
+
|
29
|
+
if can_compile_extensions
|
30
|
+
task :default => [:compile, :test, :test_docs]
|
31
|
+
else
|
32
|
+
task :default => [:test, :test_docs]
|
33
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'clandestined'
|
3
|
+
s.version = '1.0.0a'
|
4
|
+
s.date = Time.now.strftime('%Y-%m-%d')
|
5
|
+
s.summary = 'rendezvous hashing implementation based on murmur3 hash'
|
6
|
+
s.author = "Ernest W. Durbin III"
|
7
|
+
s.email = 'ewdurbin@gmail.com'
|
8
|
+
s.homepage = 'https://github.com/ewdurbin/clandestined-ruby'
|
9
|
+
|
10
|
+
s.files = `git ls-files`.split("\n")
|
11
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
12
|
+
s.require_paths = ['lib', 'ext']
|
13
|
+
s.extensions = ['ext/murmur3_native/extconf.rb']
|
14
|
+
|
15
|
+
s.add_development_dependency 'rake', '0.9.6'
|
16
|
+
s.add_development_dependency 'rake-compiler', '0.8.3'
|
17
|
+
s.add_development_dependency 'rubydoctest', '1.1.3'
|
18
|
+
|
19
|
+
s.has_rdoc = false
|
20
|
+
|
21
|
+
s.description = <<DESCRIPTION
|
22
|
+
rendezvous hashing implementation based on murmur3 hash
|
23
|
+
DESCRIPTION
|
24
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
can_compile_extensions = false
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'mkmf'
|
5
|
+
can_compile_extensions = true
|
6
|
+
dir_config("murmur3_native")
|
7
|
+
have_library("c", "main")
|
8
|
+
$defs << "-DRUBY_VERSION_CODE=#{RUBY_VERSION.gsub(/\D/, '')}"
|
9
|
+
rescue Exception
|
10
|
+
$stderr.puts "Could not require 'mkmf'. Not fatal, the extensions are optional."
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
if can_compile_extensions
|
15
|
+
create_makefile("murmur3_native")
|
16
|
+
else
|
17
|
+
open("Makefile", "wb") do |mfile|
|
18
|
+
mfile.puts '.PHONY: install'
|
19
|
+
mfile.puts 'install:'
|
20
|
+
mfile.puts "\t" + '@echo "Extensions not installed, falling back to pure Ruby version."'
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#include "ruby.h"
|
2
|
+
|
3
|
+
#include <stdint.h>
|
4
|
+
#include <string.h>
|
5
|
+
|
6
|
+
// MurmurHash3 was written by Austin Appleby, and is placed in the public
|
7
|
+
// domain. The author hereby disclaims copyright to this source code.
|
8
|
+
|
9
|
+
uint32_t murmur3_32(const char *key, uint32_t len, uint32_t seed) {
|
10
|
+
static const uint32_t c1 = 0xcc9e2d51;
|
11
|
+
static const uint32_t c2 = 0x1b873593;
|
12
|
+
static const uint32_t r1 = 15;
|
13
|
+
static const uint32_t r2 = 13;
|
14
|
+
static const uint32_t m = 5;
|
15
|
+
static const uint32_t n = 0xe6546b64;
|
16
|
+
|
17
|
+
uint32_t hash = seed;
|
18
|
+
|
19
|
+
const int nblocks = len / 4;
|
20
|
+
const uint32_t *blocks = (const uint32_t *) key;
|
21
|
+
int i;
|
22
|
+
for (i = 0; i < nblocks; i++) {
|
23
|
+
uint32_t k = blocks[i];
|
24
|
+
k *= c1;
|
25
|
+
k = (k << r1) | (k >> (32 - r1));
|
26
|
+
k *= c2;
|
27
|
+
|
28
|
+
hash ^= k;
|
29
|
+
hash = ((hash << r2) | (hash >> (32 - r2))) * m + n;
|
30
|
+
}
|
31
|
+
|
32
|
+
const uint8_t *tail = (const uint8_t *) (key + nblocks * 4);
|
33
|
+
uint32_t k1 = 0;
|
34
|
+
|
35
|
+
switch (len & 3) {
|
36
|
+
case 3:
|
37
|
+
k1 ^= tail[2] << 16;
|
38
|
+
case 2:
|
39
|
+
k1 ^= tail[1] << 8;
|
40
|
+
case 1:
|
41
|
+
k1 ^= tail[0];
|
42
|
+
|
43
|
+
k1 *= c1;
|
44
|
+
k1 = (k1 << r1) | (k1 >> (32 - r1));
|
45
|
+
k1 *= c2;
|
46
|
+
hash ^= k1;
|
47
|
+
}
|
48
|
+
|
49
|
+
hash ^= len;
|
50
|
+
hash ^= (hash >> 16);
|
51
|
+
hash *= 0x85ebca6b;
|
52
|
+
hash ^= (hash >> 13);
|
53
|
+
hash *= 0xc2b2ae35;
|
54
|
+
hash ^= (hash >> 16);
|
55
|
+
|
56
|
+
return hash;
|
57
|
+
}
|
58
|
+
|
59
|
+
static VALUE rb_mumur3_32(int argc, VALUE* argv, VALUE self) {
|
60
|
+
VALUE rstr;
|
61
|
+
if (argc == 0 || argc > 2) {
|
62
|
+
rb_raise(rb_eArgError, "accept 1 or 2 arguments: (string[, seed])");
|
63
|
+
}
|
64
|
+
rstr = argv[0];
|
65
|
+
StringValue(rstr);
|
66
|
+
uint32_t value = murmur3_32(RSTRING_PTR(rstr), RSTRING_LEN(rstr), argc == 1 ? 0 : NUM2UINT(argv[1]));
|
67
|
+
return UINT2NUM(value);
|
68
|
+
}
|
69
|
+
|
70
|
+
VALUE Murmur3Native = Qnil;
|
71
|
+
|
72
|
+
void Init_murmur3_native();
|
73
|
+
|
74
|
+
void Init_murmur3_native() {
|
75
|
+
VALUE Murmur3Native = rb_define_module("Murmur3Native");
|
76
|
+
rb_define_module_function(Murmur3Native, "murmur3_32", rb_mumur3_32, -1);
|
77
|
+
}
|
@@ -0,0 +1,111 @@
|
|
1
|
+
|
2
|
+
require 'clandestined/rendezvous_hash'
|
3
|
+
require 'murmur3'
|
4
|
+
|
5
|
+
class Cluster
|
6
|
+
|
7
|
+
include Murmur3
|
8
|
+
|
9
|
+
attr_reader :hash_function
|
10
|
+
attr_reader :murmur_seed
|
11
|
+
attr_reader :replicas
|
12
|
+
attr_reader :nodes
|
13
|
+
attr_reader :zones
|
14
|
+
attr_reader :zone_members
|
15
|
+
attr_reader :rings
|
16
|
+
|
17
|
+
def initialize(cluster_config=nil, replicas=2, murmur_seed=0, hash_function=method(:murmur3_32))
|
18
|
+
@murmur_seed = murmur_seed
|
19
|
+
|
20
|
+
if hash_function == method(:murmur3_32)
|
21
|
+
@hash_function = lambda { |key| hash_function.call(key, murmur_seed) }
|
22
|
+
elsif murmur_seed != 0
|
23
|
+
raise ArgumentError, 'Cannot apply seed to custom hash function #{hash_function}'
|
24
|
+
else
|
25
|
+
@hash_function = hash_function
|
26
|
+
end
|
27
|
+
|
28
|
+
@replicas = replicas
|
29
|
+
@nodes = Hash[]
|
30
|
+
@zones = []
|
31
|
+
@zone_members = Hash[]
|
32
|
+
@rings = Hash[]
|
33
|
+
|
34
|
+
if cluster_config
|
35
|
+
cluster_config.each do |node, node_data|
|
36
|
+
name = node_data['name']
|
37
|
+
zone = node_data['zone']
|
38
|
+
add_zone(zone)
|
39
|
+
add_node(node, zone, name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_zone(zone)
|
45
|
+
@zones.push(zone) unless zones.include?(zone)
|
46
|
+
unless zone_members.has_key?(zone)
|
47
|
+
@zone_members[zone] = []
|
48
|
+
end
|
49
|
+
@zones.sort!
|
50
|
+
end
|
51
|
+
|
52
|
+
def remove_zone(zone)
|
53
|
+
if zones.include?(zone)
|
54
|
+
@zones.delete(zone)
|
55
|
+
for member in zone_members[zone]
|
56
|
+
@nodes.delete(member)
|
57
|
+
end
|
58
|
+
@zones.sort!
|
59
|
+
@rings.delete(zone)
|
60
|
+
@zone_members.delete(zone)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_node(node_id, node_zone=nil, node_name=nil)
|
65
|
+
if nodes.include?(node_id)
|
66
|
+
raise ArgumentError, 'Node with id #{node_id} already exists'
|
67
|
+
end
|
68
|
+
add_zone(node_zone)
|
69
|
+
unless rings.has_key?(node_zone)
|
70
|
+
@rings[node_zone] = RendezvousHash.new(nil, 0, self.hash_function)
|
71
|
+
end
|
72
|
+
@rings[node_zone].add_node(node_id)
|
73
|
+
@nodes[node_id] = node_name
|
74
|
+
unless zone_members.has_key?(node_zone)
|
75
|
+
@zone_members[node_zone] = []
|
76
|
+
end
|
77
|
+
@zone_members[node_zone].push(node_id)
|
78
|
+
end
|
79
|
+
|
80
|
+
def remove_node(node_id, node_zone=nil, node_name=nil)
|
81
|
+
@rings[node_zone].remove_node(node_id)
|
82
|
+
@nodes.delete(node_id)
|
83
|
+
@zone_members[node_zone].delete(node_id)
|
84
|
+
if zone_members[node_zone].length == 0
|
85
|
+
remove_zone(node_zone)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def node_name(node_id)
|
90
|
+
nodes[node_id]
|
91
|
+
end
|
92
|
+
|
93
|
+
def find_nodes(search_key, offset=nil)
|
94
|
+
nodes = []
|
95
|
+
unless offset
|
96
|
+
offset = search_key.split("").map{|char| char[0,1].unpack('c')[0]}.inject(0) {|sum, i| sum + i }
|
97
|
+
end
|
98
|
+
for i in (0...replicas)
|
99
|
+
zone = zones[(i + offset.to_i) % zones.length]
|
100
|
+
nodes << rings[zone].find_node(search_key)
|
101
|
+
end
|
102
|
+
nodes
|
103
|
+
end
|
104
|
+
|
105
|
+
def find_nodes_by_index(product_id, block_index)
|
106
|
+
offset = (product_id.to_i + block_index.to_i) % zones.length
|
107
|
+
search_key = "#{product_id}-#{block_index}"
|
108
|
+
find_nodes(search_key, offset)
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'murmur3'
|
2
|
+
|
3
|
+
class RendezvousHash
|
4
|
+
|
5
|
+
include Murmur3
|
6
|
+
|
7
|
+
attr_reader :nodes
|
8
|
+
attr_reader :murmur_seed
|
9
|
+
attr_reader :hash_function
|
10
|
+
|
11
|
+
def initialize(nodes=nil, murmur_seed=0, hash_function=method(:murmur3_32))
|
12
|
+
@nodes = nodes || []
|
13
|
+
@murmur_seed = murmur_seed
|
14
|
+
|
15
|
+
if hash_function == method(:murmur3_32)
|
16
|
+
@hash_function = lambda { |key| hash_function.call(key, murmur_seed) }
|
17
|
+
elsif murmur_seed != 0
|
18
|
+
raise ArgumentError, "Cannot apply seed to custom hash function #{hash_function}"
|
19
|
+
else
|
20
|
+
@hash_function = hash_function
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_node(node)
|
26
|
+
@nodes.push(node) unless @nodes.include?(node)
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_node(node)
|
30
|
+
@nodes.delete(node) if @nodes.include?(node)
|
31
|
+
end
|
32
|
+
|
33
|
+
def find_node(key)
|
34
|
+
nodes.max {|a,b| hash_function.call("#{a}-#{key}") <=> hash_function.call("#{b}-#{key}")}
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
data/lib/murmur3.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
begin
|
2
|
+
# Extension target, might not exist on some installations
|
3
|
+
require 'murmur3_native'
|
4
|
+
Murmur3 = Murmur3Native
|
5
|
+
rescue LoadError
|
6
|
+
# Pure Ruby fallback, should cover all methods that are otherwise in extension
|
7
|
+
require 'murmur3_ruby'
|
8
|
+
Murmur3 = Murmur3Ruby
|
9
|
+
end
|
data/lib/murmur3_ruby.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
module Murmur3Ruby
|
2
|
+
## MurmurHash3 was written by Austin Appleby, and is placed in the public
|
3
|
+
## domain. The author hereby disclaims copyright to this source code.
|
4
|
+
|
5
|
+
MASK32 = 0xffffffff
|
6
|
+
|
7
|
+
def murmur3_32_rotl(x, r)
|
8
|
+
((x << r) | (x >> (32 - r))) & MASK32
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
def murmur3_32_fmix(h)
|
13
|
+
h &= MASK32
|
14
|
+
h ^= h >> 16
|
15
|
+
h = (h * 0x85ebca6b) & MASK32
|
16
|
+
h ^= h >> 13
|
17
|
+
h = (h * 0xc2b2ae35) & MASK32
|
18
|
+
h ^ (h >> 16)
|
19
|
+
end
|
20
|
+
|
21
|
+
def murmur3_32__mmix(k1)
|
22
|
+
k1 = (k1 * 0xcc9e2d51) & MASK32
|
23
|
+
k1 = murmur3_32_rotl(k1, 15)
|
24
|
+
(k1 * 0x1b873593) & MASK32
|
25
|
+
end
|
26
|
+
|
27
|
+
def murmur3_32(str, seed=0)
|
28
|
+
h1 = seed
|
29
|
+
numbers = str.unpack('V*C*')
|
30
|
+
tailn = str.length % 4
|
31
|
+
tail = numbers.slice!(numbers.size - tailn, tailn)
|
32
|
+
for k1 in numbers
|
33
|
+
h1 ^= murmur3_32__mmix(k1)
|
34
|
+
h1 = murmur3_32_rotl(h1, 13)
|
35
|
+
h1 = (h1*5 + 0xe6546b64) & MASK32
|
36
|
+
end
|
37
|
+
|
38
|
+
unless tail.empty?
|
39
|
+
k1 = 0
|
40
|
+
tail.reverse_each do |c1|
|
41
|
+
k1 = (k1 << 8) | c1
|
42
|
+
end
|
43
|
+
h1 ^= murmur3_32__mmix(k1)
|
44
|
+
end
|
45
|
+
|
46
|
+
h1 ^= str.length
|
47
|
+
murmur3_32_fmix(h1)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,462 @@
|
|
1
|
+
|
2
|
+
require 'test/unit'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
require 'clandestined/cluster'
|
6
|
+
|
7
|
+
def my_hash_function(key)
|
8
|
+
Digest::MD5.hexdigest(key).to_i(16)
|
9
|
+
end
|
10
|
+
|
11
|
+
class ClusterTestCase < Test::Unit::TestCase
|
12
|
+
|
13
|
+
def test_init_no_options
|
14
|
+
cluster = Cluster.new()
|
15
|
+
assert_equal(1361238019, cluster.hash_function.call('6666'))
|
16
|
+
assert_equal(2, cluster.replicas)
|
17
|
+
assert_equal(Hash[], cluster.nodes)
|
18
|
+
assert_equal([], cluster.zones)
|
19
|
+
assert_equal(Hash[], cluster.zone_members)
|
20
|
+
assert_equal(Hash[], cluster.rings)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_murmur_seed
|
24
|
+
cluster = Cluster.new(nil, 2, 10)
|
25
|
+
assert_equal(2981722772, cluster.hash_function.call('6666'))
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_custom_hash_function
|
29
|
+
cluster = Cluster.new(nil, 2, 0, method(:my_hash_function))
|
30
|
+
assert_equal(310130709337150341200260887719094037511, cluster.hash_function.call('6666'))
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_seeded_custom_hash_function
|
34
|
+
assert_raises(ArgumentError) { Cluster.new(nil, 2, 10, method(:my_hash_function)) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_init_single_zone
|
38
|
+
cluster_config = {
|
39
|
+
'1' => Hash[],
|
40
|
+
'2' => Hash[],
|
41
|
+
'3' => Hash[],
|
42
|
+
}
|
43
|
+
cluster = Cluster.new(cluster_config, 1)
|
44
|
+
assert_equal(1, cluster.replicas)
|
45
|
+
assert_equal(3, cluster.nodes.length)
|
46
|
+
assert_equal(1, cluster.zones.length)
|
47
|
+
assert_equal(3, cluster.zone_members[nil].length)
|
48
|
+
assert_equal(1, cluster.rings.length)
|
49
|
+
assert_equal(3, cluster.rings[nil].nodes.length)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_init_zones
|
53
|
+
cluster_config = {
|
54
|
+
'1' => {'zone' => 'a'},
|
55
|
+
'2' => {'zone' => 'b'},
|
56
|
+
'3' => {'zone' => 'a'},
|
57
|
+
'4' => {'zone' => 'b'},
|
58
|
+
'5' => {'zone' => 'a'},
|
59
|
+
'6' => {'zone' => 'b'},
|
60
|
+
}
|
61
|
+
cluster = Cluster.new(cluster_config)
|
62
|
+
assert_equal(2, cluster.replicas)
|
63
|
+
assert_equal(6, cluster.nodes.length)
|
64
|
+
assert_equal(['a', 'b'], cluster.zones)
|
65
|
+
assert_equal(['1', '3', '5'], cluster.zone_members['a'].sort)
|
66
|
+
assert_equal(['2', '4', '6'], cluster.zone_members['b'].sort)
|
67
|
+
assert_equal(2, cluster.rings.length)
|
68
|
+
assert_equal(3, cluster.rings['a'].nodes.length)
|
69
|
+
assert_equal(3, cluster.rings['b'].nodes.length)
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_add_zone
|
73
|
+
cluster = Cluster.new()
|
74
|
+
assert_equal(0, cluster.nodes.length)
|
75
|
+
assert_equal([], cluster.zones)
|
76
|
+
assert_equal(0, cluster.zone_members.length)
|
77
|
+
assert_equal(0, cluster.rings.length)
|
78
|
+
|
79
|
+
cluster.add_zone('b')
|
80
|
+
assert_equal(0, cluster.nodes.length)
|
81
|
+
assert_equal(['b'], cluster.zones)
|
82
|
+
assert_equal(0, cluster.zone_members['b'].length)
|
83
|
+
assert_equal(0, cluster.rings.length)
|
84
|
+
|
85
|
+
cluster.add_zone('b')
|
86
|
+
assert_equal(0, cluster.nodes.length)
|
87
|
+
assert_equal(['b'], cluster.zones)
|
88
|
+
assert_equal(0, cluster.zone_members['b'].length)
|
89
|
+
assert_equal(0, cluster.rings.length)
|
90
|
+
|
91
|
+
cluster.add_zone('a')
|
92
|
+
assert_equal(0, cluster.nodes.length)
|
93
|
+
assert_equal(['a', 'b'], cluster.zones)
|
94
|
+
assert_equal(0, cluster.zone_members['a'].length)
|
95
|
+
assert_equal(0, cluster.zone_members['b'].length)
|
96
|
+
assert_equal(0, cluster.rings.length)
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_add_node
|
100
|
+
cluster = Cluster.new()
|
101
|
+
assert_equal(0, cluster.nodes.length)
|
102
|
+
assert_equal([], cluster.zones)
|
103
|
+
assert_equal(0, cluster.zone_members.length)
|
104
|
+
assert_equal(0, cluster.rings.length)
|
105
|
+
|
106
|
+
cluster.add_node('2', 'b')
|
107
|
+
assert_equal(1, cluster.nodes.length)
|
108
|
+
assert_equal(['b'], cluster.zones)
|
109
|
+
assert_equal(1, cluster.zone_members.length)
|
110
|
+
assert_equal(['2'], cluster.zone_members['b'].sort)
|
111
|
+
assert_equal(1, cluster.rings.length)
|
112
|
+
|
113
|
+
cluster.add_node('1', 'a')
|
114
|
+
assert_equal(2, cluster.nodes.length)
|
115
|
+
assert_equal(['a', 'b'], cluster.zones)
|
116
|
+
assert_equal(2, cluster.zone_members.length)
|
117
|
+
assert_equal(['1'], cluster.zone_members['a'].sort)
|
118
|
+
assert_equal(['2'], cluster.zone_members['b'].sort)
|
119
|
+
assert_equal(2, cluster.rings.length)
|
120
|
+
|
121
|
+
cluster.add_node('21', 'b')
|
122
|
+
assert_equal(3, cluster.nodes.length)
|
123
|
+
assert_equal(['a', 'b'], cluster.zones)
|
124
|
+
assert_equal(2, cluster.zone_members.length)
|
125
|
+
assert_equal(['1'], cluster.zone_members['a'].sort)
|
126
|
+
assert_equal(['2', '21'], cluster.zone_members['b'].sort)
|
127
|
+
assert_equal(2, cluster.rings.length)
|
128
|
+
|
129
|
+
assert_raises(ArgumentError) {cluster.add_node('21')}
|
130
|
+
assert_raises(ArgumentError) {cluster.add_node('21', nil, nil)}
|
131
|
+
assert_raises(ArgumentError) {cluster.add_node('21', 'b', nil)}
|
132
|
+
|
133
|
+
cluster.add_node('22', 'c')
|
134
|
+
assert_equal(4, cluster.nodes.length)
|
135
|
+
assert_equal(['a', 'b', 'c'], cluster.zones)
|
136
|
+
assert_equal(3, cluster.zone_members.length)
|
137
|
+
assert_equal(['1'], cluster.zone_members['a'].sort)
|
138
|
+
assert_equal(['2', '21'], cluster.zone_members['b'].sort)
|
139
|
+
assert_equal(['22'], cluster.zone_members['c'].sort)
|
140
|
+
assert_equal(3, cluster.rings.length)
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_remove_node
|
144
|
+
cluster_config = {
|
145
|
+
'1' => {'zone' => 'a'},
|
146
|
+
'2' => {'zone' => 'b'},
|
147
|
+
'3' => {'zone' => 'a'},
|
148
|
+
'4' => {'zone' => 'b'},
|
149
|
+
'5' => {'zone' => 'c'},
|
150
|
+
'6' => {'zone' => 'c'},
|
151
|
+
}
|
152
|
+
cluster = Cluster.new(cluster_config)
|
153
|
+
|
154
|
+
cluster.remove_node('4', 'b')
|
155
|
+
assert_equal(5, cluster.nodes.length)
|
156
|
+
assert_equal(['a', 'b', 'c'], cluster.zones)
|
157
|
+
assert_equal(3, cluster.zone_members.length)
|
158
|
+
assert_equal(['1', '3'], cluster.zone_members['a'].sort)
|
159
|
+
assert_equal(['2'], cluster.zone_members['b'].sort)
|
160
|
+
assert_equal(['5', '6'], cluster.zone_members['c'].sort)
|
161
|
+
assert_equal(3, cluster.rings.length)
|
162
|
+
|
163
|
+
cluster.remove_node('2', 'b')
|
164
|
+
assert_equal(4, cluster.nodes.length)
|
165
|
+
assert_equal(['a', 'c'], cluster.zones)
|
166
|
+
assert_equal(2, cluster.zone_members.length)
|
167
|
+
assert_equal(['1', '3'], cluster.zone_members['a'].sort)
|
168
|
+
assert_equal(nil, cluster.zone_members['b'])
|
169
|
+
assert_equal(['5', '6'], cluster.zone_members['c'].sort)
|
170
|
+
assert_equal(2, cluster.rings.length)
|
171
|
+
end
|
172
|
+
|
173
|
+
def test_node_name_by_id
|
174
|
+
cluster_config = {
|
175
|
+
'1' => {'name' => 'node1', 'zone' => 'a'},
|
176
|
+
'2' => {'name' => 'node2', 'zone' => 'b'},
|
177
|
+
'3' => {'name' => 'node3', 'zone' => 'a'},
|
178
|
+
'4' => {'name' => 'node4', 'zone' => 'b'},
|
179
|
+
'5' => {'name' => 'node5', 'zone' => 'c'},
|
180
|
+
'6' => {'name' => 'node6', 'zone' => 'c'},
|
181
|
+
}
|
182
|
+
cluster = Cluster.new(cluster_config)
|
183
|
+
|
184
|
+
assert_equal('node1', cluster.node_name('1'))
|
185
|
+
assert_equal('node2', cluster.node_name('2'))
|
186
|
+
assert_equal('node3', cluster.node_name('3'))
|
187
|
+
assert_equal('node4', cluster.node_name('4'))
|
188
|
+
assert_equal('node5', cluster.node_name('5'))
|
189
|
+
assert_equal('node6', cluster.node_name('6'))
|
190
|
+
assert_equal(nil, cluster.node_name('7'))
|
191
|
+
end
|
192
|
+
|
193
|
+
def test_find_nodes
|
194
|
+
cluster_config = {
|
195
|
+
'1' => {'name' => 'node1', 'zone' => 'a'},
|
196
|
+
'2' => {'name' => 'node2', 'zone' => 'a'},
|
197
|
+
'3' => {'name' => 'node3', 'zone' => 'b'},
|
198
|
+
'4' => {'name' => 'node4', 'zone' => 'b'},
|
199
|
+
'5' => {'name' => 'node5', 'zone' => 'c'},
|
200
|
+
'6' => {'name' => 'node6', 'zone' => 'c'},
|
201
|
+
}
|
202
|
+
cluster = Cluster.new(cluster_config)
|
203
|
+
|
204
|
+
assert_equal(['2', '3'], cluster.find_nodes('lol'))
|
205
|
+
assert_equal(['6', '2'], cluster.find_nodes('wat'))
|
206
|
+
assert_equal(['5', '2'], cluster.find_nodes('ok'))
|
207
|
+
assert_equal(['1', '3'], cluster.find_nodes('bar'))
|
208
|
+
assert_equal(['1', '3'], cluster.find_nodes('foo'))
|
209
|
+
assert_equal(['2', '4'], cluster.find_nodes('slap'))
|
210
|
+
end
|
211
|
+
|
212
|
+
def test_find_nodes_by_index
|
213
|
+
cluster_config = {
|
214
|
+
'1' => {'name' => 'node1', 'zone' => 'a'},
|
215
|
+
'2' => {'name' => 'node2', 'zone' => 'a'},
|
216
|
+
'3' => {'name' => 'node3', 'zone' => 'b'},
|
217
|
+
'4' => {'name' => 'node4', 'zone' => 'b'},
|
218
|
+
'5' => {'name' => 'node5', 'zone' => 'c'},
|
219
|
+
'6' => {'name' => 'node6', 'zone' => 'c'},
|
220
|
+
}
|
221
|
+
cluster = Cluster.new(cluster_config)
|
222
|
+
|
223
|
+
assert_equal(['6', '1'], cluster.find_nodes_by_index(1, 1))
|
224
|
+
assert_equal(['2', '4'], cluster.find_nodes_by_index(1, 2))
|
225
|
+
assert_equal(['4', '5'], cluster.find_nodes_by_index(1, 3))
|
226
|
+
assert_equal(['1', '4'], cluster.find_nodes_by_index(2, 1))
|
227
|
+
assert_equal(['3', '5'], cluster.find_nodes_by_index(2, 2))
|
228
|
+
assert_equal(['5', '2'], cluster.find_nodes_by_index(2, 3))
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
|
233
|
+
|
234
|
+
class ClusterIntegrationTestCase < Test::Unit::TestCase
|
235
|
+
|
236
|
+
def test_grow
|
237
|
+
cluster_config = {
|
238
|
+
'1' => {'name' => 'node1', 'zone' => 'a'},
|
239
|
+
'2' => {'name' => 'node2', 'zone' => 'a'},
|
240
|
+
'3' => {'name' => 'node3', 'zone' => 'b'},
|
241
|
+
'4' => {'name' => 'node4', 'zone' => 'b'},
|
242
|
+
'5' => {'name' => 'node5', 'zone' => 'c'},
|
243
|
+
'6' => {'name' => 'node6', 'zone' => 'c'},
|
244
|
+
}
|
245
|
+
cluster = Cluster.new(cluster_config)
|
246
|
+
|
247
|
+
placements = Hash[]
|
248
|
+
for i in cluster.nodes.keys
|
249
|
+
placements[i] = []
|
250
|
+
end
|
251
|
+
for i in (0..1000)
|
252
|
+
nodes = cluster.find_nodes(i.to_s)
|
253
|
+
for node in nodes
|
254
|
+
placements[node].push(i)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
cluster.add_node('7', 'a', 'node7')
|
259
|
+
cluster.add_node('8', 'b', 'node8')
|
260
|
+
cluster.add_node('9', 'c', 'node9')
|
261
|
+
|
262
|
+
new_placements = Hash[]
|
263
|
+
for i in cluster.nodes.keys
|
264
|
+
new_placements[i] = []
|
265
|
+
end
|
266
|
+
for i in (0..1000)
|
267
|
+
nodes = cluster.find_nodes(i.to_s)
|
268
|
+
for node in nodes
|
269
|
+
new_placements[node].push(i)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
keys = placements.values.flatten
|
274
|
+
new_keys = new_placements.values.flatten
|
275
|
+
assert_equal(keys.sort, new_keys.sort)
|
276
|
+
|
277
|
+
added = 0
|
278
|
+
removed = 0
|
279
|
+
new_placements.each do |node, assignments|
|
280
|
+
after = assignments.to_set
|
281
|
+
before = placements.fetch(node, []).to_set
|
282
|
+
removed += before.difference(after).length
|
283
|
+
added += after.difference(before).length
|
284
|
+
end
|
285
|
+
|
286
|
+
assert_equal(added, removed)
|
287
|
+
assert_equal(1384, (added + removed))
|
288
|
+
end
|
289
|
+
|
290
|
+
def test_shrink
|
291
|
+
cluster_config = {
|
292
|
+
'1' => {'name' => 'node1', 'zone' => 'a'},
|
293
|
+
'2' => {'name' => 'node2', 'zone' => 'a'},
|
294
|
+
'3' => {'name' => 'node3', 'zone' => 'b'},
|
295
|
+
'4' => {'name' => 'node4', 'zone' => 'b'},
|
296
|
+
'5' => {'name' => 'node5', 'zone' => 'c'},
|
297
|
+
'6' => {'name' => 'node6', 'zone' => 'c'},
|
298
|
+
'7' => {'name' => 'node7', 'zone' => 'a'},
|
299
|
+
'8' => {'name' => 'node8', 'zone' => 'a'},
|
300
|
+
'9' => {'name' => 'node9', 'zone' => 'b'},
|
301
|
+
'10' => {'name' => 'node10', 'zone' => 'b'},
|
302
|
+
'11' => {'name' => 'node11', 'zone' => 'c'},
|
303
|
+
'12' => {'name' => 'node12', 'zone' => 'c'},
|
304
|
+
}
|
305
|
+
cluster = Cluster.new(cluster_config)
|
306
|
+
|
307
|
+
placements = Hash[]
|
308
|
+
for i in cluster.nodes.keys
|
309
|
+
placements[i] = []
|
310
|
+
end
|
311
|
+
for i in (0...10000)
|
312
|
+
nodes = cluster.find_nodes(i.to_s)
|
313
|
+
for node in nodes
|
314
|
+
placements[node].push(i)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
cluster.remove_node('7', 'a', 'nodee7')
|
319
|
+
cluster.remove_node('9', 'b', 'node9')
|
320
|
+
cluster.remove_node('11', 'c', 'node11')
|
321
|
+
|
322
|
+
new_placements = Hash[]
|
323
|
+
for i in cluster.nodes.keys
|
324
|
+
new_placements[i] = []
|
325
|
+
end
|
326
|
+
for i in (0...10000)
|
327
|
+
nodes = cluster.find_nodes(i.to_s)
|
328
|
+
for node in nodes
|
329
|
+
new_placements[node].push(i)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
keys = placements.values.flatten
|
334
|
+
new_keys = new_placements.values.flatten
|
335
|
+
assert_equal(keys.sort, new_keys.sort)
|
336
|
+
|
337
|
+
added = 0
|
338
|
+
removed = 0
|
339
|
+
placements.each do |node, assignments|
|
340
|
+
after = assignments.to_set
|
341
|
+
before = new_placements.fetch(node, []).to_set
|
342
|
+
removed += before.difference(after).length
|
343
|
+
added += after.difference(before).length
|
344
|
+
end
|
345
|
+
|
346
|
+
assert_equal(added, removed)
|
347
|
+
assert_equal(9804, (added + removed))
|
348
|
+
end
|
349
|
+
|
350
|
+
def test_add_zone
|
351
|
+
cluster_config = {
|
352
|
+
'1' => {'name' => 'node1', 'zone' => 'a'},
|
353
|
+
'2' => {'name' => 'node2', 'zone' => 'a'},
|
354
|
+
'3' => {'name' => 'node3', 'zone' => 'b'},
|
355
|
+
'4' => {'name' => 'node4', 'zone' => 'b'},
|
356
|
+
}
|
357
|
+
cluster = Cluster.new(cluster_config)
|
358
|
+
|
359
|
+
placements = Hash[]
|
360
|
+
for i in cluster.nodes.keys
|
361
|
+
placements[i] = []
|
362
|
+
end
|
363
|
+
for i in (0...1000)
|
364
|
+
nodes = cluster.find_nodes(i.to_s)
|
365
|
+
for node in nodes
|
366
|
+
placements[node].push(i)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
cluster.add_node('5', 'c', 'node5')
|
371
|
+
cluster.add_node('6', 'c', 'node6')
|
372
|
+
|
373
|
+
new_placements = Hash[]
|
374
|
+
for i in cluster.nodes.keys
|
375
|
+
new_placements[i] = []
|
376
|
+
end
|
377
|
+
for i in (0...1000)
|
378
|
+
nodes = cluster.find_nodes(i.to_s)
|
379
|
+
for node in nodes
|
380
|
+
new_placements[node].push(i)
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
keys = placements.values.flatten
|
385
|
+
new_keys = new_placements.values.flatten
|
386
|
+
assert_equal(keys.sort, new_keys.sort)
|
387
|
+
|
388
|
+
added = 0
|
389
|
+
removed = 0
|
390
|
+
new_placements.each do |node, assignments|
|
391
|
+
after = assignments.to_set
|
392
|
+
before = placements.fetch(node, []).to_set
|
393
|
+
removed += before.difference(after).length
|
394
|
+
added += after.difference(before).length
|
395
|
+
end
|
396
|
+
|
397
|
+
assert_equal(added, removed)
|
398
|
+
assert_equal(1332, (added + removed))
|
399
|
+
end
|
400
|
+
|
401
|
+
def test_remove_zone
|
402
|
+
cluster_config = {
|
403
|
+
'1' => {'name' => 'node1', 'zone' => 'a'},
|
404
|
+
'2' => {'name' => 'node2', 'zone' => 'a'},
|
405
|
+
'3' => {'name' => 'node3', 'zone' => 'b'},
|
406
|
+
'4' => {'name' => 'node4', 'zone' => 'b'},
|
407
|
+
'5' => {'name' => 'node5', 'zone' => 'c'},
|
408
|
+
'6' => {'name' => 'node6', 'zone' => 'c'},
|
409
|
+
'7' => {'name' => 'node7', 'zone' => 'a'},
|
410
|
+
'8' => {'name' => 'node8', 'zone' => 'a'},
|
411
|
+
'9' => {'name' => 'node9', 'zone' => 'b'},
|
412
|
+
'10' => {'name' => 'node10', 'zone' => 'b'},
|
413
|
+
'11' => {'name' => 'node11', 'zone' => 'c'},
|
414
|
+
'12' => {'name' => 'node12', 'zone' => 'c'},
|
415
|
+
}
|
416
|
+
cluster = Cluster.new(cluster_config)
|
417
|
+
|
418
|
+
placements = Hash[]
|
419
|
+
for i in cluster.nodes.keys
|
420
|
+
placements[i] = []
|
421
|
+
end
|
422
|
+
for i in (0...10000)
|
423
|
+
nodes = cluster.find_nodes(i.to_s)
|
424
|
+
for node in nodes
|
425
|
+
placements[node].push(i)
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
cluster.remove_node('5', 'c', 'node5')
|
430
|
+
cluster.remove_node('6', 'c', 'node6')
|
431
|
+
cluster.remove_node('11', 'c', 'node11')
|
432
|
+
cluster.remove_node('12', 'c', 'node12')
|
433
|
+
|
434
|
+
new_placements = Hash[]
|
435
|
+
for i in cluster.nodes.keys
|
436
|
+
new_placements[i] = []
|
437
|
+
end
|
438
|
+
for i in (0...10000)
|
439
|
+
nodes = cluster.find_nodes(i.to_s)
|
440
|
+
for node in nodes
|
441
|
+
new_placements[node].push(i)
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
keys = placements.values.flatten
|
446
|
+
new_keys = new_placements.values.flatten
|
447
|
+
assert_equal(keys.sort, new_keys.sort)
|
448
|
+
|
449
|
+
added = 0
|
450
|
+
removed = 0
|
451
|
+
placements.each do |node, assignments|
|
452
|
+
after = assignments.to_set
|
453
|
+
before = new_placements.fetch(node, []).to_set
|
454
|
+
removed += before.difference(after).length
|
455
|
+
added += after.difference(before).length
|
456
|
+
end
|
457
|
+
|
458
|
+
assert_equal(added, removed)
|
459
|
+
assert_equal(13332, (added + removed))
|
460
|
+
end
|
461
|
+
|
462
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
|
2
|
+
require 'test/unit'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
require 'clandestined/rendezvous_hash'
|
6
|
+
|
7
|
+
def my_hash_function(key)
|
8
|
+
Digest::MD5.hexdigest(key).to_i(16)
|
9
|
+
end
|
10
|
+
|
11
|
+
class RendezvousHashTestCase < Test::Unit::TestCase
|
12
|
+
|
13
|
+
def test_init_no_options
|
14
|
+
rendezvous = RendezvousHash.new()
|
15
|
+
assert_equal(0, rendezvous.nodes.length)
|
16
|
+
assert_equal(1361238019, rendezvous.hash_function.call('6666'))
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_init
|
20
|
+
nodes = ['0', '1', '2']
|
21
|
+
rendezvous = RendezvousHash.new(nodes)
|
22
|
+
assert_equal(3, rendezvous.nodes.length)
|
23
|
+
assert_equal(1361238019, rendezvous.hash_function.call('6666'))
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_murmur_seed
|
27
|
+
rendezvous = RendezvousHash.new(nil, 10)
|
28
|
+
assert_equal(2981722772, rendezvous.hash_function.call('6666'))
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_custom_hash_function
|
32
|
+
rendezvous = RendezvousHash.new(nil, 0, method(:my_hash_function))
|
33
|
+
assert_equal(310130709337150341200260887719094037511, rendezvous.hash_function.call('6666'))
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_seeded_custom_hash_function
|
37
|
+
assert_raises(ArgumentError) { RendezvousHash.new(nil, 10, method(:my_hash_function)) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_add_node
|
41
|
+
rendezvous = RendezvousHash.new()
|
42
|
+
rendezvous.add_node('1')
|
43
|
+
assert_equal(1, rendezvous.nodes.length)
|
44
|
+
rendezvous.add_node('1')
|
45
|
+
assert_equal(1, rendezvous.nodes.length)
|
46
|
+
rendezvous.add_node('2')
|
47
|
+
assert_equal(2, rendezvous.nodes.length)
|
48
|
+
rendezvous.add_node('1')
|
49
|
+
assert_equal(2, rendezvous.nodes.length)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_remove_node
|
53
|
+
nodes = ['0', '1', '2']
|
54
|
+
rendezvous = RendezvousHash.new(nodes)
|
55
|
+
rendezvous.remove_node('2')
|
56
|
+
assert_equal(2, rendezvous.nodes.length)
|
57
|
+
rendezvous.remove_node('2')
|
58
|
+
assert_equal(2, rendezvous.nodes.length)
|
59
|
+
rendezvous.remove_node('1')
|
60
|
+
assert_equal(1, rendezvous.nodes.length)
|
61
|
+
rendezvous.remove_node('0')
|
62
|
+
assert_equal(0, rendezvous.nodes.length)
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_find_node
|
66
|
+
nodes = ['0', '1', '2']
|
67
|
+
rendezvous = RendezvousHash.new(nodes)
|
68
|
+
assert_equal('0', rendezvous.find_node('ok'))
|
69
|
+
assert_equal('1', rendezvous.find_node('mykey'))
|
70
|
+
assert_equal('2', rendezvous.find_node('wat'))
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_find_node_after_removal
|
74
|
+
nodes = ['0', '1', '2']
|
75
|
+
rendezvous = RendezvousHash.new(nodes)
|
76
|
+
rendezvous.remove_node('1')
|
77
|
+
assert_equal('0', rendezvous.find_node('ok'))
|
78
|
+
assert_equal('0', rendezvous.find_node('mykey'))
|
79
|
+
assert_equal('2', rendezvous.find_node('wat'))
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_find_node_after_addition
|
83
|
+
nodes = ['0', '1', '2']
|
84
|
+
rendezvous = RendezvousHash.new(nodes)
|
85
|
+
assert_equal('0', rendezvous.find_node('ok'))
|
86
|
+
assert_equal('1', rendezvous.find_node('mykey'))
|
87
|
+
assert_equal('2', rendezvous.find_node('wat'))
|
88
|
+
assert_equal('2', rendezvous.find_node('lol'))
|
89
|
+
rendezvous.add_node('3')
|
90
|
+
assert_equal('0', rendezvous.find_node('ok'))
|
91
|
+
assert_equal('1', rendezvous.find_node('mykey'))
|
92
|
+
assert_equal('2', rendezvous.find_node('wat'))
|
93
|
+
assert_equal('3', rendezvous.find_node('lol'))
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
class RendezvousHashIntegrationTestCase < Test::Unit::TestCase
|
99
|
+
|
100
|
+
def test_grow
|
101
|
+
rendezvous = RendezvousHash.new()
|
102
|
+
|
103
|
+
placements = Hash[]
|
104
|
+
for i in (0...10)
|
105
|
+
rendezvous.add_node(i.to_s)
|
106
|
+
placements[i.to_s] = []
|
107
|
+
end
|
108
|
+
for i in (0...1000)
|
109
|
+
node = rendezvous.find_node(i.to_s)
|
110
|
+
placements[node].push(i)
|
111
|
+
end
|
112
|
+
|
113
|
+
new_placements = Hash[]
|
114
|
+
for i in (0...20)
|
115
|
+
rendezvous.add_node(i.to_s)
|
116
|
+
new_placements[i.to_s] = []
|
117
|
+
end
|
118
|
+
for i in (0...1000)
|
119
|
+
node = rendezvous.find_node(i.to_s)
|
120
|
+
new_placements[node].push(i)
|
121
|
+
end
|
122
|
+
|
123
|
+
keys = placements.values.flatten
|
124
|
+
new_keys = new_placements.values.flatten
|
125
|
+
assert_equal(keys.sort, new_keys.sort)
|
126
|
+
|
127
|
+
added = 0
|
128
|
+
removed = 0
|
129
|
+
new_placements.each do |node, assignments|
|
130
|
+
after = assignments.to_set
|
131
|
+
before = placements.fetch(node, []).to_set
|
132
|
+
removed += before.difference(after).length
|
133
|
+
added += after.difference(before).length
|
134
|
+
end
|
135
|
+
|
136
|
+
assert_equal(added, removed)
|
137
|
+
assert_equal(1062, (added + removed))
|
138
|
+
end
|
139
|
+
|
140
|
+
def test_shrink
|
141
|
+
rendezvous = RendezvousHash.new()
|
142
|
+
|
143
|
+
placements = {}
|
144
|
+
for i in (0...10)
|
145
|
+
rendezvous.add_node(i.to_s)
|
146
|
+
placements[i.to_s] = []
|
147
|
+
end
|
148
|
+
for i in (0...1000)
|
149
|
+
node = rendezvous.find_node(i.to_s)
|
150
|
+
placements[node].push(i)
|
151
|
+
end
|
152
|
+
|
153
|
+
rendezvous.remove_node('9')
|
154
|
+
new_placements = {}
|
155
|
+
for i in (0...9)
|
156
|
+
new_placements[i.to_s] = []
|
157
|
+
end
|
158
|
+
for i in (0...1000)
|
159
|
+
node = rendezvous.find_node(i.to_s)
|
160
|
+
new_placements[node].push(i)
|
161
|
+
end
|
162
|
+
|
163
|
+
keys = placements.values.flatten
|
164
|
+
new_keys = new_placements.values.flatten
|
165
|
+
assert_equal(keys.sort, new_keys.sort)
|
166
|
+
|
167
|
+
added = 0
|
168
|
+
removed = 0
|
169
|
+
placements.each do |node, assignments|
|
170
|
+
after = assignments.to_set
|
171
|
+
before = new_placements.fetch(node, []).to_set
|
172
|
+
removed += before.difference(after).length
|
173
|
+
added += after.difference(before).length
|
174
|
+
end
|
175
|
+
|
176
|
+
assert_equal(added, removed)
|
177
|
+
assert_equal(202, (added + removed))
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: clandestined
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0a
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ernest W. Durbin III
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-07-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.9.6
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.9.6
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake-compiler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.8.3
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.8.3
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubydoctest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.1.3
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.1.3
|
55
|
+
description: |
|
56
|
+
rendezvous hashing implementation based on murmur3 hash
|
57
|
+
email: ewdurbin@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions:
|
60
|
+
- ext/murmur3_native/extconf.rb
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- .gitignore
|
64
|
+
- .travis.yml
|
65
|
+
- Gemfile
|
66
|
+
- Gemfile.lock
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- clandestined.gemspec
|
70
|
+
- ext/murmur3_native/extconf.rb
|
71
|
+
- ext/murmur3_native/murmur3_native.c
|
72
|
+
- lib/clandestined/cluster.rb
|
73
|
+
- lib/clandestined/rendezvous_hash.rb
|
74
|
+
- lib/murmur3.rb
|
75
|
+
- lib/murmur3_ruby.rb
|
76
|
+
- test/test_cluster.rb
|
77
|
+
- test/test_rendezvous_hash.rb
|
78
|
+
homepage: https://github.com/ewdurbin/clandestined-ruby
|
79
|
+
licenses: []
|
80
|
+
metadata: {}
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
- ext
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - '>'
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 1.3.1
|
96
|
+
requirements: []
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 2.0.14
|
99
|
+
signing_key:
|
100
|
+
specification_version: 4
|
101
|
+
summary: rendezvous hashing implementation based on murmur3 hash
|
102
|
+
test_files:
|
103
|
+
- test/test_cluster.rb
|
104
|
+
- test/test_rendezvous_hash.rb
|