em-voldemort 0.1.5

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