voldemort-rb 0.1
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/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
|
+
|