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 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,3 @@
1
+ class VoldemortNode
2
+ attr_accessor :id, :host, :port, :http_port, :admin_port, :partitions
3
+ 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
+ }
@@ -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
@@ -0,0 +1,2 @@
1
+ require 'rubygems'
2
+ require 'voldemort_client'
@@ -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
+