em-voldemort 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +2 -0
- data/LICENSE +19 -0
- data/README.md +78 -0
- data/em-voldemort.gemspec +23 -0
- data/lib/em-voldemort.rb +11 -0
- data/lib/em-voldemort/binary_json.rb +330 -0
- data/lib/em-voldemort/cluster.rb +247 -0
- data/lib/em-voldemort/compressor.rb +39 -0
- data/lib/em-voldemort/connection.rb +234 -0
- data/lib/em-voldemort/errors.rb +13 -0
- data/lib/em-voldemort/protobuf.rb +105 -0
- data/lib/em-voldemort/protocol.rb +23 -0
- data/lib/em-voldemort/router.rb +62 -0
- data/lib/em-voldemort/store.rb +108 -0
- data/spec/em-voldemort/binary_json_spec.rb +33 -0
- data/spec/em-voldemort/cluster_spec.rb +363 -0
- data/spec/em-voldemort/connection_spec.rb +307 -0
- data/spec/em-voldemort/fixtures/cluster.xml +323 -0
- data/spec/em-voldemort/router_spec.rb +73 -0
- data/spec/spec_helper.rb +9 -0
- metadata +164 -0
@@ -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
|