clandestined 1.0.0a → 1.0.0b

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 47d9cefad2b6c0c74a35295e37f64aa81f8bca19
4
- data.tar.gz: ce0099265bc7b8b3929228550a657edc1057e027
3
+ metadata.gz: 03d6d29e1c5fe16b7457ae7ae9d851a55ad2c03e
4
+ data.tar.gz: 9ce134bb758d8f00442ac45a1b592ccbe143f04c
5
5
  SHA512:
6
- metadata.gz: cd241d8e5896d6608f81477ea23ec575bc9f55e71ca3917705a4217cf7d84a22e5c916271413d6d42a4bd2e81a73af34c4b1bd9ae57ae72a10346dbb84804b26
7
- data.tar.gz: f343ef64e349cc3805b1f8e994fc9bc95865e150fad80e15f75840511f90ff7e3250bf66d3e01da69594e413f236097f78b63a57f31242672a43c2c5c8c8cf9d
6
+ metadata.gz: ccdcbbbd4d4d9524cbb61a21efa5d3389da3ad6b7b366c1d1d6273a7da265686a600ece76eb1a8d68e6955761fcb3ec82323747a8ee9c6c2ef76d2fb4d4e09f3
7
+ data.tar.gz: 04e1699336e8e5b08e0d75207d80308955e86986dafd79c1cd2d8b7ae3ec1b94de470e26cc6e88aea31c9bdf06606df2a06769737faa00ee6cb70383d8cc400d
data/CHANGES.md ADDED
@@ -0,0 +1,16 @@
1
+
2
+ v1.0.0b (2014-07-07)
3
+ ====================
4
+
5
+ - `Cluster.remove_zone` now raises `ArgumentError` on attempt to remove a
6
+ non-existent zone.
7
+ - `Cluster.remove_node` and `RendezvousHash.remove_node` now raise
8
+ `ArgumentError` on attempt to remove a non-existent node.
9
+ - Support for custom hash functions retracted for 1.0.0 milestone.
10
+ - `murmur_seed` keyword argument renamed to `seed` for `Cluster` and
11
+ `RendezvousHash` `initialize` methods.
12
+
13
+ v1.0.0a (2014-07-06)
14
+ ====================
15
+
16
+ - Initial Release.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Ernest W. Durbin III
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -36,7 +36,7 @@ Currently targetting for support:
36
36
  '9' => Hash['name' => 'node9.example.com', 'zone' => 'us-east-1c'],
37
37
  ]
38
38
  >>
39
- >> cluster = Cluster.new(nodes)
39
+ >> cluster = Clandestined::Cluster.new(nodes)
40
40
  >> cluster.find_nodes('mykey')
41
41
  => ["4", "8"]
42
42
  ```
@@ -64,8 +64,8 @@ set to 1
64
64
  '9' => Hash['name' => 'node9.example.com'],
65
65
  ]
66
66
  >>
67
- >> cluster = Cluster.new(nodes, 1)
68
- >> rendezvous = RendezvousHash.new(nodes.keys)
67
+ >> cluster = Clandestined::Cluster.new(nodes, 1)
68
+ >> rendezvous = Clandestined::RendezvousHash.new(nodes.keys)
69
69
  >>
70
70
  >> cluster.find_nodes('mykey')
71
71
  => ["4"]
@@ -77,47 +77,19 @@ set to 1
77
77
 
78
78
  ### murmur3 seeding
79
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.
80
+ if you plan to use keys based on untrusted input (supported, but go ahead),
81
+ it would be best to use a custom seed for hashing. although this technique is
82
+ by no means a way to fully mitigate a DoS attack using crafted keys, it may
83
+ help you sleep better at night.
84
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
85
+ **DISCLAIMER**
111
86
 
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.
87
+ clandestined was not designed with consideration for untrusted input, please
88
+ see LICENSE.
114
89
 
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.
90
+ **END DISCLAIMER**
118
91
 
119
92
  ```ruby
120
- >> require 'digest'
121
93
  >> require 'clandestined/cluster'
122
94
  >> require 'clandestined/rendezvous_hash'
123
95
  >>
@@ -133,15 +105,11 @@ an integer.
133
105
  '9' => Hash['name' => 'node9.example.com'],
134
106
  ]
135
107
  >>
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))
108
+ >> cluster = Clandestined::Cluster.new(nodes, 1, 1337)
109
+ >> rendezvous = Clandestined::RendezvousHash.new(nodes.keys, 1337)
142
110
  >>
143
111
  >> cluster.find_nodes('mykey')
144
- => ["1"]
112
+ => ["7"]
145
113
  >> rendezvous.find_node('mykey')
146
- => "1"
114
+ => "7"
147
115
  ```
data/clandestined.gemspec CHANGED
@@ -1,6 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'clandestined'
3
- s.version = '1.0.0a'
3
+ s.version = '1.0.0b'
4
+ s.licenses = ['MIT']
4
5
  s.date = Time.now.strftime('%Y-%m-%d')
5
6
  s.summary = 'rendezvous hashing implementation based on murmur3 hash'
6
7
  s.author = "Ernest W. Durbin III"
@@ -2,110 +2,101 @@
2
2
  require 'clandestined/rendezvous_hash'
3
3
  require 'murmur3'
4
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)
5
+ module Clandestined
6
+ class Cluster
7
+
8
+ include Murmur3
9
+
10
+ attr_reader :hash_function
11
+ attr_reader :seed
12
+ attr_reader :replicas
13
+ attr_reader :nodes
14
+ attr_reader :zones
15
+ attr_reader :zone_members
16
+ attr_reader :rings
17
+
18
+ def initialize(cluster_config=nil, replicas=2, seed=0)
19
+ @seed = seed
20
+
21
+ @replicas = replicas
22
+ @nodes = Hash[]
23
+ @zones = []
24
+ @zone_members = Hash[]
25
+ @rings = Hash[]
26
+
27
+ if cluster_config
28
+ cluster_config.each do |node, node_data|
29
+ name = node_data['name']
30
+ zone = node_data['zone']
31
+ add_zone(zone)
32
+ add_node(node, zone, name)
33
+ end
40
34
  end
41
35
  end
42
- end
43
36
 
44
- def add_zone(zone)
45
- @zones.push(zone) unless zones.include?(zone)
46
- unless zone_members.has_key?(zone)
47
- @zone_members[zone] = []
37
+ def add_zone(zone)
38
+ @zones.push(zone) unless zones.include?(zone)
39
+ @zone_members[zone] ||= []
40
+ @zones.sort!
48
41
  end
49
- @zones.sort!
50
- end
51
42
 
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)
43
+ def remove_zone(zone)
44
+ if zones.include?(zone)
45
+ @zones.delete(zone)
46
+ for member in zone_members[zone]
47
+ @nodes.delete(member)
48
+ end
49
+ @zones.sort!
50
+ @rings.delete(zone)
51
+ @zone_members.delete(zone)
52
+ else
53
+ raise ArgumentError, "No such zone #{zone} to remove"
57
54
  end
58
- @zones.sort!
59
- @rings.delete(zone)
60
- @zone_members.delete(zone)
61
55
  end
62
- end
63
56
 
64
- def add_node(node_id, node_zone=nil, node_name=nil)
65
- if nodes.include?(node_id)
57
+ def add_node(node_id, node_zone=nil, node_name=nil)
58
+ if nodes.include?(node_id)
66
59
  raise ArgumentError, 'Node with id #{node_id} already exists'
60
+ end
61
+ add_zone(node_zone)
62
+ unless rings.has_key?(node_zone)
63
+ @rings[node_zone] = RendezvousHash.new(nil, seed)
64
+ end
65
+ @rings[node_zone].add_node(node_id)
66
+ @nodes[node_id] = node_name
67
+ (@zone_members[node_zone] ||= []) << node_id
67
68
  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
69
 
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
70
+ def remove_node(node_id, node_zone=nil, node_name=nil)
71
+ @rings[node_zone].remove_node(node_id)
72
+ @nodes.delete(node_id)
73
+ @zone_members[node_zone].delete(node_id)
74
+ if zone_members[node_zone].length == 0
85
75
  remove_zone(node_zone)
76
+ end
86
77
  end
87
- end
88
78
 
89
- def node_name(node_id)
90
- nodes[node_id]
91
- end
79
+ def node_name(node_id)
80
+ nodes[node_id]
81
+ end
92
82
 
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 }
83
+ def find_nodes(search_key, offset=nil)
84
+ nodes = []
85
+ unless offset
86
+ offset = search_key.split('').inject(0) { |s, c| s += c[0,1].unpack('c')[0] }
87
+ end
88
+ for i in (0...replicas)
89
+ zone = zones[(i + offset.to_i) % zones.length]
90
+ nodes << rings[zone].find_node(search_key)
91
+ end
92
+ nodes
97
93
  end
98
- for i in (0...replicas)
99
- zone = zones[(i + offset.to_i) % zones.length]
100
- nodes << rings[zone].find_node(search_key)
94
+
95
+ def find_nodes_by_index(product_id, block_index)
96
+ offset = (product_id.to_i + block_index.to_i) % zones.length
97
+ search_key = "#{product_id}-#{block_index}"
98
+ find_nodes(search_key, offset)
101
99
  end
102
- nodes
103
- end
104
100
 
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
101
  end
110
-
111
102
  end
@@ -1,37 +1,37 @@
1
1
  require 'murmur3'
2
2
 
3
- class RendezvousHash
3
+ module Clandestined
4
+ class RendezvousHash
4
5
 
5
- include Murmur3
6
+ include Murmur3
6
7
 
7
- attr_reader :nodes
8
- attr_reader :murmur_seed
9
- attr_reader :hash_function
8
+ attr_reader :nodes
9
+ attr_reader :seed
10
+ attr_reader :hash_function
10
11
 
11
- def initialize(nodes=nil, murmur_seed=0, hash_function=method(:murmur3_32))
12
- @nodes = nodes || []
13
- @murmur_seed = murmur_seed
12
+ def initialize(nodes=nil, seed=0)
13
+ @nodes = nodes || []
14
+ @seed = seed
15
+
16
+ @hash_function = lambda { |key| murmur3_32(key, seed) }
14
17
 
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
18
  end
22
19
 
23
- end
20
+ def add_node(node)
21
+ @nodes.push(node) unless @nodes.include?(node)
22
+ end
24
23
 
25
- def add_node(node)
26
- @nodes.push(node) unless @nodes.include?(node)
27
- end
24
+ def remove_node(node)
25
+ if @nodes.include?(node)
26
+ @nodes.delete(node)
27
+ else
28
+ raise ArgumentError, "No such node #{node} to remove"
29
+ end
30
+ end
28
31
 
29
- def remove_node(node)
30
- @nodes.delete(node) if @nodes.include?(node)
31
- end
32
+ def find_node(key)
33
+ nodes.max {|a,b| hash_function.call("#{a}-#{key}") <=> hash_function.call("#{b}-#{key}")}
34
+ end
32
35
 
33
- def find_node(key)
34
- nodes.max {|a,b| hash_function.call("#{a}-#{key}") <=> hash_function.call("#{b}-#{key}")}
35
36
  end
36
-
37
37
  end
data/test/test_cluster.rb CHANGED
@@ -4,15 +4,14 @@ require 'set'
4
4
 
5
5
  require 'clandestined/cluster'
6
6
 
7
- def my_hash_function(key)
8
- Digest::MD5.hexdigest(key).to_i(16)
9
- end
7
+ include Clandestined
8
+
10
9
 
11
10
  class ClusterTestCase < Test::Unit::TestCase
12
11
 
13
12
  def test_init_no_options
14
13
  cluster = Cluster.new()
15
- assert_equal(1361238019, cluster.hash_function.call('6666'))
14
+ assert_equal(0, cluster.seed)
16
15
  assert_equal(2, cluster.replicas)
17
16
  assert_equal(Hash[], cluster.nodes)
18
17
  assert_equal([], cluster.zones)
@@ -22,16 +21,7 @@ class ClusterTestCase < Test::Unit::TestCase
22
21
 
23
22
  def test_murmur_seed
24
23
  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)) }
24
+ assert_equal(10, cluster.seed)
35
25
  end
36
26
 
37
27
  def test_init_single_zone
@@ -430,6 +420,7 @@ class ClusterIntegrationTestCase < Test::Unit::TestCase
430
420
  cluster.remove_node('6', 'c', 'node6')
431
421
  cluster.remove_node('11', 'c', 'node11')
432
422
  cluster.remove_node('12', 'c', 'node12')
423
+ assert_raises(ArgumentError) {cluster.remove_zone('c')}
433
424
 
434
425
  new_placements = Hash[]
435
426
  for i in cluster.nodes.keys
@@ -4,9 +4,8 @@ require 'set'
4
4
 
5
5
  require 'clandestined/rendezvous_hash'
6
6
 
7
- def my_hash_function(key)
8
- Digest::MD5.hexdigest(key).to_i(16)
9
- end
7
+ include Clandestined
8
+
10
9
 
11
10
  class RendezvousHashTestCase < Test::Unit::TestCase
12
11
 
@@ -23,20 +22,12 @@ class RendezvousHashTestCase < Test::Unit::TestCase
23
22
  assert_equal(1361238019, rendezvous.hash_function.call('6666'))
24
23
  end
25
24
 
26
- def test_murmur_seed
25
+ def test_seed
27
26
  rendezvous = RendezvousHash.new(nil, 10)
27
+ assert_equal(10, rendezvous.seed)
28
28
  assert_equal(2981722772, rendezvous.hash_function.call('6666'))
29
29
  end
30
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
31
  def test_add_node
41
32
  rendezvous = RendezvousHash.new()
42
33
  rendezvous.add_node('1')
@@ -54,7 +45,7 @@ class RendezvousHashTestCase < Test::Unit::TestCase
54
45
  rendezvous = RendezvousHash.new(nodes)
55
46
  rendezvous.remove_node('2')
56
47
  assert_equal(2, rendezvous.nodes.length)
57
- rendezvous.remove_node('2')
48
+ assert_raises(ArgumentError) { rendezvous.remove_node(2, rendezvous.nodes.length) }
58
49
  assert_equal(2, rendezvous.nodes.length)
59
50
  rendezvous.remove_node('1')
60
51
  assert_equal(1, rendezvous.nodes.length)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clandestined
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0a
4
+ version: 1.0.0b
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ernest W. Durbin III
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-06 00:00:00.000000000 Z
11
+ date: 2014-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -62,8 +62,10 @@ extra_rdoc_files: []
62
62
  files:
63
63
  - .gitignore
64
64
  - .travis.yml
65
+ - CHANGES.md
65
66
  - Gemfile
66
67
  - Gemfile.lock
68
+ - LICENSE
67
69
  - README.md
68
70
  - Rakefile
69
71
  - clandestined.gemspec
@@ -76,7 +78,8 @@ files:
76
78
  - test/test_cluster.rb
77
79
  - test/test_rendezvous_hash.rb
78
80
  homepage: https://github.com/ewdurbin/clandestined-ruby
79
- licenses: []
81
+ licenses:
82
+ - MIT
80
83
  metadata: {}
81
84
  post_install_message:
82
85
  rdoc_options: []