voldemort-rb 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +0 -0
- data/MIT-LICENSE +20 -0
- data/README.md +46 -0
- data/Rakefile +60 -0
- data/lib/connection/connection.rb +115 -0
- data/lib/connection/tcp_connection.rb +171 -0
- data/lib/connection/voldemort_node.rb +3 -0
- data/lib/protos/voldemort-client.pb.rb +190 -0
- data/lib/protos/voldemort-client.proto +92 -0
- data/lib/voldemort-rb.rb +47 -0
- data/spec/connection_spec.rb +97 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/tcp_connection_spec.rb +40 -0
- data/spec/voldemort_client_spec.rb +97 -0
- data/spec/voldemort_node_spec.rb +17 -0
- metadata +76 -0
data/CHANGELOG
ADDED
File without changes
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Alejandro Crosa
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
Voldemort-client
|
2
|
+
================
|
3
|
+
|
4
|
+
# Requirements
|
5
|
+
|
6
|
+
Since the communication between the client and the server is done using protocol buffers you'll need the ruby_protobuf gem found at http://code.google.com/p/ruby-protobuf/.
|
7
|
+
|
8
|
+
Examples
|
9
|
+
=======
|
10
|
+
|
11
|
+
# Basic Usage
|
12
|
+
## Connecting and bootstrapping
|
13
|
+
|
14
|
+
client = VoldemortClient.new("test", "localhost:6666")
|
15
|
+
|
16
|
+
## Storing a value
|
17
|
+
|
18
|
+
client.put("some key", "some value")
|
19
|
+
|
20
|
+
## Reading a value
|
21
|
+
|
22
|
+
client.get("some key")
|
23
|
+
|
24
|
+
you'll get
|
25
|
+
|
26
|
+
=> some value
|
27
|
+
|
28
|
+
## deleting a value from a key
|
29
|
+
|
30
|
+
client.delete("some key")
|
31
|
+
|
32
|
+
# Conflict resolution
|
33
|
+
## Default
|
34
|
+
|
35
|
+
Voldemort replies with versions of a value, it's up to the client to resolve the conflicts. By default the library will return the version that's most recent.
|
36
|
+
|
37
|
+
## Custom
|
38
|
+
|
39
|
+
You can override the default behavior and perform a custom resolution of the conflict, here's how to do so:
|
40
|
+
|
41
|
+
client = VoldemortClient.new("test", "localhost:6666") do |versions|
|
42
|
+
versions.first # just return the first version for example
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
Copyright (c) 2010 Alejandro Crosa, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rubygems/specification'
|
4
|
+
require 'date'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
|
7
|
+
GEM = 'Voldemort Client'
|
8
|
+
GEM_NAME = 'voldemort_client'
|
9
|
+
GEM_VERSION = '0.1'
|
10
|
+
AUTHORS = ['Alejandro Crosa']
|
11
|
+
EMAIL = "alejandrocrosa@gmail.com"
|
12
|
+
HOMEPAGE = "http://github.com/acrosa/Voldemort-Ruby-Client"
|
13
|
+
SUMMARY = "A Ruby client for the Voldemort distributed key value store"
|
14
|
+
|
15
|
+
spec = Gem::Specification.new do |s|
|
16
|
+
s.name = GEM
|
17
|
+
s.version = GEM_VERSION
|
18
|
+
s.platform = Gem::Platform::RUBY
|
19
|
+
s.has_rdoc = true
|
20
|
+
s.extra_rdoc_files = ["LICENSE"]
|
21
|
+
s.summary = SUMMARY
|
22
|
+
s.description = s.summary
|
23
|
+
s.authors = AUTHORS
|
24
|
+
s.email = EMAIL
|
25
|
+
s.homepage = HOMEPAGE
|
26
|
+
s.add_development_dependency "rspec"
|
27
|
+
s.require_path = 'lib'
|
28
|
+
s.autorequire = GEM
|
29
|
+
s.files = %w(LICENSE README.md Rakefile) + Dir.glob("{lib,tasks,spec}/**/*")
|
30
|
+
end
|
31
|
+
|
32
|
+
task :default => :spec
|
33
|
+
|
34
|
+
desc "Run specs"
|
35
|
+
Spec::Rake::SpecTask.new do |t|
|
36
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
37
|
+
t.spec_opts = %w(-fs --color)
|
38
|
+
end
|
39
|
+
|
40
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
41
|
+
pkg.gem_spec = spec
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "install the gem locally"
|
45
|
+
task :install => [:package] do
|
46
|
+
sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
|
47
|
+
end
|
48
|
+
|
49
|
+
desc "create a gemspec file"
|
50
|
+
task :make_spec do
|
51
|
+
File.open("#{GEM}.gemspec", "w") do |file|
|
52
|
+
file.puts spec.to_ruby
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
desc "Run all examples with RCov"
|
57
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
58
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
59
|
+
t.rcov = true
|
60
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
|
3
|
+
class Connection
|
4
|
+
include REXML
|
5
|
+
|
6
|
+
attr_accessor :hosts # The hosts from where we bootstrapped.
|
7
|
+
attr_accessor :nodes # The array of VoldemortNodes available.
|
8
|
+
attr_accessor :db_name # The DB store name.
|
9
|
+
attr_accessor :connected_node # The VoldemortNode we are connected to.
|
10
|
+
attr_accessor :request_count # Used to track the number of request a node receives.
|
11
|
+
attr_accessor :request_limit_per_node # Limit the number of request per node.
|
12
|
+
|
13
|
+
STATUS_OK = "ok"
|
14
|
+
PROTOCOL = "pb0"
|
15
|
+
DEFAULT_REQUEST_LIMIT_PER_NODE = 500
|
16
|
+
|
17
|
+
def initialize(db_name, hosts, request_limit_per_node = DEFAULT_REQUEST_LIMIT_PER_NODE)
|
18
|
+
self.db_name = db_name
|
19
|
+
self.hosts = hosts
|
20
|
+
self.nodes = hosts.collect{ |h|
|
21
|
+
n = h.split(":")
|
22
|
+
node = VoldemortNode.new
|
23
|
+
node.host = n[0]
|
24
|
+
node.port = n[1]
|
25
|
+
node
|
26
|
+
}
|
27
|
+
self.request_count = 0
|
28
|
+
self.request_limit_per_node = request_limit_per_node
|
29
|
+
end
|
30
|
+
|
31
|
+
def bootstrap
|
32
|
+
response = self.get_from("metadata", "cluster.xml", false)
|
33
|
+
xml = response[1][0][1]
|
34
|
+
self.nodes = self.parse_nodes_from(xml)
|
35
|
+
self.connect_to_random_node
|
36
|
+
rescue StandardError => e
|
37
|
+
raise("There was an error trying to bootstrap from the specified servers: #{e}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def connect_to_random_node
|
41
|
+
nodes = self.nodes.sort_by { rand }
|
42
|
+
for node in nodes do
|
43
|
+
if self.connect_to(node.host, node.port)
|
44
|
+
self.connected_node = node
|
45
|
+
self.request_count = 0
|
46
|
+
return node
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_nodes_from(xml)
|
52
|
+
nodes = []
|
53
|
+
doc = REXML::Document.new(xml)
|
54
|
+
XPath.each(doc, "/cluster/server").each do |n|
|
55
|
+
node = VoldemortNode.new
|
56
|
+
node.id = n.elements["id"].text
|
57
|
+
node.host = n.elements["host"].text
|
58
|
+
node.port = n.elements["socket-port"].text
|
59
|
+
node.http_port = n.elements["http-port"].text
|
60
|
+
node.admin_port = n.elements["admin-port"].text
|
61
|
+
node.partitions = n.elements["partitions"].text
|
62
|
+
nodes << node
|
63
|
+
end
|
64
|
+
nodes
|
65
|
+
end
|
66
|
+
|
67
|
+
def protocol_version
|
68
|
+
PROTOCOL
|
69
|
+
end
|
70
|
+
|
71
|
+
def connect
|
72
|
+
self.connect!
|
73
|
+
end
|
74
|
+
|
75
|
+
def reconnect
|
76
|
+
self.reconnect!
|
77
|
+
end
|
78
|
+
|
79
|
+
def disconnect
|
80
|
+
self.disconnect!
|
81
|
+
end
|
82
|
+
|
83
|
+
def reconnect_when_errors_in(response = nil)
|
84
|
+
return unless response
|
85
|
+
self.reconnect! if response.error
|
86
|
+
end
|
87
|
+
|
88
|
+
def rebalance_connection?
|
89
|
+
self.request_count >= self.request_limit_per_node
|
90
|
+
end
|
91
|
+
|
92
|
+
def rebalance_connection_if_needed
|
93
|
+
self.reconnect if self.rebalance_connection?
|
94
|
+
self.request_count += 1
|
95
|
+
end
|
96
|
+
|
97
|
+
def get(key)
|
98
|
+
self.rebalance_connection_if_needed
|
99
|
+
self.get_from(self.db_name, key, true)
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_all(keys)
|
103
|
+
self.rebalance_connection_if_needed
|
104
|
+
self.get_all_from(self.db_name, keys, true)
|
105
|
+
end
|
106
|
+
|
107
|
+
def put(key, value, version = nil, route = true)
|
108
|
+
self.rebalance_connection_if_needed
|
109
|
+
self.put_from(self.db_name, key, value, version, route)
|
110
|
+
end
|
111
|
+
|
112
|
+
def delete(key)
|
113
|
+
self.delete_from(self.db_name, key)
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
require File.join(File.dirname(__FILE__), "..", "protos", "voldemort-client.pb")
|
5
|
+
|
6
|
+
class TCPConnection < Connection
|
7
|
+
include Voldemort
|
8
|
+
|
9
|
+
attr_accessor :socket
|
10
|
+
|
11
|
+
SOCKET_TIMEOUT = 3
|
12
|
+
|
13
|
+
def connect_to(host, port)
|
14
|
+
begin
|
15
|
+
timeout(SOCKET_TIMEOUT) do
|
16
|
+
self.socket = TCPSocket.open(host, port)
|
17
|
+
self.send_protocol_version
|
18
|
+
if(protocol_handshake_ok?)
|
19
|
+
return self.socket
|
20
|
+
else
|
21
|
+
raise "There was an error connecting to the node"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
rescue Timeout::Error
|
25
|
+
raise "Timeout when connecting to node"
|
26
|
+
rescue
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# performs a get using the specified parameters
|
32
|
+
#
|
33
|
+
def get_from(db_name, key, route = true)
|
34
|
+
request = VoldemortRequest.new
|
35
|
+
request.should_route = route
|
36
|
+
request.store = db_name
|
37
|
+
request.type = RequestType::GET
|
38
|
+
request.get = GetRequest.new
|
39
|
+
request.get.key = key
|
40
|
+
|
41
|
+
self.send(request) # send the request
|
42
|
+
raw_response = self.receive # read the response
|
43
|
+
response = GetResponse.new.parse_from_string(raw_response) # compose the get object based on the raw response
|
44
|
+
reconnect_when_errors_in(response)
|
45
|
+
response
|
46
|
+
end
|
47
|
+
|
48
|
+
# performs a get using multiple keys
|
49
|
+
#
|
50
|
+
def get_all_from(db_name, keys, route = true)
|
51
|
+
request = VoldemortRequest.new
|
52
|
+
request.should_route = route
|
53
|
+
request.store = db_name
|
54
|
+
request.type = RequestType::GET_ALL
|
55
|
+
request.getAll = GetAllRequest.new
|
56
|
+
request.getAll.keys = keys
|
57
|
+
|
58
|
+
self.send(request) # send the request
|
59
|
+
raw_response = self.receive # read the response
|
60
|
+
response = GetAllResponse.new.parse_from_string(raw_response) # compose the get object based on the raw response
|
61
|
+
reconnect_when_errors_in(response)
|
62
|
+
response
|
63
|
+
end
|
64
|
+
|
65
|
+
def put_from(db_name, key, value, version = nil, route = true)
|
66
|
+
version = get_version(key) unless version
|
67
|
+
request = VoldemortRequest.new
|
68
|
+
request.should_route = route
|
69
|
+
request.store = db_name
|
70
|
+
request.type = RequestType::PUT
|
71
|
+
request.put = PutRequest.new
|
72
|
+
request.put.key = key
|
73
|
+
request.put.versioned = Versioned.new
|
74
|
+
request.put.versioned.value = value
|
75
|
+
request.put.versioned.version = VectorClock.new
|
76
|
+
request.put.versioned.version.merge_from(version)
|
77
|
+
|
78
|
+
self.send(request) # send the request
|
79
|
+
raw_response = self.receive # read the response
|
80
|
+
response = PutResponse.new.parse_from_string(raw_response)
|
81
|
+
reconnect_when_errors_in(response)
|
82
|
+
|
83
|
+
add_to_versions(version) # add version or increment when needed
|
84
|
+
version
|
85
|
+
end
|
86
|
+
|
87
|
+
def delete_from(db_name, key, version = nil, route = true)
|
88
|
+
version = get_version(key) unless version
|
89
|
+
request = VoldemortRequest.new
|
90
|
+
request.should_route = route
|
91
|
+
request.store = db_name
|
92
|
+
request.type = RequestType::DELETE
|
93
|
+
request.delete = DeleteRequest.new
|
94
|
+
request.delete.key = key
|
95
|
+
request.delete.version = VectorClock.new
|
96
|
+
request.delete.version.merge_from(version)
|
97
|
+
|
98
|
+
self.send(request) # send the request
|
99
|
+
raw_response = self.receive # read the response
|
100
|
+
response = DeleteResponse.new.parse_from_string(raw_response)
|
101
|
+
reconnect_when_errors_in(response)
|
102
|
+
response.success
|
103
|
+
end
|
104
|
+
|
105
|
+
def add_to_versions(version)
|
106
|
+
entry = version.entries.detect { |e| e.node_id == self.connected_node.id.to_i }
|
107
|
+
if(entry)
|
108
|
+
entry.version += 1
|
109
|
+
else
|
110
|
+
entry = ClockEntry.new
|
111
|
+
entry.node_id = self.connected_node.id.to_i
|
112
|
+
entry.version = 1
|
113
|
+
version.entries << entry
|
114
|
+
version.timestamp = Time.new.to_i * 1000
|
115
|
+
end
|
116
|
+
version
|
117
|
+
end
|
118
|
+
|
119
|
+
def get_version(key)
|
120
|
+
other_version = get(key)[1][0]
|
121
|
+
if(other_version)
|
122
|
+
return other_version.version
|
123
|
+
else
|
124
|
+
version = VectorClock.new
|
125
|
+
version.timestamp = Time.new.to_i * 1000
|
126
|
+
return version
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# unpack argument is N | Long, network (big-endian) byte order.
|
131
|
+
# from http://ruby-doc.org/doxygen/1.8.4/pack_8c-source.html
|
132
|
+
def receive
|
133
|
+
raw_size = self.socket.recv(4)
|
134
|
+
size = raw_size.unpack('N')
|
135
|
+
self.socket.recv(size[0])
|
136
|
+
rescue
|
137
|
+
self.reconnect!
|
138
|
+
end
|
139
|
+
|
140
|
+
# pack argument is N | Long, network (big-endian) byte order.
|
141
|
+
# from http://ruby-doc.org/doxygen/1.8.4/pack_8c-source.html
|
142
|
+
def send(request)
|
143
|
+
self.reconnect unless self.socket
|
144
|
+
bytes = request.serialize_to_string # helper method thanks to ruby-protobuf
|
145
|
+
self.socket.write([bytes.size].pack("N") + bytes)
|
146
|
+
rescue
|
147
|
+
self.disconnect!
|
148
|
+
end
|
149
|
+
|
150
|
+
def send_protocol_version
|
151
|
+
self.socket.write(self.protocol_version)
|
152
|
+
end
|
153
|
+
|
154
|
+
def protocol_handshake_ok?
|
155
|
+
self.socket.recv(2) == STATUS_OK
|
156
|
+
end
|
157
|
+
|
158
|
+
def connect!
|
159
|
+
self.connect_to_random_node
|
160
|
+
end
|
161
|
+
|
162
|
+
def reconnect!
|
163
|
+
self.disconnect! if self.socket
|
164
|
+
self.connect!
|
165
|
+
end
|
166
|
+
|
167
|
+
def disconnect!
|
168
|
+
self.socket.close if self.socket
|
169
|
+
self.socket = nil
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
### Generated by rprotoc. DO NOT EDIT!
|
2
|
+
### <proto file: voldemort-client.proto>
|
3
|
+
# package voldemort;
|
4
|
+
#
|
5
|
+
# option java_package = "voldemort.client.protocol.pb";
|
6
|
+
# option java_outer_classname = "VProto";
|
7
|
+
# option optimize_for = SPEED;
|
8
|
+
#
|
9
|
+
# message ClockEntry {
|
10
|
+
# required int32 node_id = 1;
|
11
|
+
# required int64 version = 2;
|
12
|
+
# }
|
13
|
+
#
|
14
|
+
# message VectorClock {
|
15
|
+
# repeated ClockEntry entries = 1;
|
16
|
+
# optional int64 timestamp = 2;
|
17
|
+
# }
|
18
|
+
#
|
19
|
+
# message Versioned {
|
20
|
+
# required bytes value = 1;
|
21
|
+
# required VectorClock version = 2;
|
22
|
+
# }
|
23
|
+
#
|
24
|
+
# message Error {
|
25
|
+
# required int32 error_code = 1;
|
26
|
+
# required string error_message = 2;
|
27
|
+
# }
|
28
|
+
#
|
29
|
+
# message KeyedVersions {
|
30
|
+
# required bytes key = 1;
|
31
|
+
# repeated Versioned versions = 2;
|
32
|
+
# }
|
33
|
+
#
|
34
|
+
# message GetRequest {
|
35
|
+
# optional bytes key = 1;
|
36
|
+
# }
|
37
|
+
#
|
38
|
+
# message GetResponse {
|
39
|
+
# repeated Versioned versioned = 1;
|
40
|
+
# optional Error error = 2;
|
41
|
+
# }
|
42
|
+
#
|
43
|
+
# message GetVersionResponse {
|
44
|
+
# repeated VectorClock versions = 1;
|
45
|
+
# optional Error error = 2;
|
46
|
+
# }
|
47
|
+
#
|
48
|
+
# message GetAllRequest {
|
49
|
+
# repeated bytes keys = 1;
|
50
|
+
# }
|
51
|
+
#
|
52
|
+
# message GetAllResponse {
|
53
|
+
# repeated KeyedVersions values = 1;
|
54
|
+
# optional Error error = 2;
|
55
|
+
# }
|
56
|
+
#
|
57
|
+
# message PutRequest {
|
58
|
+
# required bytes key = 1;
|
59
|
+
# required Versioned versioned = 2;
|
60
|
+
# }
|
61
|
+
#
|
62
|
+
# message PutResponse {
|
63
|
+
# optional Error error = 1;
|
64
|
+
# }
|
65
|
+
#
|
66
|
+
# message DeleteRequest {
|
67
|
+
# required bytes key = 1;
|
68
|
+
# required VectorClock version = 2;
|
69
|
+
# }
|
70
|
+
#
|
71
|
+
# message DeleteResponse {
|
72
|
+
# required bool success = 1;
|
73
|
+
# optional Error error = 2;
|
74
|
+
# }
|
75
|
+
#
|
76
|
+
# enum RequestType {
|
77
|
+
# GET = 0;
|
78
|
+
# GET_ALL = 1;
|
79
|
+
# PUT = 2;
|
80
|
+
# DELETE = 3;
|
81
|
+
# GET_VERSION = 4;
|
82
|
+
# }
|
83
|
+
#
|
84
|
+
#
|
85
|
+
# message VoldemortRequest {
|
86
|
+
# required RequestType type = 1;
|
87
|
+
# required bool should_route = 2 [default = false];
|
88
|
+
# required string store = 3;
|
89
|
+
# optional GetRequest get = 4;
|
90
|
+
# optional GetAllRequest getAll = 5;
|
91
|
+
# optional PutRequest put = 6;
|
92
|
+
# optional DeleteRequest delete = 7;
|
93
|
+
# optional int32 requestRouteType = 8;
|
94
|
+
# }
|
95
|
+
require 'protobuf/message/message'
|
96
|
+
require 'protobuf/message/enum'
|
97
|
+
require 'protobuf/message/service'
|
98
|
+
require 'protobuf/message/extend'
|
99
|
+
|
100
|
+
module Voldemort
|
101
|
+
::Protobuf::OPTIONS[:"java_package"] = "voldemort.client.protocol.pb"
|
102
|
+
::Protobuf::OPTIONS[:"java_outer_classname"] = "VProto"
|
103
|
+
::Protobuf::OPTIONS[:"optimize_for"] = :SPEED
|
104
|
+
class ClockEntry < ::Protobuf::Message
|
105
|
+
defined_in __FILE__
|
106
|
+
required :int32, :node_id, 1
|
107
|
+
required :int64, :version, 2
|
108
|
+
end
|
109
|
+
class VectorClock < ::Protobuf::Message
|
110
|
+
defined_in __FILE__
|
111
|
+
repeated :ClockEntry, :entries, 1
|
112
|
+
optional :int64, :timestamp, 2
|
113
|
+
end
|
114
|
+
class Versioned < ::Protobuf::Message
|
115
|
+
defined_in __FILE__
|
116
|
+
required :bytes, :value, 1
|
117
|
+
required :VectorClock, :version, 2
|
118
|
+
end
|
119
|
+
class Error < ::Protobuf::Message
|
120
|
+
defined_in __FILE__
|
121
|
+
required :int32, :error_code, 1
|
122
|
+
required :string, :error_message, 2
|
123
|
+
end
|
124
|
+
class KeyedVersions < ::Protobuf::Message
|
125
|
+
defined_in __FILE__
|
126
|
+
required :bytes, :key, 1
|
127
|
+
repeated :Versioned, :versions, 2
|
128
|
+
end
|
129
|
+
class GetRequest < ::Protobuf::Message
|
130
|
+
defined_in __FILE__
|
131
|
+
optional :bytes, :key, 1
|
132
|
+
end
|
133
|
+
class GetResponse < ::Protobuf::Message
|
134
|
+
defined_in __FILE__
|
135
|
+
repeated :Versioned, :versioned, 1
|
136
|
+
optional :Error, :error, 2
|
137
|
+
end
|
138
|
+
class GetVersionResponse < ::Protobuf::Message
|
139
|
+
defined_in __FILE__
|
140
|
+
repeated :VectorClock, :versions, 1
|
141
|
+
optional :Error, :error, 2
|
142
|
+
end
|
143
|
+
class GetAllRequest < ::Protobuf::Message
|
144
|
+
defined_in __FILE__
|
145
|
+
repeated :bytes, :keys, 1
|
146
|
+
end
|
147
|
+
class GetAllResponse < ::Protobuf::Message
|
148
|
+
defined_in __FILE__
|
149
|
+
repeated :KeyedVersions, :values, 1
|
150
|
+
optional :Error, :error, 2
|
151
|
+
end
|
152
|
+
class PutRequest < ::Protobuf::Message
|
153
|
+
defined_in __FILE__
|
154
|
+
required :bytes, :key, 1
|
155
|
+
required :Versioned, :versioned, 2
|
156
|
+
end
|
157
|
+
class PutResponse < ::Protobuf::Message
|
158
|
+
defined_in __FILE__
|
159
|
+
optional :Error, :error, 1
|
160
|
+
end
|
161
|
+
class DeleteRequest < ::Protobuf::Message
|
162
|
+
defined_in __FILE__
|
163
|
+
required :bytes, :key, 1
|
164
|
+
required :VectorClock, :version, 2
|
165
|
+
end
|
166
|
+
class DeleteResponse < ::Protobuf::Message
|
167
|
+
defined_in __FILE__
|
168
|
+
required :bool, :success, 1
|
169
|
+
optional :Error, :error, 2
|
170
|
+
end
|
171
|
+
class RequestType < ::Protobuf::Enum
|
172
|
+
defined_in __FILE__
|
173
|
+
GET = 0
|
174
|
+
GET_ALL = 1
|
175
|
+
PUT = 2
|
176
|
+
DELETE = 3
|
177
|
+
GET_VERSION = 4
|
178
|
+
end
|
179
|
+
class VoldemortRequest < ::Protobuf::Message
|
180
|
+
defined_in __FILE__
|
181
|
+
required :RequestType, :type, 1
|
182
|
+
required :bool, :should_route, 2, :default => false
|
183
|
+
required :string, :store, 3
|
184
|
+
optional :GetRequest, :get, 4
|
185
|
+
optional :GetAllRequest, :getAll, 5
|
186
|
+
optional :PutRequest, :put, 6
|
187
|
+
optional :DeleteRequest, :delete, 7
|
188
|
+
optional :int32, :requestRouteType, 8
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
package voldemort;
|
2
|
+
|
3
|
+
option java_package = "voldemort.client.protocol.pb";
|
4
|
+
option java_outer_classname = "VProto";
|
5
|
+
option optimize_for = SPEED;
|
6
|
+
|
7
|
+
message ClockEntry {
|
8
|
+
required int32 node_id = 1;
|
9
|
+
required int64 version = 2;
|
10
|
+
}
|
11
|
+
|
12
|
+
message VectorClock {
|
13
|
+
repeated ClockEntry entries = 1;
|
14
|
+
optional int64 timestamp = 2;
|
15
|
+
}
|
16
|
+
|
17
|
+
message Versioned {
|
18
|
+
required bytes value = 1;
|
19
|
+
required VectorClock version = 2;
|
20
|
+
}
|
21
|
+
|
22
|
+
message Error {
|
23
|
+
required int32 error_code = 1;
|
24
|
+
required string error_message = 2;
|
25
|
+
}
|
26
|
+
|
27
|
+
message KeyedVersions {
|
28
|
+
required bytes key = 1;
|
29
|
+
repeated Versioned versions = 2;
|
30
|
+
}
|
31
|
+
|
32
|
+
message GetRequest {
|
33
|
+
optional bytes key = 1;
|
34
|
+
}
|
35
|
+
|
36
|
+
message GetResponse {
|
37
|
+
repeated Versioned versioned = 1;
|
38
|
+
optional Error error = 2;
|
39
|
+
}
|
40
|
+
|
41
|
+
message GetVersionResponse {
|
42
|
+
repeated VectorClock versions = 1;
|
43
|
+
optional Error error = 2;
|
44
|
+
}
|
45
|
+
|
46
|
+
message GetAllRequest {
|
47
|
+
repeated bytes keys = 1;
|
48
|
+
}
|
49
|
+
|
50
|
+
message GetAllResponse {
|
51
|
+
repeated KeyedVersions values = 1;
|
52
|
+
optional Error error = 2;
|
53
|
+
}
|
54
|
+
|
55
|
+
message PutRequest {
|
56
|
+
required bytes key = 1;
|
57
|
+
required Versioned versioned = 2;
|
58
|
+
}
|
59
|
+
|
60
|
+
message PutResponse {
|
61
|
+
optional Error error = 1;
|
62
|
+
}
|
63
|
+
|
64
|
+
message DeleteRequest {
|
65
|
+
required bytes key = 1;
|
66
|
+
required VectorClock version = 2;
|
67
|
+
}
|
68
|
+
|
69
|
+
message DeleteResponse {
|
70
|
+
required bool success = 1;
|
71
|
+
optional Error error = 2;
|
72
|
+
}
|
73
|
+
|
74
|
+
enum RequestType {
|
75
|
+
GET = 0;
|
76
|
+
GET_ALL = 1;
|
77
|
+
PUT = 2;
|
78
|
+
DELETE = 3;
|
79
|
+
GET_VERSION = 4;
|
80
|
+
}
|
81
|
+
|
82
|
+
|
83
|
+
message VoldemortRequest {
|
84
|
+
required RequestType type = 1;
|
85
|
+
required bool should_route = 2 [default = false];
|
86
|
+
required string store = 3;
|
87
|
+
optional GetRequest get = 4;
|
88
|
+
optional GetAllRequest getAll = 5;
|
89
|
+
optional PutRequest put = 6;
|
90
|
+
optional DeleteRequest delete = 7;
|
91
|
+
optional int32 requestRouteType = 8;
|
92
|
+
}
|
data/lib/voldemort-rb.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "connection", "voldemort_node")
|
2
|
+
require File.join(File.dirname(__FILE__), "connection", "connection")
|
3
|
+
require File.join(File.dirname(__FILE__), "connection", "tcp_connection")
|
4
|
+
|
5
|
+
class VoldemortClient
|
6
|
+
attr_accessor :connection
|
7
|
+
attr_accessor :conflict_resolver
|
8
|
+
|
9
|
+
def initialize(db_name, *hosts, &block)
|
10
|
+
self.conflict_resolver = block unless !block
|
11
|
+
self.connection = TCPConnection.new(db_name, hosts) # implement and modifiy if you don't want to use TCP protobuf.
|
12
|
+
self.connection.bootstrap
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(key)
|
16
|
+
versions = self.connection.get(key)
|
17
|
+
version = self.resolve_conflicts(versions.versioned)
|
18
|
+
if version
|
19
|
+
version.value
|
20
|
+
else
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_all(keys)
|
26
|
+
all_version = self.connection.get_all(keys)
|
27
|
+
values = {}
|
28
|
+
all_version.values.collect do |v|
|
29
|
+
values[v.key] = self.resolve_conflicts(v.versions).value
|
30
|
+
end
|
31
|
+
values
|
32
|
+
end
|
33
|
+
|
34
|
+
def put(key, value, version = nil)
|
35
|
+
self.connection.put(key, value)
|
36
|
+
end
|
37
|
+
|
38
|
+
def delete(key)
|
39
|
+
self.connection.delete(key)
|
40
|
+
end
|
41
|
+
|
42
|
+
def resolve_conflicts(versions)
|
43
|
+
return self.conflict_resolver.call(versions) if self.conflict_resolver
|
44
|
+
# by default just return the version that has the most recent timestamp.
|
45
|
+
versions.max { |a, b| a.version.timestamp <=> b.version.timestamp }
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Connection do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@connection = Connection.new("test", "localhost:6666")
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "default methods" do
|
10
|
+
|
11
|
+
it "should support connect" do
|
12
|
+
@connection.should respond_to(:connect)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should support reconnect" do
|
16
|
+
@connection.should respond_to(:reconnect)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should support disconnect" do
|
20
|
+
@connection.should respond_to(:disconnect)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should parse nodes from xml" do
|
24
|
+
@connection.should respond_to(:parse_nodes_from)
|
25
|
+
xml = "<cluster>\r\n <name>mycluster</name>\r\n <server>\r\n <id>0</id>\r\n <host>localhost</host>\r\n <http-port>8081</http-port>\r\n <socket-port>6666</socket-port>\r\n <admin-port>6667</admin-port>\r\n <partitions>0, 1</partitions>\r\n </server>\r\n</cluster>"
|
26
|
+
nodes = @connection.parse_nodes_from(xml)
|
27
|
+
nodes.first.host.should eql("localhost")
|
28
|
+
nodes.first.port.should eql("6666")
|
29
|
+
nodes.length.should eql(1)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should tell to wich node is connected to" do
|
33
|
+
@connection.should respond_to(:connected_node)
|
34
|
+
node = mock(VoldemortNode)
|
35
|
+
node.stub!(:host).and_return("localhost")
|
36
|
+
node.stub!(:port).and_return(6666)
|
37
|
+
@connection.nodes.stub!(:sort_by).and_return([node])
|
38
|
+
@connection.stub!(:connect_to).and_return(true)
|
39
|
+
@connection.connect_to_random_node
|
40
|
+
@connection.connected_node.should eql(node)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should use protobuf by default" do
|
44
|
+
@connection.protocol_version.should eql("pb0")
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should use the hosts specified" do
|
48
|
+
connection = Connection.new("test", "localhost:6666")
|
49
|
+
connection.hosts.should eql("localhost:6666")
|
50
|
+
connection.nodes.length.should eql(1)
|
51
|
+
connection2 = Connection.new("test", ["localhost:6666", "localhost:7777"])
|
52
|
+
connection2.hosts.should eql(["localhost:6666", "localhost:7777"])
|
53
|
+
connection2.nodes.length.should eql(2)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "rebalance nodes by evaluating number of requests" do
|
58
|
+
|
59
|
+
it "should have a request_count and request_limit_per_node per node connection" do
|
60
|
+
@connection.should respond_to(:request_count)
|
61
|
+
@connection.should respond_to(:request_limit_per_node)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should tell if the request limit per node was reached" do
|
65
|
+
@connection.request_count = 0
|
66
|
+
@connection.request_limit_per_node = 10
|
67
|
+
@connection.rebalance_connection?.should eql(false)
|
68
|
+
@connection.request_count = 11
|
69
|
+
@connection.request_limit_per_node = 10
|
70
|
+
@connection.rebalance_connection?.should eql(true)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should reconnect every N number of requests" do
|
74
|
+
@connection.should_receive(:rebalance_connection?).and_return(true)
|
75
|
+
@connection.should_receive(:reconnect).and_return(true)
|
76
|
+
@connection.rebalance_connection_if_needed
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should not reconnect if it haven't reached the limit of requests" do
|
80
|
+
@connection.should_receive(:rebalance_connection?).and_return(false)
|
81
|
+
@connection.should_not_receive(:reconnect).and_return(false)
|
82
|
+
@connection.rebalance_connection_if_needed
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should rebalance if needed when calling get, get_all or put" do
|
86
|
+
@connection.should_receive(:rebalance_connection_if_needed).exactly(3).times.and_return(true)
|
87
|
+
@connection.stub!(:get_from).and_return(true)
|
88
|
+
@connection.stub!(:get_all_from).and_return(true)
|
89
|
+
@connection.stub!(:put_from).and_return(true)
|
90
|
+
@connection.stub!(:delete_from).and_return(true)
|
91
|
+
@connection.get("value")
|
92
|
+
@connection.put("value", "value")
|
93
|
+
@connection.get_all(["key1", "key2"])
|
94
|
+
@connection.delete("key")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe TCPConnection do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@connection = TCPConnection.new("test", "localhost:6666")
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "connection mechanism" do
|
10
|
+
|
11
|
+
it "should connect to a specified host" do
|
12
|
+
@connection.should respond_to(:connect_to)
|
13
|
+
mock_socket = mock(TCPSocket)
|
14
|
+
TCPSocket.should_receive(:open).and_return(mock_socket)
|
15
|
+
@connection.should_receive(:send_protocol_version).and_return(true)
|
16
|
+
@connection.should_receive(:protocol_handshake_ok?).and_return(true)
|
17
|
+
@connection.connect_to("localhost", 6666).should eql(mock_socket)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should send the protocol" do
|
21
|
+
@connection.should respond_to(:send_protocol_version)
|
22
|
+
mock_socket = mock(TCPSocket)
|
23
|
+
@connection.stub!(:socket).and_return(mock_socket)
|
24
|
+
mock_socket.should_receive(:write).with(Connection::PROTOCOL).and_return(true)
|
25
|
+
@connection.send_protocol_version.should eql(true)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should receive the protocol handshake response" do
|
29
|
+
@connection.should respond_to(:protocol_handshake_ok?)
|
30
|
+
mock_socket = mock(TCPSocket)
|
31
|
+
@connection.stub!(:socket).and_return(mock_socket)
|
32
|
+
mock_socket.should_receive(:recv).with(2).and_return(Connection::STATUS_OK)
|
33
|
+
@connection.protocol_handshake_ok?.should eql(true)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should have a socket" do
|
37
|
+
@connection.should respond_to(:socket)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'lib/protos/voldemort-client.pb'
|
3
|
+
|
4
|
+
include Voldemort
|
5
|
+
|
6
|
+
describe VoldemortClient do
|
7
|
+
|
8
|
+
before(:each) do
|
9
|
+
connection = mock(TCPConnection)
|
10
|
+
node = mock(VoldemortNode)
|
11
|
+
connection.stub!(:bootstrap).and_return(node)
|
12
|
+
TCPConnection.stub!(:new).and_return(connection)
|
13
|
+
@client = VoldemortClient.new("test", "localhost:6666")
|
14
|
+
@client.stub!(:connection).and_return(connection)
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "connection abstraction" do
|
18
|
+
it "should have a connection" do
|
19
|
+
@client.should respond_to(:connection)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should initialize the connection" do
|
23
|
+
@client.connection.should_not be(nil)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "default methods" do
|
28
|
+
|
29
|
+
it "should support get" do
|
30
|
+
@client.should respond_to(:get)
|
31
|
+
version = mock(Versioned)
|
32
|
+
v = mock(VectorClock)
|
33
|
+
v.stub!(:value).and_return("some value")
|
34
|
+
version.stub!(:versioned).and_return([v])
|
35
|
+
@client.connection.should_receive(:get).with("key").and_return(version)
|
36
|
+
@client.get("key").should eql("some value")
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should support get all" do
|
40
|
+
@client.should respond_to(:get_all)
|
41
|
+
version = mock(Versioned)
|
42
|
+
v = mock(VectorClock)
|
43
|
+
v.stub!(:value).and_return("some value")
|
44
|
+
v.stub!(:key).and_return("key")
|
45
|
+
v.stub!(:versions).and_return([v])
|
46
|
+
version.stub!(:values).and_return([v])
|
47
|
+
@client.connection.should_receive(:get_all).with(["key", "key2"]).and_return(version)
|
48
|
+
@client.get_all(["key", "key2"]).should eql({ "key" => "some value" }) # we pretend key2 doesn't exist
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should support put" do
|
52
|
+
@client.should respond_to(:put)
|
53
|
+
@client.connection.should_receive(:put).with("key", "value").and_return("version")
|
54
|
+
@client.put("key", "value").should eql("version")
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should support delete" do
|
58
|
+
@client.should respond_to(:delete)
|
59
|
+
@client.connection.should_receive(:delete).with("key").and_return(true)
|
60
|
+
@client.delete("key").should eql(true)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "default resolver" do
|
65
|
+
|
66
|
+
before(:each) do
|
67
|
+
@old_versioned = Versioned.new
|
68
|
+
@old_versioned.value = "old value"
|
69
|
+
@old_versioned.version = VectorClock.new
|
70
|
+
@old_versioned.version.timestamp = (Time.now-86400).to_i * 1000
|
71
|
+
|
72
|
+
@new_versioned = Versioned.new
|
73
|
+
@new_versioned.value = "new value"
|
74
|
+
@new_versioned.version = VectorClock.new
|
75
|
+
@new_versioned.version.timestamp = (Time.now).to_i * 1000
|
76
|
+
|
77
|
+
@versions = []
|
78
|
+
@versions << @old_versioned
|
79
|
+
@versions << @new_versioned
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should have a default resolver" do
|
83
|
+
@client.should respond_to(:conflict_resolver)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should pick a default version form a list of versions, and should be the most recent value" do
|
87
|
+
@client.resolve_conflicts(@versions).should eql(@new_versioned)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should allow a custom conflict resolver" do
|
91
|
+
@client = VoldemortClient.new("test", "localhost:6666") do |versions|
|
92
|
+
versions.first # just return the first version
|
93
|
+
end
|
94
|
+
@client.resolve_conflicts(@versions).should eql(@old_versioned)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe VoldemortNode do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@voldemort_node = VoldemortNode.new
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "default methods" do
|
10
|
+
|
11
|
+
it "should have id, host, port, http_port, admin_port and partitions" do
|
12
|
+
[:id, :host, :port, :http_port, :admin_port, :partitions].each do |m|
|
13
|
+
@voldemort_node.should respond_to(m)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: voldemort-rb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
version: "0.1"
|
9
|
+
platform: ruby
|
10
|
+
authors:
|
11
|
+
- Alejandro Crosa
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
|
16
|
+
date: 2010-03-05 00:00:00 -08:00
|
17
|
+
default_executable:
|
18
|
+
dependencies: []
|
19
|
+
|
20
|
+
description: voldemort-rb allows you to connect to the Voldemort descentralized key value store.
|
21
|
+
email:
|
22
|
+
- alejandrocrosa@gmail.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- CHANGELOG
|
31
|
+
- MIT-LICENSE
|
32
|
+
- README.md
|
33
|
+
- Rakefile
|
34
|
+
- lib/voldemort-rb.rb
|
35
|
+
- lib/connection/connection.rb
|
36
|
+
- lib/connection/tcp_connection.rb
|
37
|
+
- lib/connection/voldemort_node.rb
|
38
|
+
- lib/protos/voldemort-client.pb.rb
|
39
|
+
- lib/protos/voldemort-client.proto
|
40
|
+
- spec/connection_spec.rb
|
41
|
+
- spec/tcp_connection_spec.rb
|
42
|
+
- spec/voldemort_node_spec.rb
|
43
|
+
- spec/voldemort_client_spec.rb
|
44
|
+
- spec/spec_helper.rb
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: http://github.com/acrosa/voldemort-rb
|
47
|
+
licenses: []
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
segments:
|
66
|
+
- 0
|
67
|
+
version: "0"
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.3.6
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: A Ruby client for the Voldemort distributed key value store
|
75
|
+
test_files: []
|
76
|
+
|