vagas-orientdb4r 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +26 -0
- data/.travis.yml +18 -0
- data/Gemfile +9 -0
- data/LICENSE +202 -0
- data/README.rdoc +124 -0
- data/Rakefile +43 -0
- data/changelog.txt +60 -0
- data/ci/initialize-ci.sh +36 -0
- data/fstudy/design_v1.dia +0 -0
- data/fstudy/design_v1.png +0 -0
- data/fstudy/domain_model.dia +0 -0
- data/fstudy/domain_model.png +0 -0
- data/fstudy/flat_class_perf.rb +56 -0
- data/fstudy/sample1_object_diagram.dia +0 -0
- data/fstudy/sample1_object_diagram.png +0 -0
- data/fstudy/study_case.rb +87 -0
- data/fstudy/technical_feasibility.rb +256 -0
- data/lib/orientdb4r.rb +115 -0
- data/lib/orientdb4r/bin/client.rb +86 -0
- data/lib/orientdb4r/bin/connection.rb +29 -0
- data/lib/orientdb4r/bin/constants.rb +20 -0
- data/lib/orientdb4r/bin/io.rb +38 -0
- data/lib/orientdb4r/bin/protocol28.rb +101 -0
- data/lib/orientdb4r/bin/protocol_factory.rb +25 -0
- data/lib/orientdb4r/chained_error.rb +37 -0
- data/lib/orientdb4r/client.rb +364 -0
- data/lib/orientdb4r/load_balancing.rb +113 -0
- data/lib/orientdb4r/node.rb +42 -0
- data/lib/orientdb4r/rest/client.rb +517 -0
- data/lib/orientdb4r/rest/excon_node.rb +115 -0
- data/lib/orientdb4r/rest/model.rb +159 -0
- data/lib/orientdb4r/rest/node.rb +43 -0
- data/lib/orientdb4r/rest/restclient_node.rb +77 -0
- data/lib/orientdb4r/rid.rb +54 -0
- data/lib/orientdb4r/utils.rb +203 -0
- data/lib/orientdb4r/version.rb +39 -0
- data/orientdb4r.gemspec +37 -0
- data/test/bin/test_client.rb +21 -0
- data/test/readme_sample.rb +38 -0
- data/test/test_client.rb +93 -0
- data/test/test_database.rb +261 -0
- data/test/test_ddo.rb +237 -0
- data/test/test_dmo.rb +115 -0
- data/test/test_document_crud.rb +184 -0
- data/test/test_gremlin.rb +52 -0
- data/test/test_helper.rb +10 -0
- data/test/test_loadbalancing.rb +81 -0
- data/test/test_utils.rb +67 -0
- 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
|