clandestined 1.0.0a
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/ewdurbin/clandestined-ruby.svg?branch=master)](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
|