vagas-orientdb4r 0.5.2

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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +26 -0
  4. data/.travis.yml +18 -0
  5. data/Gemfile +9 -0
  6. data/LICENSE +202 -0
  7. data/README.rdoc +124 -0
  8. data/Rakefile +43 -0
  9. data/changelog.txt +60 -0
  10. data/ci/initialize-ci.sh +36 -0
  11. data/fstudy/design_v1.dia +0 -0
  12. data/fstudy/design_v1.png +0 -0
  13. data/fstudy/domain_model.dia +0 -0
  14. data/fstudy/domain_model.png +0 -0
  15. data/fstudy/flat_class_perf.rb +56 -0
  16. data/fstudy/sample1_object_diagram.dia +0 -0
  17. data/fstudy/sample1_object_diagram.png +0 -0
  18. data/fstudy/study_case.rb +87 -0
  19. data/fstudy/technical_feasibility.rb +256 -0
  20. data/lib/orientdb4r.rb +115 -0
  21. data/lib/orientdb4r/bin/client.rb +86 -0
  22. data/lib/orientdb4r/bin/connection.rb +29 -0
  23. data/lib/orientdb4r/bin/constants.rb +20 -0
  24. data/lib/orientdb4r/bin/io.rb +38 -0
  25. data/lib/orientdb4r/bin/protocol28.rb +101 -0
  26. data/lib/orientdb4r/bin/protocol_factory.rb +25 -0
  27. data/lib/orientdb4r/chained_error.rb +37 -0
  28. data/lib/orientdb4r/client.rb +364 -0
  29. data/lib/orientdb4r/load_balancing.rb +113 -0
  30. data/lib/orientdb4r/node.rb +42 -0
  31. data/lib/orientdb4r/rest/client.rb +517 -0
  32. data/lib/orientdb4r/rest/excon_node.rb +115 -0
  33. data/lib/orientdb4r/rest/model.rb +159 -0
  34. data/lib/orientdb4r/rest/node.rb +43 -0
  35. data/lib/orientdb4r/rest/restclient_node.rb +77 -0
  36. data/lib/orientdb4r/rid.rb +54 -0
  37. data/lib/orientdb4r/utils.rb +203 -0
  38. data/lib/orientdb4r/version.rb +39 -0
  39. data/orientdb4r.gemspec +37 -0
  40. data/test/bin/test_client.rb +21 -0
  41. data/test/readme_sample.rb +38 -0
  42. data/test/test_client.rb +93 -0
  43. data/test/test_database.rb +261 -0
  44. data/test/test_ddo.rb +237 -0
  45. data/test/test_dmo.rb +115 -0
  46. data/test/test_document_crud.rb +184 -0
  47. data/test/test_gremlin.rb +52 -0
  48. data/test/test_helper.rb +10 -0
  49. data/test/test_loadbalancing.rb +81 -0
  50. data/test/test_utils.rb +67 -0
  51. metadata +136 -0
@@ -0,0 +1,364 @@
1
+ require 'uri'
2
+
3
+ module Orientdb4r
4
+
5
+ class Client
6
+ include Utils
7
+
8
+ # # Regexp to validate format of provided version.
9
+ SERVER_VERSION_PATTERN = /^\d+\.\d+\.\d+[-SNAPHOT]*$/
10
+
11
+ # connection parameters
12
+ attr_reader :user, :password, :database
13
+ # type of connection library [:restclient, :excon]
14
+ attr_reader :connection_library
15
+ # type of load balancing [:sequence, :round_robin]
16
+ attr_reader :load_balancing
17
+ # proxy for remote communication
18
+ attr_reader :proxy
19
+
20
+ # intern structures
21
+
22
+ # nodes responsible for communication with a server
23
+ attr_reader :nodes
24
+ # object implementing a LB strategy
25
+ attr_reader :lb_strategy
26
+
27
+ ###
28
+ # Constructor.
29
+ def initialize
30
+ @nodes = []
31
+ @connected = false
32
+ end
33
+
34
+
35
+ # --------------------------------------------------------------- CONNECTION
36
+
37
+ ###
38
+ # Connects client to the server.
39
+ def connect options
40
+ raise NotImplementedError, 'this should be overridden by concrete client'
41
+ end
42
+
43
+
44
+ ###
45
+ # Disconnects client from the server.
46
+ def disconnect
47
+ raise NotImplementedError, 'this should be overridden by concrete client'
48
+ end
49
+
50
+
51
+ ###
52
+ # Gets flag whenever the client is connected or not.
53
+ def connected?
54
+ @connected
55
+ end
56
+
57
+
58
+ ###
59
+ # Retrieve information about the connected OrientDB Server.
60
+ # Enables additional authentication to the server with an account
61
+ # that can access the 'server.info' resource.
62
+ def server(options={})
63
+ raise NotImplementedError, 'this should be overridden by concrete client'
64
+ end
65
+
66
+
67
+ # ----------------------------------------------------------------- DATABASE
68
+
69
+ ###
70
+ # Creates a new database.
71
+ # You can provide an additional authentication to the server with 'database.create' resource
72
+ # or the current one will be used.
73
+ # *options
74
+ # *storage - 'memory' (by default) or 'local'
75
+ # *type - 'document' (by default) or 'graph'
76
+ def create_database(options)
77
+ raise NotImplementedError, 'this should be overridden by concrete client'
78
+ end
79
+
80
+
81
+ ###
82
+ # Retrieves all the information about a database.
83
+ # Client has not to be connected to see databases suitable to connect.
84
+ def get_database(options)
85
+ raise NotImplementedError, 'this should be overridden by concrete client'
86
+ end
87
+
88
+
89
+ ###
90
+ # Checks existence of a given database.
91
+ # Client has not to be connected to see databases suitable to connect.
92
+ def database_exists?(options)
93
+ rslt = true
94
+ begin
95
+ get_database options
96
+ rescue OrientdbError
97
+ rslt = false
98
+ end
99
+ rslt
100
+ end
101
+
102
+
103
+ ###
104
+ # Drops a database.
105
+ # Requires additional authentication to the server.
106
+ def delete_database(options)
107
+ raise NotImplementedError, 'this should be overridden by concrete client'
108
+ end
109
+
110
+
111
+ ###
112
+ # Retrieves the available databases.
113
+ # That is protected by the resource "server.listDatabases"
114
+ # that by default is assigned to the guest (anonymous) user in orientdb-server-config.xml.
115
+ def list_databases(options)
116
+ raise NotImplementedError, 'this should be overridden by concrete client'
117
+ end
118
+
119
+
120
+ ###
121
+ # Exports a gzip file that contains the database JSON export.
122
+ # Returns name of stored file.
123
+ def export(options)
124
+ raise NotImplementedError, 'this should be overridden by concrete client'
125
+ end
126
+
127
+
128
+ ###
129
+ # Imports a database from an uploaded JSON text file.
130
+ def import(options)
131
+ raise NotImplementedError, 'this should be overridden by concrete client'
132
+ end
133
+
134
+ # ---------------------------------------------------------------------- SQL
135
+
136
+ ###
137
+ # Executes a query against the database.
138
+ def query(sql, options)
139
+ raise NotImplementedError, 'this should be overridden by concrete client'
140
+ end
141
+
142
+
143
+ ###
144
+ # Executes a command against the database.
145
+ def command(sql)
146
+ raise NotImplementedError, 'this should be overridden by concrete client'
147
+ end
148
+
149
+
150
+ # -------------------------------------------------------------------- CLASS
151
+
152
+ ###
153
+ # Creates a new class in the schema.
154
+ def create_class(name, options={})
155
+ raise ArgumentError, "class name is blank" if blank?(name)
156
+ opt_pattern = {
157
+ :extends => :optional, :cluster => :optional, :force => false, :abstract => false,
158
+ :properties => :optional
159
+ }
160
+ verify_options(options, opt_pattern)
161
+
162
+ sql = "CREATE CLASS #{name}"
163
+ sql << " EXTENDS #{options[:extends]}" if options.include? :extends
164
+ sql << " CLUSTER #{options[:cluster]}" if options.include? :cluster
165
+ sql << ' ABSTRACT' if options.include?(:abstract)
166
+
167
+ drop_class name if options[:force]
168
+
169
+ command sql
170
+
171
+ # properties given?
172
+ if options.include? :properties
173
+ props = options[:properties]
174
+ raise ArgumentError, 'properties have to be an array' unless props.is_a? Array
175
+
176
+ props.each do |prop|
177
+ raise ArgumentError, 'property definition has to be a hash' unless prop.is_a? Hash
178
+ prop_name = prop.delete :property
179
+ prop_type = prop.delete :type
180
+ create_property(name, prop_name, prop_type, prop)
181
+ end
182
+ end
183
+
184
+ if block_given?
185
+ proxy = Orientdb4r::Utils::Proxy.new(self, name)
186
+ def proxy.property(property, type, options={})
187
+ self.target.send :create_property, self.context, property, type, options
188
+ end
189
+ def proxy.link(property, type, linked_class, options={})
190
+ raise ArgumentError, "type has to be a linked-type, given=#{type}" unless type.to_s.start_with? 'link'
191
+ options[:linked_class] = linked_class
192
+ self.target.send :create_property, self.context, property, type, options
193
+ end
194
+ yield proxy
195
+ end
196
+ end
197
+
198
+
199
+ ###
200
+ # Gets informations about requested class.
201
+ def get_class(name)
202
+ raise NotImplementedError, 'this should be overridden by concrete client'
203
+ end
204
+
205
+
206
+ ###
207
+ # Checks existence of a given class.
208
+ def class_exists?(name)
209
+ rslt = true
210
+ begin
211
+ get_class name
212
+ rescue OrientdbError => e
213
+ raise e if e.is_a? ConnectionError and e.message == 'not connected' # workaround for AOP2 (unable to decorate already existing methods)
214
+ rslt = false
215
+ end
216
+ rslt
217
+ end
218
+
219
+
220
+ ###
221
+ # Removes a class from the schema.
222
+ def drop_class(name, options={})
223
+ raise ArgumentError, 'class name is blank' if blank?(name)
224
+
225
+ # :mode=>:strict forbids to drop a class that is a super class for other one
226
+ opt_pattern = { :mode => :nil }
227
+ verify_options(options, opt_pattern)
228
+ if :strict == options[:mode]
229
+ response = get_database
230
+ children = response['classes'].select { |i| i['superClass'] == name }
231
+ unless children.empty?
232
+ raise OrientdbError, "class is super-class, cannot be deleted, name=#{name}"
233
+ end
234
+ end
235
+
236
+ command "DROP CLASS #{name}"
237
+ end
238
+
239
+
240
+ ###
241
+ # Creates a new property in the schema.
242
+ # You need to create the class before.
243
+ def create_property(clazz, property, type, options={})
244
+ raise ArgumentError, "class name is blank" if blank?(clazz)
245
+ raise ArgumentError, "property name is blank" if blank?(property)
246
+ opt_pattern = {
247
+ :mandatory => :optional , :notnull => :optional, :min => :optional, :max => :optional,
248
+ :readonly => :optional, :linked_class => :optional
249
+ }
250
+ verify_options(options, opt_pattern)
251
+
252
+ cmd = "CREATE PROPERTY #{clazz}.#{property} #{type.to_s}"
253
+ # link?
254
+ if [:link, :linklist, :linkset, :linkmap].include? type.to_s.downcase.to_sym
255
+ raise ArgumentError, "defined linked-type, but not linked-class" unless options.include? :linked_class
256
+ cmd << " #{options[:linked_class]}"
257
+ end
258
+ command cmd
259
+
260
+ # ALTER PROPERTY ...
261
+ options.delete :linked_class # it's not option for ALTER
262
+ unless options.empty?
263
+ options.each do |k,v|
264
+ command "ALTER PROPERTY #{clazz}.#{property} #{k.to_s.upcase} #{v}"
265
+ end
266
+ end
267
+ end
268
+
269
+
270
+ # ----------------------------------------------------------------- DOCUMENT
271
+
272
+ ###
273
+ # Create a new document.
274
+ # Returns the Record-id assigned for OrientDB version <= 1.3.x
275
+ # and the whole new document for version >= 1.4.x
276
+ # (see https://groups.google.com/forum/?fromgroups=#!topic/orient-database/UJGAXYpHDmo for more info).
277
+ def create_document(doc)
278
+ raise NotImplementedError, 'this should be overridden by concrete client'
279
+ end
280
+
281
+
282
+ ###
283
+ # Retrieves a document by given ID.
284
+ def get_document(rid)
285
+ raise NotImplementedError, 'this should be overridden by concrete client'
286
+ end
287
+
288
+
289
+ ###
290
+ # Updates an existing document.
291
+ def update_document(doc)
292
+ raise NotImplementedError, 'this should be overridden by concrete client'
293
+ end
294
+
295
+
296
+ ###
297
+ # Deletes an existing document.
298
+ def delete_document(rid)
299
+ raise NotImplementedError, 'this should be overridden by concrete client'
300
+ end
301
+
302
+
303
+ protected
304
+
305
+ ###
306
+ # Calls the server with a specific task.
307
+ # Returns a response according to communication channel (e.g. HTTP response).
308
+ def call_server(options)
309
+ lb_all_bad_msg = 'all nodes failed to communicate with server!'
310
+ response = nil
311
+
312
+ # credentials if not defined explicitly
313
+ options[:user] = user unless options.include? :user
314
+ options[:password] = password unless options.include? :password
315
+ debug_string = options[:uri]
316
+ if debug_string
317
+ query_log("Orientdb4r::Client#call_server", URI.decode(debug_string))
318
+ end
319
+ idx = lb_strategy.node_index
320
+ raise OrientdbError, lb_all_bad_msg if idx.nil? # no good node found
321
+
322
+ begin
323
+ node = @nodes[idx]
324
+ begin
325
+ response = node.request options
326
+ lb_strategy.good_one idx
327
+ return response
328
+
329
+ rescue NodeError => e
330
+ Orientdb4r::logger.error "node error, index=#{idx}, msg=#{e.message}, #{node}"
331
+ node.cleanup
332
+ lb_strategy.bad_one idx
333
+ idx = lb_strategy.node_index
334
+ end
335
+ end until idx.nil? and response.nil? # both 'nil' <= we tried all nodes and all with problem
336
+
337
+ raise OrientdbError, lb_all_bad_msg
338
+ end
339
+
340
+
341
+ ###
342
+ # Asserts if the client is connected and raises an error if not.
343
+ def assert_connected
344
+ raise ConnectionError, 'not connected' unless @connected
345
+ end
346
+
347
+
348
+ ###
349
+ # Around advice to meassure and print the method time.
350
+ def time_around(&block)
351
+ start = Time.now
352
+ rslt = block.call
353
+ query_log("#{aop_context[:class].name}##{aop_context[:method]}", "elapsed time = #{Time.now - start} [s]")
354
+ rslt
355
+ end
356
+
357
+ def query_log(context, message)
358
+ Orientdb4r::logger.debug \
359
+ " \033[01;33m#{context}:\033[0m #{message}"
360
+ end
361
+
362
+ end
363
+
364
+ end
@@ -0,0 +1,113 @@
1
+ module Orientdb4r
2
+
3
+ ###
4
+ # Base class for implementation of load balancing strategy.
5
+ class LBStrategy
6
+
7
+ # After what time [s] can be a failed node reused in load balancing.
8
+ DEFAULT_RECOVER_TIME = 30
9
+
10
+ attr_reader :nodes_count, :bad_nodes
11
+ attr_accessor :recover_time
12
+
13
+ ###
14
+ # Constructor.
15
+ def initialize nodes_count
16
+ @nodes_count = nodes_count
17
+ @bad_nodes = {}
18
+ @recover_time = DEFAULT_RECOVER_TIME
19
+ end
20
+
21
+ ###
22
+ # Gets index of node to be used for next request
23
+ # or 'nil' if there is no one next.
24
+ def node_index
25
+ raise NotImplementedError, 'this should be overridden in subclass'
26
+ end
27
+
28
+ ###
29
+ # Marks an index as good that means it can be used for next server calls.
30
+ def good_one(idx)
31
+ @bad_nodes.delete idx
32
+ end
33
+
34
+ ###
35
+ # Marks an index as bad that means it will be not used until:
36
+ # * there is other 'good' node
37
+ # * timeout
38
+ def bad_one(idx)
39
+ @bad_nodes[idx] = Time.now
40
+ end
41
+
42
+ protected
43
+
44
+ ###
45
+ # Tries to find a new node if the given failed.
46
+ # Returns <i>nil</i> if no one found
47
+ def search_next_good(bad_idx)
48
+ Orientdb4r::logger.warn "identified bad node, idx=#{bad_idx}, age=#{Time.now - @bad_nodes[bad_idx]} [s]"
49
+
50
+ # alternative nodes of not found a good one
51
+ timeout_candidate = nil
52
+
53
+ # first round - try to find a first good one
54
+ 1.upto(nodes_count) do |i|
55
+ candidate = (i + bad_idx) % nodes_count
56
+
57
+ if @bad_nodes.include? candidate
58
+ failure_time = @bad_nodes[candidate]
59
+ now = Time.now
60
+ # timeout candidate
61
+ if (now - failure_time) > recover_time
62
+ timeout_candidate = candidate
63
+ Orientdb4r::logger.debug "node timeout recovery, idx=#{candidate}"
64
+ good_one(candidate)
65
+ end
66
+ else
67
+ Orientdb4r::logger.debug "found good node, idx=#{candidate}"
68
+ return candidate
69
+ end
70
+ end
71
+
72
+ # no good index found -> try timeouted one
73
+ unless timeout_candidate.nil?
74
+ Orientdb4r::logger.debug "good node not found, delivering timeouted one, idx=#{timeout_candidate}"
75
+ return timeout_candidate
76
+ end
77
+
78
+ Orientdb4r::logger.error 'no nodes more, all invalid'
79
+ nil
80
+ end
81
+
82
+ end
83
+
84
+ ###
85
+ # Implementation of Sequence strategy.
86
+ # Assigns work in the order of nodes defined by the client initialization.
87
+ class Sequence < LBStrategy
88
+
89
+ def node_index #:nodoc:
90
+ @last_index = 0 if @last_index.nil?
91
+
92
+ @last_index = search_next_good(@last_index) if @bad_nodes.include? @last_index
93
+ @last_index
94
+ end
95
+
96
+ end
97
+
98
+ ###
99
+ # Implementation of Round Robin strategy.
100
+ # Assigns work in round-robin order per nodes defined by the client initialization.
101
+ class RoundRobin < LBStrategy
102
+
103
+ def node_index #:nodoc:
104
+ @last_index = -1 if @last_index.nil?
105
+
106
+ @last_index = (@last_index + 1) % nodes_count
107
+ @last_index = search_next_good(@last_index) if @bad_nodes.include? @last_index
108
+ @last_index
109
+ end
110
+
111
+ end
112
+
113
+ end