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.
@@ -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
@@ -0,0 +1,9 @@
1
+ /.bundle
2
+ /bin
3
+ /vendor
4
+ /.vendor
5
+
6
+ /pkg
7
+ /tmp
8
+ /lib/murmur3_native.bundle
9
+ /lib/murmur3_native.so
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.0
7
+ - jruby-18mode
8
+ - jruby-19mode
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kissmetrics_core_api-admin-client.gemspec
4
+ gemspec
@@ -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)
@@ -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
+ ```
@@ -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
@@ -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
@@ -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