orientdb4r 0.2.10 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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