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.
- 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
|