clandestined 1.0.0a

Sign up to get free protection for your applications and to get access to all the features.
@@ -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