ruote-ar 0.0.4

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 74ff893d6570dea5e548db182d76ab6f9f4cf669
4
+ data.tar.gz: b0340869ce9c611fbf280cf27f4561745a6b31fe
5
+ SHA512:
6
+ metadata.gz: ffdca4a2483653789724db5cec12e398ee319415440008e204087d17854fd787c8c6fb634502c98c18944269b47cad162bc65a9ff139f865315712e51503a145
7
+ data.tar.gz: 8606fc9ceb173ead835b6f951a95867faca673629721b45d0c6f43916e6efa79f1ea0e457590e821b125943b3ad9da3310eee2ff1fc1905b0b1765982133f362
data/lib/ruote-ar.rb ADDED
@@ -0,0 +1 @@
1
+ require 'ruote/ar/storage'
data/lib/ruote/ar.rb ADDED
@@ -0,0 +1 @@
1
+ require 'ruote/ar/storage'
@@ -0,0 +1,422 @@
1
+ # encoding: utf-8
2
+
3
+ require 'active_record'
4
+ require 'ruote/storage/base'
5
+
6
+ Rufus::Json.backend = :active_support
7
+
8
+ module Ruote
9
+ module ActiveRecord
10
+ class Storage
11
+ include Ruote::StorageBase
12
+
13
+ def initialize(options = {})
14
+
15
+ @table_name = options['table_name'] || ('documents').to_sym
16
+
17
+ replace_engine_configuration(options)
18
+ end
19
+
20
+
21
+ def put_msg(action, options)
22
+
23
+ # put_msg is a unique action, no need for all the complexity of put
24
+ do_insert(prepare_msg_doc(action, options), 1)
25
+
26
+ nil
27
+ end
28
+
29
+ # Used to reserve 'msgs' and 'schedules'. Simply update and
30
+ # return true if the update was affected more than one line.
31
+ #
32
+ def reserve(doc)
33
+ um = Arel::UpdateManager.new Arel::Table.engine
34
+ um.table table
35
+ um.where table[:typ].eq(doc['type'].to_s).and(table[:ide].eq(doc['_id'].to_s).and(table[:rev].eq(1).and(table[:worker].eq(nil))))
36
+ um.set [
37
+ [table[:worker], worker]
38
+ ]
39
+ connection.update(um.to_sql) > 0
40
+ end
41
+
42
+ # removing doc after success (or fail) success.
43
+ # It's important to not leave any message.
44
+ def done(doc)
45
+ dm = Arel::DeleteManager.new Arel::Table.engine
46
+ dm.from table
47
+ dm.where table[:typ].eq(doc['type']).and(table[:ide].eq(doc['_id']).and(table[:rev].eq(1).and(table[:worker].eq(worker))))
48
+ connection.delete(dm)
49
+ end
50
+
51
+ def put_schedule(flavour, owner_fei, s, msg)
52
+
53
+ # put_schedule is a unique action, no need for all the complexity of put
54
+
55
+ doc = prepare_schedule_doc(flavour, owner_fei, s, msg)
56
+
57
+ return nil unless doc
58
+
59
+ do_insert(doc, 1)
60
+
61
+ doc['_id']
62
+ end
63
+
64
+ def put(doc, opts={})
65
+
66
+ cache_clear(doc)
67
+
68
+ if doc['_rev']
69
+
70
+ d = get(doc['type'], doc['_id'])
71
+
72
+ return true unless d
73
+ return d if d['_rev'] != doc['_rev']
74
+ # failures
75
+ end
76
+
77
+ nrev = doc['_rev'].to_i + 1
78
+
79
+ begin
80
+
81
+ do_insert(doc, nrev, opts[:update_rev])
82
+
83
+ rescue Exception => de
84
+ puts "Error putting: #{de.message}: #{doc.inspect}"
85
+ return (get(doc['type'], doc['_id']) || true)
86
+ # failure
87
+ end
88
+
89
+ dm = Arel::DeleteManager.new Arel::Table.engine
90
+ dm.from table
91
+ dm.where table[:typ].eq(doc['type']).and(table[:ide].eq(doc['_id']).and(table[:rev].lt(nrev)))
92
+ connection.delete(dm)
93
+
94
+ nil
95
+ # success
96
+ end
97
+
98
+ def get(type, key)
99
+ cache_get(type, key) || do_get(type, key)
100
+ end
101
+
102
+ def delete(doc)
103
+ raise true if doc.nil?
104
+
105
+ raise ArgumentError.new('no _rev for doc') unless doc['_rev']
106
+
107
+ cache_clear(doc)
108
+ # usually not necessary, adding it not to forget it later on
109
+
110
+ dm = Arel::DeleteManager.new Arel::Table.engine
111
+ dm.from table
112
+ dm.where table[:typ].eq(doc['type']).and(table[:ide].eq(doc['_id']).and(table[:rev].eq(doc['_rev'].to_i)))
113
+ count = connection.delete(dm)
114
+
115
+ return (get(doc['type'], doc['_id']) || true) if count < 1
116
+ # failure
117
+
118
+ nil
119
+ # success
120
+ end
121
+
122
+ def get_many(type, key=nil, opts={})
123
+
124
+ ###
125
+
126
+ cached = cache_get_many(type, key, opts)
127
+ return cached if cached
128
+
129
+ ds = table[:typ].eq(type)
130
+
131
+ keys = key ? Array(key) : nil
132
+ ds = ds.and(table[:wfid].in(keys)) if keys && keys.first.is_a?(String)
133
+
134
+ ds = table.where(ds)
135
+
136
+ return connection.select_value(ds.project(table[:wfid].count)) if opts[:count]
137
+
138
+ if opts[:descending].is_a?(Array) && opts[:descending].first.class != String
139
+ opts[:descending] = opts[:descending].collect {|s| s.inspect.gsub(':','').gsub('.', ' ')}
140
+ end
141
+
142
+ if opts[:descending]
143
+ ds = ds.order(table[:ide].desc, table[:rev].desc)
144
+ else
145
+ ds = ds.order(table[:ide].asc, table[:rev].asc)
146
+ end
147
+
148
+ ds = ds.take(opts[:limit]).skip(opts[:skip]||opts[:offset])
149
+
150
+ docs = connection.select_all(ds.project('*'))
151
+ docs = select_last_revs(docs)
152
+ docs = docs.collect { |d| Rufus::Json.decode(d['doc']) }
153
+
154
+ if keys && keys.first.is_a?(Regexp)
155
+ docs.select { |doc| keys.find { |k| k.match(doc['_id']) } }
156
+ else
157
+ docs
158
+ end
159
+
160
+ # (pass on the dataset.filter(:wfid => /regexp/) for now
161
+ # since we have potentially multiple keys)
162
+ end
163
+
164
+ # Returns all the ids of the documents of a given type.
165
+ #
166
+ def ids(type)
167
+ connection.select_values(table.where(table[:typ].eq(type)).project('distinct ide').order(table[:ide]))
168
+ end
169
+
170
+ # Nukes all the documents in this storage.
171
+ #
172
+ def purge!
173
+ # just for test
174
+ end
175
+
176
+ # Returns connection to pool
177
+ def shutdown
178
+ ::ActiveRecord::Base.clear_active_connections!
179
+ ::ActiveRecord::Base.connection.close
180
+ end
181
+
182
+ # Grrr... I should sort the mess between close and shutdown...
183
+ # Tests vs production :-(
184
+ #
185
+ def close
186
+ shutdown
187
+ end
188
+
189
+ # Mainly used by ruote's test/unit/ut_17_storage.rb
190
+ #
191
+ def add_type(type)
192
+ # does nothing, types are differentiated by the 'typ' column
193
+ end
194
+
195
+ # Nukes a db type and reputs it (losing all the documents that were in it).
196
+ #
197
+ def purge_type!(type)
198
+ # just for test
199
+ end
200
+
201
+ # A provision made for workitems, allow to query them directly by
202
+ # participant name.
203
+ #
204
+ def by_participant(type, participant_name, opts={})
205
+
206
+ raise NotImplementedError if type != 'workitems'
207
+
208
+ docs = table.where(table[:typ].eq(type).and(table[:participant_name].eq(participant_name)))
209
+
210
+ return connection.select_value(docs.project('count(*)')) if opts[:count]
211
+
212
+ docs = connection.select_all(docs.project('*').order(table[:ide].asc, table[:rev].desc).take(opts[:limit]).skip(opts[:offset] || opts[:skip]))
213
+
214
+ select_last_revs(docs).collect { |d| Ruote::Workitem.from_json(d['doc']) }
215
+ end
216
+
217
+ # Querying workitems by field (warning, goes deep into the JSON structure)
218
+ #
219
+ def by_field(type, field, value, opts={})
220
+
221
+ raise NotImplementedError if type != 'workitems'
222
+
223
+ lk = [ '%"', field, '":' ]
224
+ lk.push(Rufus::Json.encode(value)) if value
225
+ lk.push('%')
226
+
227
+ docs = table.where(table[:typ].eq(type).and(table[:doc].matches(lk.join)))
228
+
229
+ return connection.select_value(docs.project('count(*)')) if opts[:count]
230
+
231
+ docs = connection.select_all(docs.project('*').order(table[:ide].asc, table[:rev].desc).take(opts[:limit]).skip(opts[:offset] || opts[:skip]))
232
+ select_last_revs(docs).collect { |d| Ruote::Workitem.from_json(d['doc']) }
233
+ end
234
+
235
+ def query_workitems(criteria)
236
+
237
+ ds = table[:typ].eq('workitems')
238
+
239
+ wfid = criteria.delete('wfid')
240
+ ds = ds.and(table[:ide].matches("%!#{wfid}")) if wfid
241
+
242
+ pname = criteria.delete('participant_name') || criteria.delete('participant')
243
+ ds = ds.and(table[:participant_name].eq(pname)) if pname
244
+
245
+ count = criteria.delete('count')
246
+ limit = criteria.delete('limit')
247
+ offset = criteria.delete('offset') || criteria.delete('skip')
248
+
249
+ criteria.collect do |k, v|
250
+ ds = ds.and(table[:doc].matches("%\"#{k}\":#{Rufus::Json.encode(v)}%"))
251
+ end
252
+
253
+ ds = table.where(ds).take(limit).skip(offset)
254
+
255
+ return connection.select_one(ds.project(table[:wfid].count)).first if count
256
+
257
+ select_last_revs(connection.select_all(ds.project('*'))).collect { |d| Ruote::Workitem.from_json(d['doc']) }
258
+ end
259
+
260
+ def begin_step
261
+
262
+ prepare_cache
263
+ end
264
+
265
+ protected
266
+
267
+ def decode_doc(doc)
268
+
269
+ return nil if doc.nil?
270
+
271
+ doc = doc['doc']
272
+
273
+ Rufus::Json.decode(doc)
274
+ end
275
+
276
+
277
+ def do_insert(doc, rev, update_rev=false)
278
+
279
+ doc = doc.send(
280
+ update_rev ? :merge! : :merge,
281
+ {'_rev' => rev, 'put_at' => Ruote.now_to_utc_s}
282
+ )
283
+
284
+ m = Arel::InsertManager.new(Arel::Table.engine)
285
+ m.into table
286
+ m.insert [
287
+ [table[:ide], (doc['_id'] || '')],
288
+ [table[:rev], (rev || '')],
289
+ [table[:typ], (doc['type'] || '')],
290
+ [table[:doc], (Rufus::Json.encode(doc) || '')],
291
+ [table[:wfid], (extract_wfid(doc) || '')],
292
+ [table[:participant_name], (doc['participant_name'] || '')]]
293
+
294
+ connection.insert(m)
295
+ end
296
+
297
+
298
+ def extract_wfid(doc)
299
+ doc['wfid'] || (doc['fei'] ? doc['fei']['wfid'] : nil)
300
+ end
301
+
302
+ def do_get(type, key)
303
+ decode_doc connection.select_one(table.project('*').
304
+ where(table[:typ].eq(type).and(table[:ide].eq(key))).
305
+ order(table[:rev].desc))
306
+ end
307
+
308
+ # Don't put configuration if it's already in
309
+ #
310
+ # (avoid storages from trashing configuration...)
311
+ #
312
+ # def put_configuration
313
+ #
314
+ # return if get('configurations', 'engine')
315
+ #
316
+ # conf = { '_id' => 'engine', 'type' => 'configurations' }.merge(@options)
317
+ # put(conf)
318
+ # end
319
+
320
+ # Weed out older docs (same ide, smaller rev).
321
+ #
322
+ # This could all have been done via SQL, but those inconsistencies
323
+ # are rare, the cost of the pumped SQL is not constant :-(
324
+ #
325
+ def select_last_revs(docs)
326
+ docs.each_with_object([]) { |doc,a|
327
+ a << doc if a.last.nil? || doc['ide'] != a.last['ide']
328
+ }
329
+ end
330
+
331
+ #--
332
+ # worker step cache
333
+ #
334
+ # in order to cut down the number of selects, do one select with
335
+ # all the information the worker needs for one step of work
336
+ #++
337
+
338
+
339
+ CACHED_TYPES = %w[ msgs schedules configurations variables ]
340
+
341
+ # One select to grab in all the info necessary for a worker step
342
+ # (expressions excepted).
343
+ #
344
+ def prepare_cache
345
+
346
+ CACHED_TYPES.each { |t| cache[t] = {} }
347
+
348
+ ds = table.where(table[:typ].in(CACHED_TYPES)).
349
+ project(table[:ide], table[:typ], table[:doc]).
350
+ order(table[:ide].asc, table[:rev].desc)
351
+
352
+ connection.select_all(ds).each do |d|
353
+ (cache[d['typ']] ||= {})[d['ide']] ||= decode_doc(d)
354
+ end
355
+
356
+ cache['variables']['trackers'] ||=
357
+ { '_id' => 'trackers', 'type' => 'variables', 'trackers' => {} }
358
+ end
359
+
360
+ # Ask the cache for a doc. Returns nil if it's not cached.
361
+ #
362
+ def cache_get(type, key)
363
+
364
+ (cache[type] || {})[key]
365
+ end
366
+
367
+ # Ask the cache for a set of documents. Returns nil if it's not cached
368
+ # or caching is not OK.
369
+ #
370
+ def cache_get_many(type, keys, options)
371
+
372
+ if !options[:batch] && CACHED_TYPES.include?(type) && cache[type]
373
+ cache[type].values
374
+ else
375
+ nil
376
+ end
377
+ end
378
+
379
+ # Removes a document from the cache.
380
+ #
381
+ def cache_clear(doc)
382
+
383
+ (cache[doc['type']] || {}).delete(doc['_id'])
384
+ end
385
+
386
+ # Returns the cache for the given thread. Returns {} if there is no
387
+ # cache available.
388
+ #
389
+ def cache
390
+
391
+ worker = Thread.current['ruote_worker']
392
+
393
+ return {} unless worker
394
+
395
+ (Thread.current["cache_#{worker.name}"] ||= {})
396
+ end
397
+
398
+
399
+ # def do_delete(doc)
400
+ # Document.delete_all(
401
+ # :ide => doc['_id'], :typ => doc['type'], :rev => doc['_rev'].to_i
402
+ # )
403
+ # end
404
+
405
+ private
406
+ def table
407
+ @table ||= ::Arel::Table.new @table_name
408
+ end
409
+
410
+ def connection
411
+ ::ActiveRecord::Base.connection
412
+ end
413
+
414
+ def worker
415
+ worker = Thread.current['ruote_worker']
416
+ if worker
417
+ worker.name
418
+ end || 'worker'
419
+ end
420
+ end
421
+ end
422
+ end
@@ -0,0 +1,5 @@
1
+ module Ruote
2
+ module ActiveRecord
3
+ VERSION = '0.0.5'
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruote-ar
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - pedroteixeira
8
+ - jiangchaofan
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-07-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '3.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '3.0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rails
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '3.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '3.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: ruote
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '2.0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '2.0'
56
+ description: ruote storage
57
+ email:
58
+ - pedro@intelie.com.br
59
+ - jiangchaofan@gmail.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - lib/ruote-ar.rb
65
+ - lib/ruote/ar.rb
66
+ - lib/ruote/ar/storage.rb
67
+ - lib/ruote/ar/version.rb
68
+ homepage: https://github.com/intelie/ruote-ar
69
+ licenses:
70
+ - apache
71
+ metadata: {}
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubyforge_project:
88
+ rubygems_version: 2.2.2
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: ruote storage
92
+ test_files: []