em-voldemort 0.1.5

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,13 @@
1
+ module EM::Voldemort
2
+ # Like HTTP 400 series responses, this exception is used for errors that are the client's fault.
3
+ # The request generally should not be retried.
4
+ class ClientError < RuntimeError; end
5
+
6
+ # Exception to indicate that the requested key does not exist in the Voldemort store.
7
+ class KeyNotFound < ClientError; end
8
+
9
+ # Like HTTP 500 series responses, this exception is used for errors on the server side or in the
10
+ # network. They are generally transient, so it makes sense to retry failed requests a limited
11
+ # number of times.
12
+ class ServerError < RuntimeError; end
13
+ end
@@ -0,0 +1,105 @@
1
+ module EM::Voldemort
2
+ # https://github.com/voldemort/voldemort/blob/master/src/proto/voldemort-client.proto
3
+ class Protobuf
4
+ class ClockEntry
5
+ include Beefcake::Message
6
+ required :node_id, :int32, 1
7
+ required :version, :int64, 2
8
+ end
9
+
10
+ class VectorClock
11
+ include Beefcake::Message
12
+ repeated :entries, ClockEntry, 1
13
+ optional :timestamp, :int64, 2
14
+ end
15
+
16
+ class Versioned
17
+ include Beefcake::Message
18
+ required :value, :bytes, 1
19
+ required :version, VectorClock, 2
20
+ end
21
+
22
+ class Error
23
+ include Beefcake::Message
24
+ required :error_code, :int32, 1
25
+ required :error_message, :string, 2
26
+ end
27
+
28
+ class KeyedVersions
29
+ include Beefcake::Message
30
+ required :key, :bytes, 1
31
+ repeated :versions, Versioned, 2
32
+ end
33
+
34
+ class GetRequest
35
+ include Beefcake::Message
36
+ optional :key, :bytes, 1
37
+ end
38
+
39
+ class GetResponse
40
+ include Beefcake::Message
41
+ repeated :versioned, Versioned, 1
42
+ optional :error, Error, 2
43
+ end
44
+
45
+ class GetVersionResponse
46
+ include Beefcake::Message
47
+ repeated :versions, VectorClock, 1
48
+ optional :error, Error, 2
49
+ end
50
+
51
+ class GetAllRequest
52
+ include Beefcake::Message
53
+ repeated :keys, :bytes, 1
54
+ end
55
+
56
+ class GetAllResponse
57
+ include Beefcake::Message
58
+ repeated :values, KeyedVersions, 1
59
+ optional :error, Error, 2
60
+ end
61
+
62
+ class PutRequest
63
+ include Beefcake::Message
64
+ required :key, :bytes, 1
65
+ required :versioned, Versioned, 2
66
+ end
67
+
68
+ class PutResponse
69
+ include Beefcake::Message
70
+ optional :error, Error, 1
71
+ end
72
+
73
+ class DeleteRequest
74
+ include Beefcake::Message
75
+ required :key, :bytes, 1
76
+ required :version, VectorClock, 2
77
+ end
78
+
79
+ class DeleteResponse
80
+ include Beefcake::Message
81
+ required :success, :bool, 1
82
+ optional :error, Error, 2
83
+ end
84
+
85
+ module RequestType
86
+ GET = 0
87
+ GET_ALL = 1
88
+ PUT = 2
89
+ DELETE = 3
90
+ GET_VERSION = 4
91
+ end
92
+
93
+ class Request
94
+ include Beefcake::Message
95
+ required :type, RequestType, 1
96
+ required :should_route, :bool, 2, :default => false
97
+ required :store, :string, 3
98
+ optional :get, GetRequest, 4
99
+ optional :getAll, GetAllRequest, 5
100
+ optional :put, PutRequest, 6
101
+ optional :delete, DeleteRequest, 7
102
+ optional :requestRouteType, :int32, 8
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,23 @@
1
+ module EM::Voldemort
2
+ # Implementation of Voldemort's pb0 (protocol buffers) protocol.
3
+ # Very incomplete -- currently only supports the get command.
4
+ module Protocol
5
+ def get_request(store, key)
6
+ Protobuf::Request.new(
7
+ :type => Protobuf::RequestType::GET,
8
+ :should_route => false,
9
+ :store => store.to_s,
10
+ :get => Protobuf::GetRequest.new(:key => key.to_s)
11
+ ).encode.to_s
12
+ end
13
+
14
+ def get_response(bytes)
15
+ response = Protobuf::GetResponse.decode(bytes.dup)
16
+ if response.error
17
+ raise ClientError, "GetResponse error #{response.error.error_code}: #{response.error.error_message}"
18
+ end
19
+ raise KeyNotFound if response.versioned.nil? || response.versioned.empty?
20
+ response.versioned.max{|a, b| a.version.timestamp <=> b.version.timestamp }.value
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,62 @@
1
+ module EM::Voldemort
2
+ # For a given request, the router figures out which partition on which node in the cluster it
3
+ # should be sent to. Ruby port of voldemort.routing.ConsistentRoutingStrategy.
4
+ class Router
5
+
6
+ def initialize(type, replicas)
7
+ raise ClientError, "unsupported routing strategy: #{type}" if !type && type != 'consistent-routing'
8
+ raise ClientError, "bad number of replicas: #{replicas.inspect}" if !replicas || replicas <= 0
9
+ @replicas = replicas
10
+ end
11
+
12
+ # Returns a list of partitions on which a particular key can be found.
13
+ #
14
+ # @param key A binary string
15
+ # @param partitions Hash of partition number to node
16
+ # @returns Array of partitions (numbers between 0 and partitions.size - 1)
17
+ def partitions(key, partitions)
18
+ master = fnv_hash(key) % partitions.size
19
+ selected = [master]
20
+ nodes = [partitions[master]]
21
+ current = (master + 1) % partitions.size
22
+
23
+ # Walk clockwise around the ring of partitions, starting from the master partition.
24
+ # The next few unique nodes in ring order are the replicas.
25
+ while current != master && selected.size < @replicas
26
+ if !nodes.include? partitions[current]
27
+ nodes << partitions[current]
28
+ selected << current
29
+ end
30
+ current = (current + 1) % partitions.size
31
+ end
32
+
33
+ selected
34
+ end
35
+
36
+
37
+ private
38
+
39
+ FNV_BASIS = 0x811c9dc5
40
+ FNV_PRIME = (1 << 24) + 0x193
41
+
42
+ # Port of voldemort.utils.FnvHashFunction. See also http://www.isthe.com/chongo/tech/comp/fnv
43
+ # Returns a number between 0 and 2**31 - 1.
44
+ def fnv_hash(bytes)
45
+ hash = FNV_BASIS
46
+ bytes.each_byte do |byte|
47
+ hash = (hash ^ byte) * FNV_PRIME % 2**64
48
+ hash -= 2**64 if hash >= 2**63 # simulate overflow of signed long
49
+ end
50
+
51
+ # cast signed long to signed int
52
+ hash = hash % 2**32
53
+ hash -= 2**32 if hash >= 2**31
54
+
55
+ # modified absolute value, as per voldemort.routing.ConsistentRoutingStrategy.abs(int)
56
+ hash = 2**31 - 1 if hash == -2**31
57
+ hash = -hash if hash < 0
58
+ hash
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,108 @@
1
+ module EM::Voldemort
2
+ # Provides access to one particular store on a Voldemort cluster. Deals with encoding of keys and
3
+ # values.
4
+ class Store
5
+
6
+ attr_reader :cluster, :store_name
7
+
8
+ # Creates a new store client from a URL like voldemort://node0.example.com:6666/store-name
9
+ def self.from_url(url, options={})
10
+ url = URI.parse(url) if url.is_a?(String)
11
+ cluster = Cluster.new({:host => url.host, :port => url.port || 6666}.merge(options))
12
+ cluster.store(url.path.sub(%r{\A/}, ''))
13
+ end
14
+
15
+ # Internal -- don't call this from application code. Use {Cluster#store} or {Store.from_url}
16
+ # instead.
17
+ def initialize(cluster, store_name)
18
+ @cluster = cluster
19
+ @store_name = store_name.to_s
20
+ end
21
+
22
+ # Internal
23
+ def load_config(xml)
24
+ @persistence = xml.xpath('persistence').text
25
+ @router = Router.new(xml.xpath('routing-strategy').text, xml.xpath('replication-factor').text.to_i)
26
+ @key_serializer = make_serializer(xml.xpath('key-serializer').first)
27
+ @key_compressor = Compressor.new(xml.xpath('key-serializer/compression').first)
28
+ @value_serializer = make_serializer(xml.xpath('value-serializer').first)
29
+ @value_compressor = Compressor.new(xml.xpath('value-serializer/compression').first)
30
+ end
31
+
32
+ # Fetches the value associated with a particular key in this store. Returns a deferrable that
33
+ # succeeds with the value, or fails with an exception object. If a serializer is configured for
34
+ # the store, the key is automatically serialized and the value automatically unserialized.
35
+ def get(key)
36
+ EM::DefaultDeferrable.new.tap do |request|
37
+ if @persistence
38
+ get_after_bootstrap(key, request)
39
+ else
40
+ bootstrap = @cluster.connect
41
+ if bootstrap
42
+ bootstrap.callback { get_after_bootstrap(key, request) }
43
+ bootstrap.errback do
44
+ request.fail(ServerError.new('Could not bootstrap Voldemort cluster'))
45
+ end
46
+ else
47
+ request.fail(ClientError.new("Store #{store_name} is not configured on the cluster"))
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def get_after_bootstrap(key, deferrable)
56
+ if @persistence.nil?
57
+ deferrable.fail(ClientError.new("Store #{store_name} is not configured on the cluster"))
58
+ elsif @persistence != 'read-only'
59
+ deferrable.fail(ClientError.new("Sorry, accessing #{persistence} stores is not yet supported"))
60
+ else
61
+ begin
62
+ encoded_key = encode_key(key)
63
+ rescue => error
64
+ deferrable.fail(error)
65
+ else
66
+ request = @cluster.get(store_name, encoded_key, @router)
67
+ request.errback {|error| deferrable.fail(error) }
68
+
69
+ request.callback do |response|
70
+ begin
71
+ value = decode_value(response)
72
+ rescue => error
73
+ deferrable.fail(error)
74
+ else
75
+ deferrable.succeed(value)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ def encode_key(key)
83
+ @key_compressor.encode(@key_serializer.encode(key))
84
+ end
85
+
86
+ def decode_value(value)
87
+ @value_serializer.decode(@value_compressor.decode(value))
88
+ end
89
+
90
+ def make_serializer(xml)
91
+ if xml.xpath('type').text == 'json'
92
+ has_version_tag = true
93
+ schemas = xml.xpath('schema-info').each_with_object({}) do |schema, hash|
94
+ has_version_tag = false if schema['version'] == 'none'
95
+ hash[schema['version'].to_i] = schema.text
96
+ end
97
+ BinaryJson.new(schemas, has_version_tag)
98
+ else
99
+ NullSerializer.new
100
+ end
101
+ end
102
+
103
+ class NullSerializer
104
+ def encode(data); data; end
105
+ def decode(data); data; end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe EM::Voldemort::BinaryJson do
4
+ describe 'encoding strings' do
5
+ before do
6
+ @codec = EM::Voldemort::BinaryJson.new(0 => '"string"')
7
+ end
8
+
9
+ it 'should encode short strings' do
10
+ @codec.encode('hello').should == "\x00\x00\x05hello"
11
+ end
12
+
13
+ it 'should decode short strings' do
14
+ @codec.decode("\x00\x00\x05hello").should == 'hello'
15
+ end
16
+
17
+ it 'should encode strings between 16kB and 32kB in length' do
18
+ @codec.encode('hellohello' * 1700).should == "\x00\x42\x68" + 'hellohello' * 1700
19
+ end
20
+
21
+ it 'should decode strings between 16kB and 32kB in length' do
22
+ @codec.decode("\x00\x42\x68" + 'hellohello' * 1700).should == 'hellohello' * 1700
23
+ end
24
+
25
+ it 'should encode strings above 32kB in length' do
26
+ @codec.encode('hellohello' * 3400).should == "\x00\xC0\x00\x84\xd0" + 'hellohello' * 3400
27
+ end
28
+
29
+ it 'should decode strings above 32kB in length' do
30
+ @codec.decode("\x00\xC0\x00\x84\xd0" + 'hellohello' * 3400).should == 'hellohello' * 3400
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,363 @@
1
+ require 'spec_helper'
2
+
3
+ describe EM::Voldemort::Cluster do
4
+ before do
5
+ @timer_double = double('timer', :cancel => nil)
6
+ EM::Voldemort::Cluster.any_instance.stub(:setup_bootstrap_timer) do |&timer|
7
+ @bootstrap_timer = timer
8
+ @timer_double
9
+ end
10
+
11
+ @logger = Logger.new($stdout)
12
+ @logger.level = Logger::ERROR
13
+ @cluster = EM::Voldemort::Cluster.new :host => 'localhost', :port => 6666, :logger => @logger
14
+
15
+ Timecop.freeze
16
+ end
17
+
18
+ def request(store, key)
19
+ EM::Voldemort::Protobuf::Request.new(
20
+ :type => EM::Voldemort::Protobuf::RequestType::GET,
21
+ :should_route => false,
22
+ :store => store.to_s,
23
+ :get => EM::Voldemort::Protobuf::GetRequest.new(:key => key.to_s)
24
+ ).encode.to_s
25
+ end
26
+
27
+ def success_response(value)
28
+ EM::Voldemort::Protobuf::GetResponse.new(
29
+ :versioned => [EM::Voldemort::Protobuf::Versioned.new(
30
+ :value => value,
31
+ :version => EM::Voldemort::Protobuf::VectorClock.new(
32
+ :entries => [], # read-only stores leave this empty
33
+ :timestamp => (Time.now.to_r * 1000).to_i
34
+ )
35
+ )]
36
+ ).encode.to_s
37
+ end
38
+
39
+ def error_response(code, message)
40
+ EM::Voldemort::Protobuf::GetResponse.new(
41
+ :error => EM::Voldemort::Protobuf::Error.new(:error_code => code, :error_message => message)
42
+ ).encode.to_s
43
+ end
44
+
45
+ def cluster_xml(partitions_by_host={})
46
+ servers = ''
47
+ partitions_by_host.each_with_index do |(host, partitions), index|
48
+ servers << '<server>'
49
+ servers << "<id>#{index}</id>"
50
+ servers << "<host>#{host}</host>"
51
+ servers << '<http-port>8081</http-port><socket-port>6666</socket-port><admin-port>6667</admin-port>'
52
+ servers << "<partitions>#{partitions.join(', ')}</partitions>"
53
+ servers << '</server>'
54
+ end
55
+ "<cluster><name>example-cluster</name>#{servers}</cluster>"
56
+ end
57
+
58
+ def stores_xml(properties_by_name={})
59
+ stores = ''
60
+ properties_by_name.each_pair do |name, properties|
61
+ stores << '<store>'
62
+ stores << "<name>#{name}</name>"
63
+ stores << '<persistence>read-only</persistence>'
64
+ stores << '<routing-strategy>consistent-routing</routing-strategy>'
65
+ stores << '<routing>client</routing>'
66
+ stores << '<replication-factor>2</replication-factor>'
67
+ stores << '<required-reads>1</required-reads>'
68
+ stores << '<required-writes>1</required-writes>'
69
+ stores << '<key-serializer>'
70
+ stores << "<type>#{properties[:key_type]}</type>"
71
+ stores << "<schema-info version=\"0\">#{properties[:key_schema]}</schema-info>"
72
+ stores << '</key-serializer>'
73
+ stores << '<value-serializer>'
74
+ stores << "<type>#{properties[:value_type]}</type>"
75
+ properties[:value_schemas].each_pair do |version, schema|
76
+ stores << "<schema-info version=\"#{version}\">#{schema}</schema-info>"
77
+ end
78
+ stores << "<compression><type>#{properties[:compression]}</type></compression>"
79
+ stores << '</value-serializer>'
80
+ stores << '</store>'
81
+ end
82
+ "<stores>#{stores}</stores>"
83
+ end
84
+
85
+ def expect_bootstrap(cluster_info={}, stores_info={})
86
+ @bootstrap_connection = double('bootstrap connection')
87
+ EM::Voldemort::Connection.should_receive(:new).
88
+ with(:host => 'localhost', :port => 6666, :logger => @logger).
89
+ and_return(@bootstrap_connection)
90
+
91
+ cluster_request = EM::DefaultDeferrable.new
92
+ @bootstrap_connection.should_receive(:send_request).with(request('metadata', 'cluster.xml')) do
93
+ EM.next_tick do
94
+ stores_request = EM::DefaultDeferrable.new
95
+ @bootstrap_connection.should_receive(:send_request).with(request('metadata', 'stores.xml')) do
96
+ EM.next_tick do
97
+ stores_request.succeed(success_response(stores_xml(stores_info)))
98
+ end
99
+ stores_request
100
+ end
101
+ cluster_request.succeed(success_response(cluster_xml(cluster_info)))
102
+ end
103
+ cluster_request
104
+ end
105
+
106
+ @bootstrap_connection.should_receive(:close) { yield if block_given? }
107
+ end
108
+
109
+
110
+ it 'should request cluster.xml and stores.xml when bootstrapping' do
111
+ expect_bootstrap('voldemort0.example.com' => [0])
112
+ EM::Voldemort::Connection.should_receive(:new).
113
+ with(:host => 'voldemort0.example.com', :port => 6666, :node_id => 0, :logger => @logger).
114
+ and_return(double('Connection 0'))
115
+ EM.run { @cluster.connect.callback { EM.stop } }
116
+ end
117
+
118
+ it 'should make a connection to each node in the cluster' do
119
+ expect_bootstrap('voldemort0.example.com' => [0, 1, 2, 3], 'voldemort1.example.com' => [4, 5, 6, 7])
120
+ EM::Voldemort::Connection.should_receive(:new).
121
+ with(:host => 'voldemort0.example.com', :port => 6666, :node_id => 0, :logger => @logger).
122
+ and_return(double('Connection 0'))
123
+ EM::Voldemort::Connection.should_receive(:new).
124
+ with(:host => 'voldemort1.example.com', :port => 6666, :node_id => 1, :logger => @logger).
125
+ and_return(double('Connection 1'))
126
+ EM.run { @cluster.connect.callback { EM.stop } }
127
+ end
128
+
129
+ it 'should retry bootstrapping if it fails' do
130
+ EM.run do
131
+ request1 = EM::DefaultDeferrable.new
132
+ connection1 = double('connection attempt 1', :send_request => request1, :close => nil)
133
+ EM::Voldemort::Connection.should_receive(:new).and_return(connection1)
134
+ @cluster.connect
135
+ @bootstrap_timer.call
136
+ EM.next_tick do
137
+ request1.fail(EM::Voldemort::ServerError.new('connection refused'))
138
+ EM.next_tick do
139
+ request2 = EM::DefaultDeferrable.new
140
+ connection2 = double('connection attempt 2', :send_request => request2, :close => nil)
141
+ EM::Voldemort::Connection.should_receive(:new).and_return(connection2)
142
+ @bootstrap_timer.call
143
+ EM.next_tick do
144
+ request2.fail(EM::Voldemort::ServerError.new('connection refused'))
145
+ EM.next_tick do
146
+ EM::Voldemort::Connection.should_receive(:new).
147
+ with(:host => 'voldemort0.example.com', :port => 6666, :node_id => 0, :logger => @logger).
148
+ and_return(double('Connection 0'))
149
+ expect_bootstrap('voldemort0.example.com' => [0]) { EM.stop }
150
+ @timer_double.should_receive(:cancel)
151
+ @bootstrap_timer.call
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ it 'should delay requests until bootstrapping is complete' do
160
+ metadata_request = EM::DefaultDeferrable.new
161
+ bootstrap = double('bootstrap connection', :send_request => metadata_request, :close => nil)
162
+ EM::Voldemort::Connection.should_receive(:new).and_return(bootstrap)
163
+ connection = double('connection', :health => :good)
164
+ EM::Voldemort::Connection.should_receive(:new).and_return(connection)
165
+ EM.run do
166
+ @cluster.get('store1', 'request1').callback {|response| @response1 = response }
167
+ @cluster.get('store1', 'request2').callback do |response|
168
+ @response1.should == 'response1'
169
+ response.should == 'response2'
170
+ EM.stop
171
+ end
172
+ EM.next_tick do
173
+ connection.should_receive(:send_request).with(request('store1', 'request1')) do
174
+ EM::DefaultDeferrable.new.tap do |deferrable|
175
+ EM.next_tick { deferrable.succeed(success_response('response1')) }
176
+ end
177
+ end
178
+ connection.should_receive(:send_request).with(request('store1', 'request2')) do
179
+ EM::DefaultDeferrable.new.tap do |deferrable|
180
+ EM.next_tick { deferrable.succeed(success_response('response2')) }
181
+ end
182
+ end
183
+ metadata_request.succeed(success_response(cluster_xml('voldemort0.example.com' => [0, 1, 2, 3])))
184
+ end
185
+ end
186
+ end
187
+
188
+ it 'should fail requests if bootstrapping failed' do
189
+ EM.run do
190
+ metadata_request = EM::DefaultDeferrable.new
191
+ bootstrap = double('bootstrap connection', :send_request => metadata_request, :close => nil)
192
+ EM::Voldemort::Connection.should_receive(:new).and_return(bootstrap)
193
+ @cluster.get('store1', 'request1').errback {|error| @error = error }
194
+ EM.next_tick do
195
+ @error.should be_nil
196
+ metadata_request.fail(EM::Voldemort::ServerError.new('connection refused'))
197
+ @error.should be_a(EM::Voldemort::ServerError)
198
+ EM.stop
199
+ end
200
+ end
201
+ end
202
+
203
+ it 'should handle invalid XML responses' do
204
+ bootstrap = double('bootstrap connection', :close => nil)
205
+ EM::Voldemort::Connection.should_receive(:new).and_return(bootstrap)
206
+ cluster_request = EM::DefaultDeferrable.new
207
+ bootstrap.should_receive(:send_request).with(request('metadata', 'cluster.xml')) do
208
+ EM.next_tick { cluster_request.succeed(success_response("<xml>Ceci n'est pas XML.</xml>")) }
209
+ cluster_request
210
+ end
211
+ EM.run { @cluster.connect.errback { EM.stop } }
212
+ end
213
+
214
+
215
+ describe 'handling unavailable nodes' do
216
+ before do
217
+ expect_bootstrap('node0' => [0, 1, 2, 3], 'node1' => [4, 5, 6, 7])
218
+ @conn0 = double('connection 0')
219
+ @conn1 = double('connection 1')
220
+ EM::Voldemort::Connection.should_receive(:new).with(hash_including(:host => 'node0')).and_return(@conn0)
221
+ EM::Voldemort::Connection.should_receive(:new).with(hash_including(:host => 'node1')).and_return(@conn1)
222
+ end
223
+
224
+ it 'should only make a request to one connection if it is healthy' do
225
+ EM.run do
226
+ @conn0.should_receive(:health).and_return(:good)
227
+ @conn0.should_receive(:send_request).with(request('store', 'request')) do
228
+ EM::DefaultDeferrable.new.tap do |request1|
229
+ EM.next_tick { request1.succeed(success_response('response1')) }
230
+ end
231
+ end
232
+ @cluster.get('store', 'request', double('router', :partitions => [2, 4])).callback do |response|
233
+ response.should == 'response1'
234
+ EM.stop
235
+ end
236
+ end
237
+ end
238
+
239
+ it 'should retry a request on another connection if the first request failed' do
240
+ EM.run do
241
+ @conn0.should_receive(:health).and_return(:good)
242
+ @conn0.should_receive(:send_request).with(request('store', 'request')) do
243
+ EM::DefaultDeferrable.new.tap do |request1|
244
+ EM.next_tick do
245
+ @conn1.should_receive(:send_request).with(request('store', 'request')) do
246
+ EM::DefaultDeferrable.new.tap do |request2|
247
+ EM.next_tick { request2.succeed(success_response('response2')) }
248
+ end
249
+ end
250
+ request1.fail(EM::Voldemort::ServerError.new('connection closed'))
251
+ end
252
+ end
253
+ end
254
+ @cluster.get('store', 'request', double('router', :partitions => [2, 4])).callback do |response|
255
+ response.should == 'response2'
256
+ EM.stop
257
+ end
258
+ end
259
+ end
260
+
261
+ it 'should fail the request if all attempts fail' do
262
+ EM.run do
263
+ @conn0.should_receive(:health).and_return(:good)
264
+ @conn0.should_receive(:send_request).with(request('store', 'request')) do
265
+ EM::DefaultDeferrable.new.tap do |request1|
266
+ EM.next_tick do
267
+ @conn1.should_receive(:send_request).with(request('store', 'request')) do
268
+ EM::DefaultDeferrable.new.tap do |request2|
269
+ EM.next_tick { request2.fail(EM::Voldemort::ServerError.new('no route to host')) }
270
+ end
271
+ end
272
+ request1.fail(EM::Voldemort::ServerError.new('connection timed out'))
273
+ end
274
+ end
275
+ end
276
+ @cluster.get('store', 'request', double('router', :partitions => [2, 4])).errback do |error|
277
+ error.should be_a(EM::Voldemort::ServerError)
278
+ error.message.should == 'no route to host'
279
+ EM.stop
280
+ end
281
+ end
282
+ end
283
+
284
+ it 'should not retry requests that failed due to client error' do
285
+ EM.run do
286
+ @conn0.should_receive(:health).and_return(:good)
287
+ @conn0.should_receive(:send_request).with(request('store', 'request')) do
288
+ EM::DefaultDeferrable.new.tap do |request1|
289
+ EM.next_tick { request1.succeed('') } # empty response = no value for that key
290
+ end
291
+ end
292
+ @cluster.get('store', 'request', double('router', :partitions => [2, 4])).errback do |error|
293
+ error.should be_a(EM::Voldemort::KeyNotFound)
294
+ EM.stop
295
+ end
296
+ end
297
+ end
298
+
299
+ it 'should retry a request on another connection if parsing the first response failed' do
300
+ EM.run do
301
+ @conn0.should_receive(:health).and_return(:good)
302
+ @conn0.should_receive(:send_request).with(request('store', 'request')) do
303
+ EM::DefaultDeferrable.new.tap do |request1|
304
+ EM.next_tick do
305
+ @conn1.should_receive(:send_request).with(request('store', 'request')) do
306
+ EM::DefaultDeferrable.new.tap do |request2|
307
+ EM.next_tick { request2.succeed(success_response('response2')) }
308
+ end
309
+ end
310
+ @logger.should_receive(:error).with(/protocol error/)
311
+ request1.succeed("\x00") # not valid protobuf
312
+ end
313
+ end
314
+ end
315
+ @cluster.get('store', 'request', double('router', :partitions => [2, 4])).callback do |response|
316
+ response.should == 'response2'
317
+ EM.stop
318
+ end
319
+ end
320
+ end
321
+
322
+ it 'should keep trying to make requests to a node that is down' do
323
+ EM.run do
324
+ @conn0.should_receive(:health).and_return(:bad)
325
+ @conn0.should_receive(:send_request).with(request('store', 'request')) do
326
+ EM::DefaultDeferrable.new.tap do |request1|
327
+ EM.next_tick { request1.fail(EM::Voldemort::ServerError.new('not connected')) }
328
+ end
329
+ end
330
+ @conn1.should_receive(:send_request).with(request('store', 'request')) do
331
+ EM::DefaultDeferrable.new.tap do |request2|
332
+ EM.next_tick { request2.succeed(success_response('response2')) }
333
+ end
334
+ end
335
+ @cluster.get('store', 'request', double('router', :partitions => [2, 4])).callback do |response|
336
+ response.should == 'response2'
337
+ EM.stop
338
+ end
339
+ end
340
+ end
341
+
342
+ it 'should fail the request if all nodes are down' do
343
+ EM.run do
344
+ @conn0.should_receive(:health).and_return(:bad)
345
+ @conn0.should_receive(:send_request).with(request('store', 'request')) do
346
+ EM::DefaultDeferrable.new.tap do |request1|
347
+ EM.next_tick { request1.fail(EM::Voldemort::ServerError.new('not connected')) }
348
+ end
349
+ end
350
+ @conn1.should_receive(:send_request).with(request('store', 'request')) do
351
+ EM::DefaultDeferrable.new.tap do |request2|
352
+ EM.next_tick { request2.fail(EM::Voldemort::ServerError.new('not connected')) }
353
+ end
354
+ end
355
+ @cluster.get('store', 'request', double('router', :partitions => [2, 4])).errback do |error|
356
+ error.should be_a(EM::Voldemort::ServerError)
357
+ error.message.should == 'not connected'
358
+ EM.stop
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end