orientdb4r 0.2.10 → 0.3.0

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/README.rdoc CHANGED
@@ -63,9 +63,7 @@ see Wiki page for more sample at https://github.com/veny/orientdb4r/wiki
63
63
 
64
64
  === Important Upgrade Notice
65
65
 
66
- * 2012-06-19 [v0.2.1]: Client#create_class - parameter given to a block has new method 'link' to define a linked property
67
- * 2012-06-17 [v0.2.0]: Client#get_class raises Orientdb4r::NotFoundError instead of ArgumentError
68
-
66
+ * see changelog.txt
69
67
 
70
68
 
71
69
  == FEATURES/PROBLEMS
data/changelog.txt CHANGED
@@ -1,20 +1,19 @@
1
- 0.3.0 07/x/12
2
- ===============
3
- - 'rest-client' replaced by 'excon' for HTTP communication with Keep-Alive
4
- - introduced support for distributed server
1
+ 0.3.0 2012-08-01
2
+ - introduced support for cluster of distributed servers
3
+ - initial strategies for load balancing: sequence, round robin
4
+ - Keep-Alive feature: Excon HTTP library is fully working
5
5
 
6
- 0.2.10 /07/07/21
6
+ 0.2.10 2012-07-21
7
7
  - experimental support for Excon HTTP library with Keep-Alive connection
8
8
 
9
- 0.2.9 /07/07/18
9
+ 0.2.9 2012-07-18
10
10
  - introduced class Rid
11
11
  - added feature Client#delete_database
12
12
 
13
-
14
- 0.2.8 /07/07/16
13
+ 0.2.8 2012-07-16
15
14
  - changed and stabilized exception handling
16
15
  - added feature Client#create_class(:properties)
17
16
 
18
- 0.2.7 07/07/12
17
+ 0.2.7 2012-07-07
19
18
  - changed design to support distributed server
20
19
  - added method Client#class_exists?
@@ -20,7 +20,7 @@ class FlatClassPerf < FStudy::Case
20
20
  client.command 'DELETE FROM User'
21
21
  end
22
22
  def data
23
- 1.upto(10) do |i|
23
+ 1.upto(10000) do |i|
24
24
  Orientdb4r::logger.info "...done: #{i}" if 0 == (i % 1000)
25
25
  first_name = dg.word
26
26
  surname = dg.word
@@ -9,7 +9,10 @@ module Orientdb4r
9
9
  # # Regexp to validate format of providet version.
10
10
  SERVER_VERSION_PATTERN = /^\d+\.\d+\.\d+/
11
11
 
12
+ attr_reader :user, :password, :database
12
13
  attr_reader :server_version
14
+ attr_reader :nodes, :connection_library
15
+ attr_reader :load_balancing, :lb_strategy
13
16
 
14
17
  ###
15
18
  # Constructor.
@@ -181,7 +184,7 @@ module Orientdb4r
181
184
  opt_pattern = { :mode => :nil }
182
185
  verify_options(options, opt_pattern)
183
186
  if :strict == options[:mode]
184
- response = a_node.request(:method => :get, :uri => "connect/#{@database}") # TODO there cannot be REST
187
+ response = call_server(:method => :get, :uri => "connect/#{@database}") # TODO there cannot be REST
185
188
  connect_info = process_response response
186
189
  children = connect_info['classes'].select { |i| i['superClass'] == name }
187
190
  unless children.empty?
@@ -256,8 +259,35 @@ module Orientdb4r
256
259
 
257
260
  protected
258
261
 
259
- def a_node
260
- @nodes[0]
262
+ ###
263
+ # Calls the server with a specific task.
264
+ # Returns a response according to communication channel (e.g. HTTP response).
265
+ def call_server(options)
266
+ lb_all_bad_msg = 'all nodes failed to communicate with server!'
267
+ response = nil
268
+
269
+ # credentials if not defined explicitly
270
+ options[:user] = user unless options.include? :user
271
+ options[:password] = password unless options.include? :password
272
+
273
+ idx = lb_strategy.node_index
274
+ raise OrientdbError, lb_all_bad_msg if idx.nil? # no good node found
275
+
276
+ begin
277
+ node = @nodes[idx]
278
+ begin
279
+ response = node.request options
280
+ lb_strategy.good_one idx
281
+ return response
282
+
283
+ rescue NodeError => e
284
+ Orientdb4r::logger.error "node error, index=#{idx}, msg=#{e.message}, #{node}"
285
+ lb_strategy.bad_one idx
286
+ idx = lb_strategy.node_index
287
+ end
288
+ end until idx.nil? and response.nil? # both 'nil' <= we tried all nodes and all with problem
289
+
290
+ raise OrientdbError, lb_all_bad_msg
261
291
  end
262
292
 
263
293
 
@@ -267,6 +297,7 @@ module Orientdb4r
267
297
  raise ConnectionError, 'not connected' unless @connected
268
298
  end
269
299
 
300
+
270
301
  ###
271
302
  # Around advice to meassure and print the method time.
272
303
  def time_around(&block)
@@ -0,0 +1,89 @@
1
+ module Orientdb4r
2
+
3
+ ###
4
+ # Base class for implementation of load balancing strategy.
5
+ class LBStrategy
6
+
7
+ # If occures a new try to communicate from node can be tested.
8
+ RECOVERY_TIMEOUT = 30
9
+
10
+ attr_reader :nodes_count, :bad_nodes
11
+
12
+ ###
13
+ # Constructor.
14
+ def initialize nodes_count
15
+ @nodes_count = nodes_count
16
+ @bad_nodes = {}
17
+ end
18
+
19
+ ###
20
+ # Gets index of node to be used for next request
21
+ # or 'nil' if there is no one next.
22
+ def node_index
23
+ raise NotImplementedError, 'this should be overridden in subclass'
24
+ end
25
+
26
+ ###
27
+ # Marks an index as good that means it can be used for next server calls.
28
+ def good_one(idx)
29
+ @bad_nodes.delete idx
30
+ end
31
+
32
+ ###
33
+ # Marks an index as bad that means it will be not used until:
34
+ # * there is other 'good' node
35
+ # * timeout
36
+ def bad_one(idx)
37
+ @bad_nodes[idx] = Time.now
38
+ end
39
+
40
+ protected
41
+
42
+ def search_next_good(bad_idx)
43
+ Orientdb4r::logger.warn "identified bad node, idx=#{bad_idx}, age=#{Time.now - @bad_nodes[bad_idx]} [s]"
44
+ 1.upto(nodes_count) do |i|
45
+ candidate = (i + bad_idx) % nodes_count
46
+ unless @bad_nodes.include? candidate
47
+ Orientdb4r::logger.debug "found good node, idx=#{candidate}"
48
+ return candidate
49
+ end
50
+ end
51
+
52
+ # TODO implement search based on LRU for next round
53
+
54
+ Orientdb4r::logger.error 'no nodes more, all invalid'
55
+ nil
56
+ end
57
+
58
+ end
59
+
60
+ ###
61
+ # Implementation of Sequence strategy.
62
+ # Assigns work in the order of nodes defined by the client initialization.
63
+ class Sequence < LBStrategy
64
+
65
+ def node_index #:nodoc:
66
+ @last_index = 0 if @last_index.nil?
67
+
68
+ @last_index = search_next_good(@last_index) if @bad_nodes.include? @last_index
69
+ @last_index
70
+ end
71
+
72
+ end
73
+
74
+ ###
75
+ # Implementation of Round Robin strategy.
76
+ # Assigns work in round-robin order per nodes defined by the client initialization.
77
+ class RoundRobin < LBStrategy
78
+
79
+ def node_index #:nodoc:
80
+ @last_index = -1 if @last_index.nil?
81
+
82
+ @last_index = (@last_index + 1) % nodes_count
83
+ @last_index = search_next_good(@last_index) if @bad_nodes.include? @last_index
84
+ @last_index
85
+ end
86
+
87
+ end
88
+
89
+ end
@@ -7,6 +7,7 @@ module Orientdb4r
7
7
  include Utils
8
8
 
9
9
  attr_reader :host, :port # they are immutable
10
+ attr_reader :session_id
10
11
 
11
12
  ###
12
13
  # Constructor.
@@ -21,7 +22,7 @@ module Orientdb4r
21
22
  ###
22
23
  # Cleans up resources used by the node.
23
24
  def cleanup
24
- raise NotImplementedError, 'this should be overridden by subclass'
25
+ @session_id = nil
25
26
  end
26
27
 
27
28
 
@@ -31,6 +32,11 @@ module Orientdb4r
31
32
  raise NotImplementedError, 'this should be overridden by subclass'
32
33
  end
33
34
 
35
+
36
+ def to_s #:nodoc:
37
+ "Node(host=#{host},port=#{port})"
38
+ end
39
+
34
40
  end
35
41
 
36
42
  end
@@ -9,35 +9,45 @@ module Orientdb4r
9
9
  before [:create_document, :get_document, :update_document, :delete_document], :assert_connected
10
10
  around [:query, :command], :time_around
11
11
 
12
- attr_reader :user, :password, :database, :http_lib
13
-
14
12
 
15
13
  def initialize(options) #:nodoc:
16
14
  super()
17
15
  options_pattern = { :host => 'localhost', :port => 2480, :ssl => false,
18
- :nodes => :optional,
19
- :connection_library => :restclient}
16
+ :nodes => :optional, :load_balancing => :sequence,
17
+ :connection_library => Orientdb4r::connection_library}
20
18
  verify_and_sanitize_options(options, options_pattern)
21
19
 
22
20
  # fake nodes for single server
23
- unless options[:nodes].nil?
21
+ if options[:nodes].nil?
24
22
  options[:nodes] = [{:host => options[:host], :port => options[:port], :ssl => options[:ssl]}]
25
23
  end
26
24
  raise ArgumentError, 'nodes has to be arrray' unless options[:nodes].is_a? Array
27
25
 
28
- node_clazz = case options[:connection_library]
26
+ # instantiate nodes accroding to HTTP library
27
+ @connection_library = options[:connection_library]
28
+ node_clazz = case connection_library
29
29
  when :restclient then Orientdb4r::RestClientNode
30
30
  when :excon then Orientdb4r::ExconNode
31
- else raise ArgumentError, "unknown connection library: #{options[:connection_library]}"
31
+ else raise ArgumentError, "unknown connection library: #{connection_library}"
32
32
  end
33
- @http_lib = options[:connection_library]
34
33
 
34
+ # nodes
35
35
  options[:nodes].each do |node_options|
36
- @nodes << node_clazz.new(options[:host], options[:port], options[:ssl])
37
36
  verify_and_sanitize_options(node_options, options_pattern)
37
+ @nodes << node_clazz.new(node_options[:host], node_options[:port], node_options[:ssl])
38
+ end
39
+
40
+ # load balancing
41
+ @load_balancing = options[:load_balancing]
42
+ @lb_strategy = case load_balancing
43
+ when :sequence then Orientdb4r::Sequence.new nodes.size
44
+ when :round_robin then Orientdb4r::RoundRobin.new nodes.size
45
+ else raise ArgumentError, "unknow load balancing type: #{load_balancing}"
38
46
  end
39
47
 
40
- Orientdb4r::logger.info "client initialized, #{@nodes.size} node(s) with connection library = #{options[:connection_library]}"
48
+
49
+ Orientdb4r::logger.info "client initialized with #{@nodes.size} node(s) "
50
+ Orientdb4r::logger.info "connection_library=#{options[:connection_library]}, load_balancing=#{load_balancing}"
41
51
  end
42
52
 
43
53
 
@@ -50,9 +60,8 @@ module Orientdb4r
50
60
  @user = options[:user]
51
61
  @password = options[:password]
52
62
 
53
- node = a_node
54
63
  begin
55
- response = node.oo_request(:method => :get, :uri => "connect/#{@database}", :user => user, :password => password)
64
+ response = call_server(:method => :get, :uri => "connect/#{@database}")
56
65
  rescue
57
66
  @connected = false
58
67
  @server_version = nil
@@ -63,7 +72,6 @@ module Orientdb4r
63
72
  raise ConnectionError
64
73
  end
65
74
  rslt = process_response response
66
- node.post_connect(user, password, response)
67
75
  decorate_classes_with_model(rslt['classes'])
68
76
 
69
77
  # try to read server version
@@ -77,7 +85,7 @@ module Orientdb4r
77
85
  @server_version = DEFAULT_SERVER_VERSION
78
86
  end
79
87
 
80
- Orientdb4r::logger.debug "successfully connected to server, version=#{server_version}, session=#{node.session_id}"
88
+ Orientdb4r::logger.debug "successfully connected to server, version=#{server_version}"
81
89
  @connected = true
82
90
  rslt
83
91
  end
@@ -87,7 +95,7 @@ module Orientdb4r
87
95
  return unless @connected
88
96
 
89
97
  begin
90
- a_node.request(:method => :get, :uri => 'disconnect')
98
+ call_server(:method => :get, :uri => 'disconnect')
91
99
  # https://groups.google.com/forum/?fromgroups#!topic/orient-database/5MAMCvFavTc
92
100
  # Disconnect doesn't require you're authenticated.
93
101
  # It always returns 401 because some browsers intercept this and avoid to reuse the same session again.
@@ -107,11 +115,8 @@ module Orientdb4r
107
115
  options_pattern = { :user => :optional, :password => :optional }
108
116
  verify_options(options, options_pattern)
109
117
 
110
- u = options.include?(:user) ? options[:user] : user
111
- p = options.include?(:password) ? options[:password] : password
112
-
113
- # uses one-off request because of additional authentication to the server
114
- response = a_node.oo_request :method => :get, :user => u, :password => p, :uri => 'server'
118
+ # additional authentication allowed, overriden in 'call_server' if not defined
119
+ response = call_server :method => :get, :uri => 'server'
115
120
  process_response(response)
116
121
  end
117
122
 
@@ -125,34 +130,31 @@ module Orientdb4r
125
130
  }
126
131
  verify_and_sanitize_options(options, options_pattern)
127
132
 
128
- u = options.include?(:user) ? options[:user] : user
129
- p = options.include?(:password) ? options[:password] : password
130
-
131
- # uses one-off request because of additional authentication to the server
132
- response = a_node.oo_request :method => :post, :user => u, :password => p, \
133
- :uri => "database/#{options[:database]}/#{options[:type]}"
133
+ # additional authentication allowed, overriden in 'call_server' if not defined
134
+ response = call_server_one_off :method => :post, :uri => "database/#{options[:database]}/#{options[:type]}"
134
135
  process_response(response)
135
136
  end
136
137
 
137
138
 
139
+ #> curl --user admin:admin http://localhost:2480/database/temp
138
140
  def get_database(options=nil) #:nodoc:
139
141
  raise ArgumentError, 'options have to be a Hash' if !options.nil? and !options.kind_of? Hash
140
142
 
141
143
  if options.nil?
142
- # use values from connect
144
+ # use database from connect
143
145
  raise ConnectionError, 'client has to be connected if no params' unless connected?
144
- options = { :database => database, :user => user, :password => password }
146
+ options = { :database => database }
145
147
  end
146
148
 
147
149
  options_pattern = { :database => :mandatory, :user => :optional, :password => :optional }
148
150
  verify_options(options, options_pattern)
149
151
 
150
- u = options.include?(:user) ? options[:user] : user
151
- p = options.include?(:password) ? options[:password] : password
152
+ # additional authentication allowed, overriden in 'call_server' if not defined
153
+ params = {:method => :get, :uri => "database/#{options[:database]}"}
154
+ params[:user] = options[:user] if options.include? :user
155
+ params[:password] = options[:password] if options.include? :password
152
156
 
153
- # uses one-off request because of additional authentication to the server
154
- response = a_node.oo_request :method => :get, :user => u, :password => p, \
155
- :uri => "database/#{options[:database]}"
157
+ response = call_server params
156
158
 
157
159
  # NotFoundError cannot be raised - no way how to recognize from 401 bad auth
158
160
  process_response(response)
@@ -165,12 +167,8 @@ module Orientdb4r
165
167
  }
166
168
  verify_and_sanitize_options(options, options_pattern)
167
169
 
168
- u = options.include?(:user) ? options[:user] : user
169
- p = options.include?(:password) ? options[:password] : password
170
-
171
- # uses one-off request because of additional authentication to the server
172
- response = a_node.oo_request :method => :delete, :user => u, :password => p, \
173
- :uri => "database/#{options[:database]}"
170
+ # additional authentication allowed, overriden in 'call_server' if not defined
171
+ response = call_server_one_off :method => :delete, :uri => "database/#{options[:database]}"
174
172
  process_response(response)
175
173
  end
176
174
 
@@ -186,7 +184,7 @@ module Orientdb4r
186
184
  limit = ''
187
185
  limit = "/#{options[:limit]}" if !options.nil? and options.include?(:limit)
188
186
 
189
- response = a_node.request(:method => :get, :uri => "query/#{@database}/sql/#{CGI::escape(sql)}#{limit}")
187
+ response = call_server(:method => :get, :uri => "query/#{@database}/sql/#{CGI::escape(sql)}#{limit}")
190
188
  entries = process_response(response) do
191
189
  raise NotFoundError, 'record not found' if response.body =~ /ORecordNotFoundException/
192
190
  end
@@ -200,7 +198,7 @@ module Orientdb4r
200
198
 
201
199
  def command(sql) #:nodoc:
202
200
  raise ArgumentError, 'command is blank' if blank? sql
203
- response = a_node.request(:method => :post, :uri => "command/#{@database}/sql/#{CGI::escape(sql)}")
201
+ response = call_server(:method => :post, :uri => "command/#{@database}/sql/#{CGI::escape(sql)}")
204
202
  process_response(response)
205
203
  end
206
204
 
@@ -211,7 +209,7 @@ module Orientdb4r
211
209
  raise ArgumentError, "class name is blank" if blank?(name)
212
210
 
213
211
  if compare_versions(server_version, '1.1.0') >= 0
214
- response = a_node.request(:method => :get, :uri => "class/#{@database}/#{name}")
212
+ response = call_server(:method => :get, :uri => "class/#{@database}/#{name}")
215
213
  rslt = process_response(response) do
216
214
  raise NotFoundError, 'class not found' if response.body =~ /Invalid class/
217
215
  end
@@ -220,7 +218,7 @@ module Orientdb4r
220
218
  else
221
219
  # there is bug in REST API [v1.0.0, fixed in r5902], only data are returned
222
220
  # workaround - use metadate delivered by 'connect'
223
- response = a_node.request(:method => :get, :uri => "connect/#{@database}")
221
+ response = call_server(:method => :get, :uri => "connect/#{@database}")
224
222
  connect_info = process_response(response) do
225
223
  raise NotFoundError, 'class not found' if response.body =~ /Invalid class/
226
224
  end
@@ -247,7 +245,7 @@ module Orientdb4r
247
245
  # ----------------------------------------------------------------- DOCUMENT
248
246
 
249
247
  def create_document(doc) #:nodoc:
250
- response = a_node.request(:method => :post, :uri => "document/#{@database}", \
248
+ response = call_server(:method => :post, :uri => "document/#{@database}", \
251
249
  :content_type => 'application/json', :data => doc.to_json)
252
250
  srid = process_response(response) do
253
251
  raise DataError, 'validation problem' if response.body =~ /OValidationException/
@@ -259,7 +257,7 @@ module Orientdb4r
259
257
 
260
258
  def get_document(rid) #:nodoc:
261
259
  rid = Rid.new(rid) unless rid.is_a? Rid
262
- response = a_node.request(:method => :get, :uri => "document/#{@database}/#{rid.unprefixed}")
260
+ response = call_server(:method => :get, :uri => "document/#{@database}/#{rid.unprefixed}")
263
261
  rslt = process_response(response) do
264
262
  raise NotFoundError, 'record not found' if response.body =~ /ORecordNotFoundException/
265
263
  raise NotFoundError, 'record not found' if response.body =~ /Record with id .* was not found/ # why after delete?
@@ -278,7 +276,7 @@ module Orientdb4r
278
276
  rid = doc.doc_rid
279
277
  doc.delete '@rid' # will be not updated
280
278
 
281
- response = a_node.request(:method => :put, :uri => "document/#{@database}/#{rid.unprefixed}", \
279
+ response = call_server(:method => :put, :uri => "document/#{@database}/#{rid.unprefixed}", \
282
280
  :content_type => 'application/json', :data => doc.to_json)
283
281
  process_response(response) do
284
282
  raise DataError, 'concurrent modification' if response.body =~ /OConcurrentModificationException/
@@ -291,13 +289,14 @@ module Orientdb4r
291
289
  def delete_document(rid) #:nodoc:
292
290
  rid = Rid.new(rid) unless rid.is_a? Rid
293
291
 
294
- response = a_node.request(:method => :delete, :uri => "document/#{@database}/#{rid.unprefixed}")
292
+ response = call_server(:method => :delete, :uri => "document/#{@database}/#{rid.unprefixed}")
295
293
  process_response(response) do
296
294
  raise NotFoundError, 'record not found' if response.body =~ /ORecordNotFoundException/
297
295
  end
298
296
  # empty http response
299
297
  end
300
298
 
299
+
301
300
  # ------------------------------------------------------------------ Helpers
302
301
 
303
302
  private
@@ -321,8 +320,8 @@ module Orientdb4r
321
320
  raise OrientdbError, "unexpected return code, code=#{response.code}, body=#{compose_error_message(response)}"
322
321
  end
323
322
 
324
- content_type = response.headers[:content_type] if http_lib == :restclient
325
- content_type = response.headers['Content-Type'] if http_lib == :excon
323
+ content_type = response.headers[:content_type] if connection_library == :restclient
324
+ content_type = response.headers['Content-Type'] if connection_library == :excon
326
325
  content_type ||= 'text/plain'
327
326
 
328
327
  rslt = case
@@ -7,37 +7,48 @@ module Orientdb4r
7
7
  # accessible view REST API and 'excon' library on the client side.
8
8
  class ExconNode < RestNode
9
9
 
10
- def oo_request(options) #:nodoc:
11
- address = "#{url}/#{options[:uri]}"
12
- headers = {}
13
- headers['Authorization'] = basic_auth_header(options[:user], options[:password]) if options.include?(:user)
14
- headers['Cookie'] = "#{SESSION_COOKIE_NAME}=#{session_id}" unless session_id.nil?
15
- response = ::Excon.send options[:method].to_sym, address, :headers => headers
16
-
17
- def response.code
18
- status
19
- end
20
-
21
- response
22
- end
23
-
24
-
25
10
  def request(options) #:nodoc:
26
- raise OrientdbError, 'long life connection not initialized' if @connection.nil?
27
-
28
- head = headers
29
- head['Content-Type'] = options[:content_type] if options.include? :content_type
30
- options[:headers] = head
31
-
32
- options[:body] = options[:data] if options.include? :data # just other naming convention
33
- options.delete :data
34
- options[:path] = options[:uri] if options.include? :uri # just other naming convention
35
- options.delete :uri
36
-
37
- response = @connection.request options
11
+ verify_options(options, {:user => :mandatory, :password => :mandatory, \
12
+ :uri => :mandatory, :method => :mandatory, :content_type => :optional, :data => :optional})
13
+
14
+ opts = options.clone # if not cloned we change original hash map that cannot be used more with load balancing
15
+
16
+ # Auth + Cookie + Content-Type
17
+ opts[:headers] = headers(opts)
18
+ opts.delete :user
19
+ opts.delete :password
20
+
21
+ opts[:body] = opts[:data] if opts.include? :data # just other naming convention
22
+ opts.delete :data
23
+ opts[:path] = opts[:uri] if opts.include? :uri # just other naming convention
24
+ opts.delete :uri
25
+
26
+ was_ok = false
27
+ begin
28
+ response = connection.request opts
29
+ was_ok = (2 == (response.status / 100))
30
+
31
+ # store session ID if received to reuse in next request
32
+ cookies = CGI::Cookie::parse(response.headers['Set-Cookie'])
33
+ sessid = cookies[SESSION_COOKIE_NAME][0]
34
+ if session_id != sessid
35
+ @session_id = sessid
36
+ Orientdb4r::logger.debug "new session id: #{session_id}"
37
+ end
38
+
39
+ def response.code
40
+ status
41
+ end
42
+
43
+ rescue Excon::Errors::SocketError
44
+ raise NodeError
45
+ end
38
46
 
39
- def response.code
40
- status
47
+ # this is workaround for a strange behavior:
48
+ # excon delivered magic response status '1' when previous request was not 20x
49
+ unless was_ok
50
+ connection.reset
51
+ Orientdb4r::logger.debug 'response code not 20x -> connection reset'
41
52
  end
42
53
 
43
54
  response
@@ -45,28 +56,47 @@ module Orientdb4r
45
56
 
46
57
 
47
58
  def post_connect(user, password, http_response) #:nodoc:
48
- @basic_auth = basic_auth_header(user, password)
49
59
 
50
60
  cookies = CGI::Cookie::parse(http_response.headers['Set-Cookie'])
51
61
  @session_id = cookies[SESSION_COOKIE_NAME][0]
52
62
 
53
- @connection = Excon.new(url) if @connection.nil?
54
63
  end
55
64
 
56
65
 
57
66
  def cleanup #:nodoc:
58
- @session_id = nil
59
- @basic_auth = nil
67
+ super
68
+ connection.reset
60
69
  @connection = nil
61
70
  end
62
71
 
63
72
 
73
+ # ---------------------------------------------------------- Assistant Stuff
74
+
64
75
  private
65
76
 
77
+ ###
78
+ # Gets Excon connection.
79
+ def connection
80
+ @connection ||= Excon::Connection.new(url)
81
+ #:read_timeout => self.class.read_timeout,
82
+ #:write_timeout => self.class.write_timeout,
83
+ #:connect_timeout => self.class.connect_timeout
84
+ end
85
+
66
86
  ###
67
87
  # Get request headers prepared with session ID and Basic Auth.
68
- def headers
69
- {'Authorization' => @basic_auth, 'Cookie' => "#{SESSION_COOKIE_NAME}=#{session_id}"}
88
+ def headers(options)
89
+ rslt = {'Authorization' => basic_auth_header(options[:user], options[:password])}
90
+ rslt['Cookie'] = "#{SESSION_COOKIE_NAME}=#{session_id}" unless session_id.nil?
91
+ rslt['Content-Type'] = options[:content_type] if options.include? :content_type
92
+ rslt
93
+ end
94
+
95
+ ###
96
+ # Gets value of the Basic Auth header.
97
+ def basic_auth_header(user, password)
98
+ b64 = Base64.encode64("#{user}:#{password}").delete("\r\n")
99
+ "Basic #{b64}"
70
100
  end
71
101
 
72
102
  end
@@ -8,7 +8,7 @@ module Orientdb4r
8
8
  # Name of cookie that represents a session.
9
9
  SESSION_COOKIE_NAME = 'OSESSIONID'
10
10
 
11
- attr_reader :ssl, :session_id
11
+ attr_reader :ssl
12
12
 
13
13
  ###
14
14
  # Constructor.
@@ -26,36 +26,16 @@ module Orientdb4r
26
26
 
27
27
  # ----------------------------------------------------------- RestNode Stuff
28
28
 
29
- ###
30
- # Initializes a long life connection with credentials and session ID
31
- # after successful connect.
32
- def post_connect(user, password, http_response)
33
- raise NotImplementedError, 'this should be overridden by subclass'
34
- end
35
-
36
29
 
37
30
  ###
38
- # Sends an one-off request to the remote server.
39
- def oo_request(options)
40
- raise NotImplementedError, 'this should be overridden by subclass'
41
- end
42
-
43
-
44
- ###
45
- # Sends a request to the remote server
46
- # based on a connection object which is reusable across multiple requests.
31
+ # Sends a HTTP request to the remote server.
32
+ # Use following if possible:
33
+ # * session_id
34
+ # * Keep-Alive (if possible)
47
35
  def request(options)
48
36
  raise NotImplementedError, 'this should be overridden by subclass'
49
37
  end
50
38
 
51
-
52
- ###
53
- # Gets value of the Basic Auth header.
54
- def basic_auth_header(user, password)
55
- b64 = Base64.encode64("#{user}:#{password}").delete("\r\n")
56
- "Basic #{b64}"
57
- end
58
-
59
39
  end
60
40
 
61
41
  end
@@ -4,35 +4,41 @@ module Orientdb4r
4
4
 
5
5
  ###
6
6
  # This class represents a single sever/node in the Distributed Multi-Master Architecture
7
- # accessible view REST API and 'rest-client' library on the client side.
7
+ # accessible via REST API and 'rest-client' library on the client side.
8
8
  class RestClientNode < RestNode
9
9
 
10
- def oo_request(options) #:nodoc:
11
- begin
12
- options[:url] = "#{url}/#{options[:uri]}"
13
- options.delete :uri
14
- response = ::RestClient::Request.new(options).execute
15
- rescue ::RestClient::Exception => e
16
- response = transform_error2_response(e)
17
- end
10
+ def request(options) #:nodoc:
11
+ verify_options(options, {:user => :mandatory, :password => :mandatory, \
12
+ :uri => :mandatory, :method => :mandatory, :content_type => :optional, :data => :optional})
18
13
 
19
- response
20
- end
14
+ opts = options.clone # if not cloned we change original hash map that cannot be used more with load balancing
21
15
 
16
+ # URL
17
+ opts[:url] = "#{url}/#{opts[:uri]}"
18
+ opts.delete :uri
22
19
 
23
- def request(options) #:nodoc:
24
- raise OrientdbError, 'long life connection not initialized' if @resource.nil?
20
+ # data
21
+ data = opts.delete :data
22
+ data = '' if data.nil? and :post == opts[:method] # POST has to have data
23
+ opts[:payload] = data unless data.nil?
24
+
25
+ # headers
26
+ opts[:cookies] = { SESSION_COOKIE_NAME => session_id} unless session_id.nil?
25
27
 
26
- data = options[:data]
27
- options.delete :data
28
- data = '' if data.nil? and :post == options[:method] # POST has to have data
29
28
  begin
30
- # e.g. @resource['disconnect'].get
31
- if data.nil?
32
- response = @resource[options[:uri]].send options[:method].to_sym
33
- else
34
- response = @resource[options[:uri]].send options[:method].to_sym, data
29
+ response = ::RestClient::Request.new(opts).execute
30
+
31
+ # store session ID if received to reuse in next request
32
+ sessid = response.cookies[SESSION_COOKIE_NAME]
33
+ if session_id != sessid
34
+ @session_id = sessid
35
+ Orientdb4r::logger.debug "new session id: #{session_id}"
35
36
  end
37
+
38
+ rescue Errno::ECONNREFUSED
39
+ raise NodeError
40
+ rescue ::RestClient::ServerBrokeConnection
41
+ raise NodeError
36
42
  rescue ::RestClient::Exception => e
37
43
  response = transform_error2_response(e)
38
44
  end
@@ -41,23 +47,6 @@ module Orientdb4r
41
47
  end
42
48
 
43
49
 
44
- def post_connect(user, password, http_response) #:nodoc:
45
- @basic_auth = basic_auth_header(user, password)
46
- @session_id = http_response.cookies[SESSION_COOKIE_NAME]
47
-
48
- @resource = ::RestClient::Resource.new(url, \
49
- :user => user, :password => password, \
50
- :cookies => { SESSION_COOKIE_NAME => session_id})
51
- end
52
-
53
-
54
- def cleanup #:nodoc:
55
- @session_id = nil
56
- @basic_auth = nil
57
- @resource = nil
58
- end
59
-
60
-
61
50
  private
62
51
 
63
52
  ###
@@ -28,7 +28,7 @@ module Orientdb4r
28
28
 
29
29
  # set default values if missing in options
30
30
  pattern.each do |k,v|
31
- options[k] = v if !v.nil? and !options.keys.include? k
31
+ options[k] = v if !v.nil? and :optional != v and !options.keys.include? k
32
32
  end
33
33
  options
34
34
  end
@@ -2,6 +2,7 @@ module Orientdb4r
2
2
 
3
3
  # Version history.
4
4
  VERSION_HISTORY = [
5
+ ['0.3.0', '2012-08-01', "Added support for cluster of distributed servers + load balancing"],
5
6
  ['0.2.10', '2012-07-21', "Experimental support for Excon HTTP library with Keep-Alive connection"],
6
7
  ['0.2.9', '2012-07-18', "Added feature Client#delete_database, New class Rid"],
7
8
  ['0.2.8', '2012-07-16', "New exception handling, added feature Client#create_class(:properties)"],
data/lib/orientdb4r.rb CHANGED
@@ -19,6 +19,8 @@ module Orientdb4r
19
19
  autoload :RestNode, 'orientdb4r/rest/node'
20
20
  autoload :RestClientNode, 'orientdb4r/rest/restclient_node'
21
21
  autoload :ExconNode, 'orientdb4r/rest/excon_node'
22
+ autoload :Sequence, 'orientdb4r/load_balancing'
23
+ autoload :RoundRobin, 'orientdb4r/load_balancing'
22
24
 
23
25
 
24
26
  class << self
@@ -44,8 +46,15 @@ module Orientdb4r
44
46
  RestClient.proxy = url
45
47
  end
46
48
 
49
+ ###
50
+ # Logger used for logging output
47
51
  attr_accessor :logger
48
52
 
53
+ ###
54
+ # Predefined connection library.
55
+ # Can be overriden by option in client initialization.
56
+ attr_accessor :connection_library
57
+
49
58
  end
50
59
 
51
60
 
@@ -76,6 +85,13 @@ module Orientdb4r
76
85
  # mismatched types or incorrect cardinality.
77
86
  class DataError < OrientdbError; end
78
87
 
88
+ # ---------------------------------------------------------- System Exceptions
89
+
90
+ ###
91
+ # This exception represents a fatal failure which meens that the node is not accessible more.
92
+ # e.g. connection broken pipe
93
+ class NodeError < OrientdbError; end
94
+
79
95
  end
80
96
 
81
97
 
@@ -83,6 +99,10 @@ end
83
99
  Orientdb4r::logger = Logger.new(STDOUT)
84
100
  Orientdb4r::logger.level = Logger::INFO
85
101
 
102
+ # Default connection library
103
+ Orientdb4r::connection_library = :restclient
104
+ #Orientdb4r::connection_library = :excon
105
+
86
106
  Orientdb4r::logger.info \
87
107
  "Orientdb4r #{Orientdb4r::VERSION}, running on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
88
108
 
@@ -6,6 +6,9 @@ require 'orientdb4r'
6
6
  # * CONNECT
7
7
  # * DISCONNECT
8
8
  # * CREATE DATABASE
9
+ # * GET DATABASE
10
+ # * DELETE DATABASE
11
+ # * SERVER info
9
12
  class TestDatabase < Test::Unit::TestCase
10
13
 
11
14
  Orientdb4r::logger.level = Logger::DEBUG
@@ -23,13 +26,9 @@ class TestDatabase < Test::Unit::TestCase
23
26
  assert rslt.size > 0
24
27
  assert rslt.include? 'classes'
25
28
 
26
- #assert_equal 'localhost', @client.host # TODO moved to Node; mock?
27
- #assert_equal 2480, @client.port
28
- #assert_equal false, @client.ssl
29
29
  assert_equal 'admin', @client.user
30
30
  assert_equal 'admin', @client.password
31
31
  assert_equal 'temp', @client.database
32
- #assert_not_nil @client.session_id
33
32
  assert_not_nil @client.server_version
34
33
 
35
34
  # connection refused
@@ -63,13 +62,9 @@ class TestDatabase < Test::Unit::TestCase
63
62
  # unable to query after disconnect
64
63
  assert_raise Orientdb4r::ConnectionError do @client.query 'SELECT FROM OUser'; end
65
64
 
66
- #assert_equal 'localhost', @client.host # TODO moved to Node; mock?
67
- #assert_equal 2480, @client.port
68
- #assert_equal false, @client.ssl
69
65
  assert_nil @client.user
70
66
  assert_nil @client.password
71
67
  assert_nil @client.database
72
- #assert_nil @client.session_id
73
68
  assert_nil @client.server_version
74
69
  end
75
70
 
@@ -181,4 +176,21 @@ class TestDatabase < Test::Unit::TestCase
181
176
  assert_raise Orientdb4r::ConnectionError do @client.delete_document('#1:0'); end
182
177
  end
183
178
 
179
+
180
+ ###
181
+ # Tests using of session ID.
182
+ def test_session_id
183
+ client = Orientdb4r.client :instance => :new
184
+ assert_nil client.nodes[0].session_id
185
+ client.connect :database => 'temp', :user => 'admin', :password => 'admin'
186
+ session_id = client.nodes[0].session_id
187
+ assert_not_nil session_id
188
+ client.query 'SELECT count(*) FROM OUser'
189
+ assert_equal session_id, client.nodes[0].session_id
190
+ client.get_class 'OUser'
191
+ assert_equal session_id, client.nodes[0].session_id
192
+ client.disconnect
193
+ assert_nil client.nodes[0].session_id
194
+ end
195
+
184
196
  end
@@ -0,0 +1,105 @@
1
+ require 'test/unit'
2
+ require 'orientdb4r'
3
+
4
+ ###
5
+ # This class tests communication with OrientDB cluster and load balancing.
6
+ class TestDatabase < Test::Unit::TestCase
7
+
8
+ Orientdb4r::logger.level = Logger::DEBUG
9
+
10
+
11
+ ###
12
+ # Test inintialization of single node.
13
+ def test_one_node_initialization
14
+ client = Orientdb4r.client :instance => :new
15
+ assert_not_nil client.nodes
16
+ assert_instance_of Array, client.nodes
17
+ assert_equal 1, client.nodes.size
18
+ assert_equal 2480, client.nodes[0].port
19
+ assert_equal false, client.nodes[0].ssl
20
+ end
21
+
22
+ ###
23
+ # Test inintialization of more nodes.
24
+ def test_nodes_initialization
25
+ client = Orientdb4r.client :nodes => [{}, {:port => 2481}], :instance => :new
26
+ assert_not_nil client.nodes
27
+ assert_instance_of Array, client.nodes
28
+ assert_equal 2, client.nodes.size
29
+ assert_equal 2480, client.nodes[0].port
30
+ assert_equal 2481, client.nodes[1].port
31
+ assert_equal false, client.nodes[0].ssl
32
+ assert_equal false, client.nodes[1].ssl
33
+ end
34
+
35
+ ###
36
+ # Test default Sequence strategy.
37
+ def test_sequence_loadbalancing
38
+ client = Orientdb4r.client :nodes => [{}, {:port => 2481}], :instance => :new
39
+ lb_strategy = client.lb_strategy
40
+ assert_not_nil lb_strategy
41
+ assert_instance_of Orientdb4r::Sequence, lb_strategy
42
+ assert_equal 0, lb_strategy.node_index
43
+ assert_equal 0, lb_strategy.node_index
44
+ assert_equal client.nodes[0], client.nodes[client.lb_strategy.node_index]
45
+ assert_equal client.nodes[0], client.nodes[client.lb_strategy.node_index]
46
+ end
47
+
48
+ ###
49
+ # Test RoundRobin strategy.
50
+ def test_roundrobin_loadbalancing
51
+ client = Orientdb4r.client :nodes => [{}, {:port => 2481}], :load_balancing => :round_robin, :instance => :new
52
+ lb_strategy = client.lb_strategy
53
+ assert_not_nil lb_strategy
54
+ assert_instance_of Orientdb4r::RoundRobin, lb_strategy
55
+ assert_equal 0, lb_strategy.node_index
56
+ assert_equal 1, lb_strategy.node_index
57
+ assert_equal 0, lb_strategy.node_index
58
+ assert_equal client.nodes[1], client.nodes[client.lb_strategy.node_index]
59
+ assert_equal client.nodes[0], client.nodes[client.lb_strategy.node_index]
60
+ assert_equal client.nodes[1], client.nodes[client.lb_strategy.node_index]
61
+ end
62
+
63
+ def test_load_balancing_in_problems
64
+ # invalid port
65
+ client = Orientdb4r.client :port => 9999, :instance => :new
66
+ assert_raise Orientdb4r::ConnectionError do
67
+ client.connect :database => 'temp', :user => 'admin', :password => 'admin'
68
+ end
69
+ # opened port, but not REST
70
+ client = Orientdb4r.client :port => 2424, :instance => :new
71
+ assert_raise Orientdb4r::ConnectionError do
72
+ client.connect :database => 'temp', :user => 'admin', :password => 'admin'
73
+ end
74
+
75
+ # invalid ports - both
76
+ client = Orientdb4r.client :nodes => [{:port => 9998}, {:port => 9999}], :instance => :new
77
+ begin
78
+ client.connect :database => 'temp', :user => 'admin', :password => 'admin'
79
+ assert_equal 0, 1, "Orientdb4r::ConnectionError EXPECTED"
80
+ rescue Orientdb4r::ConnectionError => e
81
+ assert_equal 'all nodes failed to communicate with server!', e.message
82
+ end
83
+
84
+
85
+ # more nodes
86
+
87
+ # first node bad, second must work (sequence)
88
+ client = Orientdb4r.client :nodes => [{:port => 2481}, {}], :instance => :new
89
+ assert_nothing_thrown do # there has to be ERROR in log
90
+ client.connect :database => 'temp', :user => 'admin', :password => 'admin'
91
+ end
92
+ assert_equal 1, client.lb_strategy.bad_nodes.size
93
+ assert client.lb_strategy.bad_nodes.include? 0
94
+
95
+ # second node bad => second call has to be realized by first one (round robin)
96
+ client = Orientdb4r.client :nodes => [{}, {:port => 2481}], :load_balancing => :round_robin, :instance => :new
97
+ assert client.lb_strategy.bad_nodes.empty?
98
+ client.connect :database => 'temp', :user => 'admin', :password => 'admin'
99
+ assert client.lb_strategy.bad_nodes.empty?
100
+ client.query 'SELECT FROM OUser'
101
+ assert_equal 1, client.lb_strategy.bad_nodes.size
102
+ assert client.lb_strategy.bad_nodes.include? 1
103
+ end
104
+
105
+ end
data/test/test_utils.rb CHANGED
@@ -25,6 +25,14 @@ class TestDmo < Test::Unit::TestCase
25
25
  assert_equal 2, options.size
26
26
  assert_equal 'X', options[:a]
27
27
  assert_equal 'B', options[:b]
28
+
29
+ # :optional cannot be set as default value
30
+ opt_pattern = {:a => :optional, :b => 'B'}
31
+ options = {}
32
+ verify_and_sanitize_options(options, opt_pattern)
33
+ assert_equal 1, options.size
34
+ assert !options.include?(:a)
35
+ assert_equal 'B', options[:b]
28
36
  end
29
37
 
30
38
  def test_compare_versions
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: orientdb4r
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.10
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-21 00:00:00.000000000 Z
12
+ date: 2012-08-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rest-client
@@ -53,6 +53,7 @@ files:
53
53
  - lib/orientdb4r.rb
54
54
  - lib/orientdb4r/chained_error.rb
55
55
  - lib/orientdb4r/client.rb
56
+ - lib/orientdb4r/load_balancing.rb
56
57
  - lib/orientdb4r/node.rb
57
58
  - lib/orientdb4r/rest/client.rb
58
59
  - lib/orientdb4r/rest/excon_node.rb
@@ -66,6 +67,7 @@ files:
66
67
  - test/readme_sample.rb
67
68
  - test/test_database.rb
68
69
  - test/test_ddo.rb
70
+ - test/test_distributed.rb
69
71
  - test/test_dmo.rb
70
72
  - test/test_document_crud.rb
71
73
  - test/test_utils.rb
@@ -98,6 +100,7 @@ test_files:
98
100
  - test/readme_sample.rb
99
101
  - test/test_database.rb
100
102
  - test/test_ddo.rb
103
+ - test/test_distributed.rb
101
104
  - test/test_dmo.rb
102
105
  - test/test_document_crud.rb
103
106
  - test/test_utils.rb