ruote 2.1.10 → 2.1.11
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/CHANGELOG.txt +51 -1
- data/CREDITS.txt +9 -0
- data/README.rdoc +13 -0
- data/Rakefile +50 -21
- data/TODO.txt +42 -4
- data/examples/pong.rb +37 -0
- data/lib/ruote/context.rb +19 -9
- data/lib/ruote/engine/process_error.rb +10 -0
- data/lib/ruote/engine/process_status.rb +140 -41
- data/lib/ruote/engine.rb +394 -27
- data/lib/ruote/exp/command.rb +2 -0
- data/lib/ruote/exp/fe_concurrence.rb +8 -0
- data/lib/ruote/exp/fe_concurrent_iterator.rb +3 -0
- data/lib/ruote/exp/fe_cursor.rb +48 -4
- data/lib/ruote/exp/fe_iterator.rb +40 -0
- data/lib/ruote/exp/fe_listen.rb +3 -3
- data/lib/ruote/exp/fe_participant.rb +30 -12
- data/lib/ruote/exp/fe_ref.rb +126 -0
- data/lib/ruote/exp/fe_subprocess.rb +20 -1
- data/lib/ruote/exp/fe_wait.rb +4 -1
- data/lib/ruote/exp/fe_when.rb +7 -10
- data/lib/ruote/exp/flowexpression.rb +23 -12
- data/lib/ruote/exp/ro_attributes.rb +5 -8
- data/lib/ruote/exp/ro_variables.rb +4 -2
- data/lib/ruote/fei.rb +2 -0
- data/lib/ruote/id/wfid_generator.rb +1 -1
- data/lib/ruote/log/pretty.rb +137 -0
- data/lib/ruote/log/storage_history.rb +1 -1
- data/lib/ruote/log/test_logger.rb +51 -126
- data/lib/ruote/log/wait_logger.rb +8 -13
- data/lib/ruote/parser/ruby_dsl.rb +4 -4
- data/lib/ruote/parser.rb +2 -2
- data/lib/ruote/part/block_participant.rb +1 -1
- data/lib/ruote/part/engine_participant.rb +1 -1
- data/lib/ruote/part/storage_participant.rb +27 -28
- data/lib/ruote/part/template.rb +8 -3
- data/lib/ruote/receiver/base.rb +24 -6
- data/lib/ruote/storage/base.rb +76 -11
- data/lib/ruote/storage/fs_storage.rb +10 -0
- data/lib/ruote/storage/hash_storage.rb +19 -8
- data/lib/ruote/{part → svc}/dispatch_pool.rb +3 -2
- data/lib/ruote/svc/dollar_sub.rb +265 -0
- data/lib/ruote/{error_handler.rb → svc/error_handler.rb} +6 -1
- data/lib/ruote/{exp → svc}/expression_map.rb +31 -37
- data/lib/ruote/{part → svc}/participant_list.rb +165 -25
- data/lib/ruote/{evt → svc}/tracker.rb +0 -0
- data/lib/ruote/{util → svc}/treechecker.rb +0 -0
- data/lib/ruote/util/look.rb +4 -1
- data/lib/ruote/util/ometa.rb +21 -5
- data/lib/ruote/{subprocess.rb → util/subprocess.rb} +0 -0
- data/lib/ruote/version.rb +1 -1
- data/lib/ruote/worker.rb +29 -69
- data/lib/ruote/workitem.rb +28 -1
- data/ruote.gemspec +26 -22
- data/test/functional/base.rb +3 -0
- data/test/functional/concurrent_base.rb +1 -0
- data/test/functional/crunner.sh +1 -1
- data/test/functional/ct_0_concurrence.rb +6 -0
- data/test/functional/ct_1_iterator.rb +3 -0
- data/test/functional/ct_2_cancel.rb +5 -0
- data/test/functional/eft_13_iterator.rb +39 -4
- data/test/functional/eft_14_cursor.rb +39 -0
- data/test/functional/eft_30_ref.rb +140 -0
- data/test/functional/eft_3_participant.rb +25 -23
- data/test/functional/ft_10_dollar.rb +17 -1
- data/test/functional/ft_14_re_apply.rb +76 -0
- data/test/functional/ft_1_process_status.rb +170 -29
- data/test/functional/ft_20_storage_participant.rb +14 -0
- data/test/functional/ft_24_block_participants.rb +1 -1
- data/test/functional/ft_26_participant_timeout.rb +93 -0
- data/test/functional/ft_2_errors.rb +24 -17
- data/test/functional/ft_30_smtp_participant.rb +7 -2
- data/test/functional/ft_38_participant_more.rb +15 -0
- data/test/functional/ft_39_wait_for.rb +34 -1
- data/test/functional/ft_3_participant_registration.rb +270 -2
- data/test/functional/ft_40_wait_logger.rb +61 -0
- data/test/functional/ft_42_storage_copy.rb +4 -0
- data/test/functional/{ft_40_participant_on_reply.rb → ft_43_participant_on_reply.rb} +17 -0
- data/test/functional/ft_44_var_participant.rb +35 -0
- data/test/functional/ft_45_participant_accept.rb +64 -0
- data/test/functional/ft_46_launch_single.rb +49 -0
- data/test/functional/ft_5_on_error.rb +39 -1
- data/test/functional/storage_helper.rb +7 -1
- data/test/test_helper.rb +1 -1
- data/test/unit/storage.rb +105 -32
- data/test/unit/ut_0_ruby_parser.rb +31 -1
- data/test/unit/ut_16_parser.rb +20 -0
- data/test/unit/ut_19_part_template.rb +11 -1
- data/test/unit/ut_20_composite_storage.rb +1 -1
- data/test/unit/ut_4_expmap.rb +1 -1
- data/test/unit/ut_6_condition.rb +2 -2
- metadata +112 -74
- data/lib/ruote/exp/raw.rb +0 -44
- data/lib/ruote/util/dollar.rb +0 -193
data/lib/ruote/engine.rb
CHANGED
@@ -36,7 +36,7 @@ module Ruote
|
|
36
36
|
# issues with stalled processes or processes stuck in errors.
|
37
37
|
#
|
38
38
|
# NOTE : the methods #launch and #reply are implemented in
|
39
|
-
# Ruote::ReceiverMixin
|
39
|
+
# Ruote::ReceiverMixin (this Engine class has all the methods of a Receiver).
|
40
40
|
#
|
41
41
|
class Engine
|
42
42
|
|
@@ -45,43 +45,144 @@ module Ruote
|
|
45
45
|
attr_reader :context
|
46
46
|
attr_reader :variables
|
47
47
|
|
48
|
-
|
48
|
+
# Creates an engine using either worker or storage.
|
49
|
+
#
|
50
|
+
# If a storage instance is given as the first argument, the engine will be
|
51
|
+
# able to manage processes (for example, launch and cancel workflows) but
|
52
|
+
# will not actually run any workflows.
|
53
|
+
#
|
54
|
+
# If a worker instance is given as the first argument and the second
|
55
|
+
# argument is true, engine will start the worker and will be able to both
|
56
|
+
# manage and run workflows.
|
57
|
+
#
|
58
|
+
# If the second options is set to { :join => true }, the worker wil
|
59
|
+
# be started and run in the current thread.
|
60
|
+
#
|
61
|
+
def initialize (worker_or_storage, opts=true)
|
49
62
|
|
50
63
|
@context = worker_or_storage.context
|
51
64
|
@context.engine = self
|
52
65
|
|
53
66
|
@variables = EngineVariables.new(@context.storage)
|
54
67
|
|
55
|
-
|
56
|
-
|
68
|
+
if @context.worker
|
69
|
+
if opts == true
|
70
|
+
@context.worker.run_in_thread
|
71
|
+
# runs worker in its own thread
|
72
|
+
elsif opts == { :join => true }
|
73
|
+
@context.worker.run
|
74
|
+
# runs worker in current thread (and doesn't return)
|
75
|
+
#else
|
76
|
+
# worker is not run
|
77
|
+
end
|
78
|
+
#else
|
79
|
+
# no worker
|
80
|
+
end
|
57
81
|
end
|
58
82
|
|
83
|
+
# Returns the storage this engine works with passed at engine
|
84
|
+
# initialization.
|
85
|
+
#
|
59
86
|
def storage
|
60
87
|
|
61
88
|
@context.storage
|
62
89
|
end
|
63
90
|
|
91
|
+
# Returns the worker nested inside this engine (passed at initialization).
|
92
|
+
# Returns nil if this engine is only linked to a storage (and the worker
|
93
|
+
# is running somewhere else (hopefully)).
|
94
|
+
#
|
64
95
|
def worker
|
65
96
|
|
66
97
|
@context.worker
|
67
98
|
end
|
68
99
|
|
100
|
+
# Quick note : the implementation of launch is found in the module
|
101
|
+
# Ruote::ReceiverMixin that the engine includes.
|
102
|
+
#
|
103
|
+
# Some processes have to have one and only one instance of themselves
|
104
|
+
# running, these are called 'singles' ('singleton' is too object-oriented).
|
105
|
+
#
|
106
|
+
# When called, this method will check if an instance of the pdef is
|
107
|
+
# already running (it uses the process definition name attribute), if
|
108
|
+
# yes, it will return without having launched anything. If there is no
|
109
|
+
# such process running, it will launch it (and register it).
|
110
|
+
#
|
111
|
+
# Returns the wfid (workflow instance id) of the running single.
|
112
|
+
#
|
113
|
+
def launch_single (process_definition, fields={}, variables={})
|
114
|
+
|
115
|
+
tree = @context.parser.parse(process_definition)
|
116
|
+
name = tree[1]['name'] || (tree[1].find { |k, v| v.nil? } || []).first
|
117
|
+
|
118
|
+
raise ArgumentError.new(
|
119
|
+
'process definition is missing a name, cannot launch as single'
|
120
|
+
) unless name
|
121
|
+
|
122
|
+
singles = @context.storage.get('variables', 'singles') || {
|
123
|
+
'_id' => 'singles', 'type' => 'variables', 'h' => {}
|
124
|
+
}
|
125
|
+
wfid, timestamp = singles['h'][name]
|
126
|
+
|
127
|
+
if wfid && (timestamp + 1.0 < Time.now.to_f || process(wfid) != nil)
|
128
|
+
return wfid
|
129
|
+
end
|
130
|
+
# process is already running
|
131
|
+
|
132
|
+
wfid = @context.wfidgen.generate
|
133
|
+
|
134
|
+
singles['h'][name] = [ wfid, Time.now.to_f ]
|
135
|
+
|
136
|
+
r = @context.storage.put(singles)
|
137
|
+
|
138
|
+
return launch_single(tree, fields, variables) unless r.nil?
|
139
|
+
#
|
140
|
+
# the put failed, back to the start...
|
141
|
+
#
|
142
|
+
# all this to prevent races between multiple engines,
|
143
|
+
# multiple launch_single calls (from different Ruby runtimes)
|
144
|
+
|
145
|
+
# ... green for launch
|
146
|
+
|
147
|
+
@context.storage.put_msg(
|
148
|
+
'launch',
|
149
|
+
'wfid' => wfid,
|
150
|
+
'tree' => tree,
|
151
|
+
'workitem' => { 'fields' => fields },
|
152
|
+
'variables' => variables)
|
153
|
+
|
154
|
+
wfid
|
155
|
+
end
|
156
|
+
|
157
|
+
# Given a process identifier (wfid), cancels this process.
|
158
|
+
#
|
69
159
|
def cancel_process (wfid)
|
70
160
|
|
71
161
|
@context.storage.put_msg('cancel_process', 'wfid' => wfid)
|
72
162
|
end
|
73
163
|
|
164
|
+
# Given a process identifier (wfid), kills this process. Killing is
|
165
|
+
# equivalent to cancelling, but when killing, :on_cancel attributes
|
166
|
+
# are not triggered.
|
167
|
+
#
|
74
168
|
def kill_process (wfid)
|
75
169
|
|
76
170
|
@context.storage.put_msg('kill_process', 'wfid' => wfid)
|
77
171
|
end
|
78
172
|
|
173
|
+
# Cancels a segment of process instance. Since expressions are nodes in
|
174
|
+
# processes instances, cancelling an expression, will cancel the expression
|
175
|
+
# and all its children (the segment of process).
|
176
|
+
#
|
79
177
|
def cancel_expression (fei)
|
80
178
|
|
81
179
|
fei = fei.to_h if fei.respond_to?(:to_h)
|
82
180
|
@context.storage.put_msg('cancel', 'fei' => fei)
|
83
181
|
end
|
84
182
|
|
183
|
+
# Like #cancel_expression, but :on_cancel attributes (of the expressions)
|
184
|
+
# are not triggered.
|
185
|
+
#
|
85
186
|
def kill_expression (fei)
|
86
187
|
|
87
188
|
fei = fei.to_h if fei.respond_to?(:to_h)
|
@@ -142,41 +243,94 @@ module Ruote
|
|
142
243
|
#
|
143
244
|
def process (wfid)
|
144
245
|
|
145
|
-
|
146
|
-
errs = self.errors( wfid )
|
147
|
-
|
148
|
-
return nil if exps.empty? && errs.empty?
|
149
|
-
|
150
|
-
ProcessStatus.new(@context, exps, errs)
|
246
|
+
list_processes([ wfid ], {}).first
|
151
247
|
end
|
152
248
|
|
153
249
|
# Returns an array of ProcessStatus instances.
|
154
250
|
#
|
155
|
-
# WARNING : this is an expensive operation
|
251
|
+
# WARNING : this is an expensive operation, but it understands :skip
|
252
|
+
# and :limit, so pagination is our friend.
|
253
|
+
#
|
254
|
+
# Please note, if you're interested only in processes that have errors,
|
255
|
+
# Engine#errors is a more efficient means.
|
156
256
|
#
|
157
|
-
|
257
|
+
# To simply list the wfids of the currently running, Engine#process_wfids
|
258
|
+
# is way cheaper to call.
|
259
|
+
#
|
260
|
+
def processes (opts={})
|
158
261
|
|
159
|
-
|
160
|
-
errs = self.errors
|
262
|
+
wfids = nil
|
161
263
|
|
162
|
-
|
264
|
+
if opts.size > 0
|
163
265
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
errs.each do |err|
|
168
|
-
(by_wfid[err['msg']['fei']['wfid']] ||= [ [], [] ]).last << err
|
266
|
+
wfids = @context.storage.expression_wfids(opts)
|
267
|
+
|
268
|
+
return wfids.size if opts[:count]
|
169
269
|
end
|
170
270
|
|
171
|
-
|
271
|
+
list_processes(wfids, opts)
|
172
272
|
end
|
173
273
|
|
174
274
|
# Returns an array of current errors (hashes)
|
175
275
|
#
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
276
|
+
# Can be called in two ways :
|
277
|
+
#
|
278
|
+
# engine.errors(wfid)
|
279
|
+
#
|
280
|
+
# and
|
281
|
+
#
|
282
|
+
# engine.errors(:skip => 100, :limit => 100)
|
283
|
+
#
|
284
|
+
def errors (wfid=nil)
|
285
|
+
|
286
|
+
wfid, options = wfid.is_a?(Hash) ? [ nil, wfid ] : [ wfid, {} ]
|
287
|
+
|
288
|
+
errs = wfid.nil? ?
|
289
|
+
@context.storage.get_many('errors', nil, options) :
|
290
|
+
@context.storage.get_many('errors', wfid)
|
291
|
+
|
292
|
+
return errs if options[:count]
|
293
|
+
|
294
|
+
errs.collect { |err| ProcessError.new(err) }
|
295
|
+
end
|
296
|
+
|
297
|
+
# Returns an array of schedules. Those schedules are open structs
|
298
|
+
# with various properties, like target, owner, at, put_at, ...
|
299
|
+
#
|
300
|
+
# Introduced mostly for ruote-kit.
|
301
|
+
#
|
302
|
+
# Can be called in two ways :
|
303
|
+
#
|
304
|
+
# engine.schedules(wfid)
|
305
|
+
#
|
306
|
+
# and
|
307
|
+
#
|
308
|
+
# engine.schedules(:skip => 100, :limit => 100)
|
309
|
+
#
|
310
|
+
def schedules (wfid=nil)
|
311
|
+
|
312
|
+
wfid, options = wfid.is_a?(Hash) ? [ nil, wfid ] : [ wfid, {} ]
|
313
|
+
|
314
|
+
scheds = wfid.nil? ?
|
315
|
+
@context.storage.get_many('schedules', nil, options) :
|
316
|
+
@context.storage.get_many('schedules', /!#{wfid}-\d+$/)
|
317
|
+
|
318
|
+
return scheds if options[:count]
|
319
|
+
|
320
|
+
scheds.collect { |sched| Ruote.schedule_to_h(sched) }
|
321
|
+
end
|
322
|
+
|
323
|
+
# Returns a [sorted] list of wfids of the process instances currently
|
324
|
+
# running in the engine.
|
325
|
+
#
|
326
|
+
# This operation is substantially less costly than Engine#processes (though
|
327
|
+
# the 'how substantially' depends on the storage chosen).
|
328
|
+
#
|
329
|
+
def process_wfids
|
330
|
+
|
331
|
+
@context.storage.ids('expressions').collect { |sfei|
|
332
|
+
sfei.split('!').last
|
333
|
+
}.uniq.sort
|
180
334
|
end
|
181
335
|
|
182
336
|
# Shuts down the engine, mostly passes the shutdown message to the other
|
@@ -187,9 +341,13 @@ module Ruote
|
|
187
341
|
@context.shutdown
|
188
342
|
end
|
189
343
|
|
190
|
-
# This method expects there
|
344
|
+
# This method expects there to be a logger with a wait_for method in the
|
191
345
|
# context, else it will raise an exception.
|
192
346
|
#
|
347
|
+
# *WARNING* : wait_for() is meant for environments where there is a unique
|
348
|
+
# worker and that worker is nested in this engine. In a multiple worker
|
349
|
+
# environment wait_for doesn't see events handled by 'other' workers.
|
350
|
+
#
|
193
351
|
# This method is only useful for test/quickstart/examples environments.
|
194
352
|
#
|
195
353
|
# engine.wait_for(:alpha)
|
@@ -224,6 +382,14 @@ module Ruote
|
|
224
382
|
logger.wait_for(items)
|
225
383
|
end
|
226
384
|
|
385
|
+
# Joins the worker thread. If this engine has no nested worker, calling
|
386
|
+
# this method will simply return immediately.
|
387
|
+
#
|
388
|
+
def join
|
389
|
+
|
390
|
+
worker.join if worker
|
391
|
+
end
|
392
|
+
|
227
393
|
# Loads and parses the process definition at the given path.
|
228
394
|
#
|
229
395
|
def load_definition (path)
|
@@ -293,11 +459,27 @@ module Ruote
|
|
293
459
|
# end
|
294
460
|
# end
|
295
461
|
#
|
296
|
-
# engine.register_participant
|
462
|
+
# engine.register_participant(
|
463
|
+
# 'moon', MyStatelessParticipant, 'name' => 'saturn5')
|
297
464
|
#
|
298
465
|
# Remember that the options (the hash that follows the class name), must be
|
299
466
|
# serialisable via JSON.
|
300
467
|
#
|
468
|
+
#
|
469
|
+
# == require_path and load_path
|
470
|
+
#
|
471
|
+
# It's OK to register a participant by passing its full classname as a
|
472
|
+
# String.
|
473
|
+
#
|
474
|
+
# engine.register_participant(
|
475
|
+
# 'auditor', 'AuditParticipant', 'require_path' => 'part/audit.rb')
|
476
|
+
# engine.register_participant(
|
477
|
+
# 'auto_decision', 'DecParticipant', 'load_path' => 'part/dec.rb')
|
478
|
+
#
|
479
|
+
# Note the option load_path / require_path that point to the ruby file
|
480
|
+
# containing the participant implementation. 'require' will load and eval
|
481
|
+
# the ruby code only once, 'load' each time.
|
482
|
+
#
|
301
483
|
def register_participant (regex, participant=nil, opts={}, &block)
|
302
484
|
|
303
485
|
pa = @context.plist.register(regex, participant, opts, block)
|
@@ -310,6 +492,30 @@ module Ruote
|
|
310
492
|
pa
|
311
493
|
end
|
312
494
|
|
495
|
+
# A shorter version of #register_participant
|
496
|
+
#
|
497
|
+
# engine.register 'alice', MailParticipant, :target => 'alice@example.com'
|
498
|
+
#
|
499
|
+
# or a block registering mechanism.
|
500
|
+
#
|
501
|
+
# engine.register do
|
502
|
+
# alpha 'Participants::Alpha', 'flavour' => 'vanilla'
|
503
|
+
# participant 'bravo', 'Participants::Bravo', :flavour => 'peach'
|
504
|
+
# catchall ParticipantCharlie, 'flavour' => 'coconut'
|
505
|
+
# end
|
506
|
+
#
|
507
|
+
# Originally implemented in ruote-kit by Torsten Schoenebaum.
|
508
|
+
#
|
509
|
+
def register (*args, &block)
|
510
|
+
|
511
|
+
if args.size > 0
|
512
|
+
register_participant(*args, &block)
|
513
|
+
else
|
514
|
+
proxy = ParticipantRegistrationProxy.new(self)
|
515
|
+
block.arity < 1 ? proxy.instance_eval(&block) : block.call(proxy)
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
313
519
|
# Removes/unregisters a participant from the engine.
|
314
520
|
#
|
315
521
|
def unregister_participant (name_or_participant)
|
@@ -323,6 +529,55 @@ module Ruote
|
|
323
529
|
'regex' => re.to_s)
|
324
530
|
end
|
325
531
|
|
532
|
+
alias :unregister :unregister_participant
|
533
|
+
|
534
|
+
# Returns a list of Ruote::ParticipantEntry instances.
|
535
|
+
#
|
536
|
+
# engine.register_participant :alpha, MyParticipant, 'message' => 'hello'
|
537
|
+
#
|
538
|
+
# # interrogate participant list
|
539
|
+
# #
|
540
|
+
# list = engine.participant_list
|
541
|
+
# participant = list.first
|
542
|
+
# p participant.regex
|
543
|
+
# # => "^alpha$"
|
544
|
+
# p participant.classname
|
545
|
+
# # => "MyParticipant"
|
546
|
+
# p participant.options
|
547
|
+
# # => {"message"=>"hello"}
|
548
|
+
#
|
549
|
+
# # update participant list
|
550
|
+
# #
|
551
|
+
# participant.regex = '^alfred$'
|
552
|
+
# engine.participant_list = list
|
553
|
+
#
|
554
|
+
def participant_list
|
555
|
+
|
556
|
+
@context.plist.list
|
557
|
+
end
|
558
|
+
|
559
|
+
# Accepts a list of Ruote::ParticipantEntry instances.
|
560
|
+
#
|
561
|
+
# See Engine#participant_list
|
562
|
+
#
|
563
|
+
def participant_list= (pl)
|
564
|
+
|
565
|
+
@context.plist.list = pl
|
566
|
+
end
|
567
|
+
|
568
|
+
# A convenience method for
|
569
|
+
#
|
570
|
+
# sp = Ruote::StorageParticipant.new(engine)
|
571
|
+
#
|
572
|
+
# simply do
|
573
|
+
#
|
574
|
+
# sp = engine.storage_participant
|
575
|
+
#
|
576
|
+
def storage_participant
|
577
|
+
|
578
|
+
@storage_participant ||= Ruote::StorageParticipant.new(self)
|
579
|
+
end
|
580
|
+
|
326
581
|
# Adds a service locally (will not get propagated to other workers).
|
327
582
|
#
|
328
583
|
# tracer = Tracer.new
|
@@ -373,6 +628,64 @@ module Ruote
|
|
373
628
|
|
374
629
|
Ruote::Workitem.new(fexp.h.applied_workitem)
|
375
630
|
end
|
631
|
+
|
632
|
+
# A debug helper :
|
633
|
+
#
|
634
|
+
# engine.noisy = true
|
635
|
+
#
|
636
|
+
# will let the engine (in fact the worker) pour all the details of the
|
637
|
+
# executing process instances to STDOUT.
|
638
|
+
#
|
639
|
+
def noisy= (b)
|
640
|
+
|
641
|
+
@context.logger.noisy = b
|
642
|
+
end
|
643
|
+
|
644
|
+
protected
|
645
|
+
|
646
|
+
# Used by #process and #processes
|
647
|
+
#
|
648
|
+
def list_processes (wfids, opts)
|
649
|
+
|
650
|
+
swfids = wfids ? wfids.collect { |wfid| /!#{wfid}-\d+$/ } : nil
|
651
|
+
|
652
|
+
exps = @context.storage.get_many('expressions', wfids)
|
653
|
+
swis = @context.storage.get_many('workitems', wfids)
|
654
|
+
errs = @context.storage.get_many('errors', wfids)
|
655
|
+
schs = @context.storage.get_many('schedules', swfids)
|
656
|
+
|
657
|
+
errs = errs.collect { |err| ProcessError.new(err) }
|
658
|
+
schs = schs.collect { |sch| Ruote.schedule_to_h(sch) }
|
659
|
+
|
660
|
+
by_wfid = {}
|
661
|
+
|
662
|
+
exps.each do |exp|
|
663
|
+
(by_wfid[exp['fei']['wfid']] ||= [ [], [], [], [] ])[0] << exp
|
664
|
+
end
|
665
|
+
swis.each do |swi|
|
666
|
+
(by_wfid[swi['fei']['wfid']] ||= [ [], [], [], [] ])[1] << swi
|
667
|
+
end
|
668
|
+
errs.each do |err|
|
669
|
+
(by_wfid[err.wfid] ||= [ [], [], [], [] ])[2] << err
|
670
|
+
end
|
671
|
+
schs.each do |sch|
|
672
|
+
(by_wfid[sch['wfid']] ||= [ [], [], [], [] ])[3] << sch
|
673
|
+
end
|
674
|
+
|
675
|
+
wfids = if wfids
|
676
|
+
wfids
|
677
|
+
else
|
678
|
+
wfids = by_wfid.keys.sort
|
679
|
+
wfids = wfids.reverse if opts[:descending]
|
680
|
+
wfids
|
681
|
+
end
|
682
|
+
|
683
|
+
wfids.inject([]) { |a, wfid|
|
684
|
+
info = by_wfid[wfid]
|
685
|
+
a << ProcessStatus.new(@context, *info) if info
|
686
|
+
a
|
687
|
+
}
|
688
|
+
end
|
376
689
|
end
|
377
690
|
|
378
691
|
#
|
@@ -398,5 +711,59 @@ module Ruote
|
|
398
711
|
@storage.put_engine_variable(k, v)
|
399
712
|
end
|
400
713
|
end
|
714
|
+
|
715
|
+
#
|
716
|
+
# Engine#register uses this proxy when it's passed a block.
|
717
|
+
#
|
718
|
+
# Originally written by Torsten Schoenebaum for ruote-kit.
|
719
|
+
#
|
720
|
+
class ParticipantRegistrationProxy
|
721
|
+
|
722
|
+
def initialize (engine)
|
723
|
+
|
724
|
+
@engine = engine
|
725
|
+
end
|
726
|
+
|
727
|
+
def participant (name, klass, options={})
|
728
|
+
|
729
|
+
@engine.register_participant(name, klass, options)
|
730
|
+
end
|
731
|
+
|
732
|
+
def catchall (*args)
|
733
|
+
|
734
|
+
klass = args.empty? ? Ruote::StorageParticipant : args.first
|
735
|
+
options = args[1] || {}
|
736
|
+
|
737
|
+
participant('.+', klass, options)
|
738
|
+
end
|
739
|
+
|
740
|
+
# Maybe a bit audacious...
|
741
|
+
#
|
742
|
+
def method_missing (method_name, *args)
|
743
|
+
|
744
|
+
participant(method_name, *args)
|
745
|
+
end
|
746
|
+
end
|
747
|
+
|
748
|
+
# Refines a schedule as found in the ruote storage into something a bit
|
749
|
+
# easier to present.
|
750
|
+
#
|
751
|
+
def self.schedule_to_h (sched)
|
752
|
+
|
753
|
+
h = sched.dup
|
754
|
+
|
755
|
+
h.delete('_rev')
|
756
|
+
h.delete('type')
|
757
|
+
msg = h.delete('msg')
|
758
|
+
owner = h.delete('owner')
|
759
|
+
|
760
|
+
h['wfid'] = owner['wfid']
|
761
|
+
h['action'] = msg['action']
|
762
|
+
h['type'] = msg['flavour']
|
763
|
+
h['owner'] = Ruote::FlowExpressionId.new(owner)
|
764
|
+
h['target'] = Ruote::FlowExpressionId.new(msg['fei'])
|
765
|
+
|
766
|
+
h
|
767
|
+
end
|
401
768
|
end
|
402
769
|
|
data/lib/ruote/exp/command.rb
CHANGED
@@ -99,16 +99,24 @@ module Ruote::Exp
|
|
99
99
|
#
|
100
100
|
# === :merge_type
|
101
101
|
#
|
102
|
+
# ==== :override
|
103
|
+
#
|
102
104
|
# By default, the merge type is set to 'override', which means that the
|
103
105
|
# 'winning' workitem's payload supplants all other workitems' payloads.
|
104
106
|
#
|
107
|
+
# ==== :mix
|
108
|
+
#
|
105
109
|
# Setting :merge_type to :mix, will actually attempt to merge field by field,
|
106
110
|
# making sure that the field value of the winner(s) are used.
|
107
111
|
#
|
112
|
+
# ==== :isolate
|
113
|
+
#
|
108
114
|
# :isolate will rearrange the resulting workitem payload so that there is
|
109
115
|
# a new field for each branch. The name of each field is the index of the
|
110
116
|
# branch from '0' to ...
|
111
117
|
#
|
118
|
+
# ==== :stack
|
119
|
+
#
|
112
120
|
# :stack will stack the workitems coming back from the concurrence branches
|
113
121
|
# in an array whose order is determined by the :merge attributes. The array
|
114
122
|
# is placed in the 'stack' field of the resulting workitem.
|
@@ -32,6 +32,9 @@ module Ruote::Exp
|
|
32
32
|
#
|
33
33
|
# This expression is a cross between 'concurrence' and 'iterator'.
|
34
34
|
#
|
35
|
+
# Please look at the documentation of 'iterator' to learn more about the
|
36
|
+
# common options between 'iterator' and 'concurrent-iterator'.
|
37
|
+
#
|
35
38
|
# pdef = Ruote.process_definition :name => 'test' do
|
36
39
|
# concurrent_iterator :on_val => 'alice, bob, charly', :to_var => 'v' do
|
37
40
|
# participant '${v:v}'
|
data/lib/ruote/exp/fe_cursor.rb
CHANGED
@@ -66,7 +66,7 @@ module Ruote::Exp
|
|
66
66
|
# publisher
|
67
67
|
# end
|
68
68
|
#
|
69
|
-
# === break
|
69
|
+
# === stop, over & break
|
70
70
|
#
|
71
71
|
# Exits the cursor.
|
72
72
|
#
|
@@ -74,10 +74,12 @@ module Ruote::Exp
|
|
74
74
|
# author
|
75
75
|
# reviewer
|
76
76
|
# rewind :if => '${f:review} == fix'
|
77
|
-
#
|
77
|
+
# stop :if => '${f:review} == abort'
|
78
78
|
# publisher
|
79
79
|
# end
|
80
80
|
#
|
81
|
+
# '_break' or 'over' can be used instead of 'stop'.
|
82
|
+
#
|
81
83
|
# === skip & back
|
82
84
|
#
|
83
85
|
# Those two commands jump forth and back respectively. By default, they
|
@@ -159,6 +161,44 @@ module Ruote::Exp
|
|
159
161
|
# cursor, but it will break the main 'cursor' (and thus break the whole
|
160
162
|
# review process).
|
161
163
|
#
|
164
|
+
# == cursor command in the workitem
|
165
|
+
#
|
166
|
+
# The command expressions are merely setting the workitem field '__command__'
|
167
|
+
# with an array value [ {command}, {arg} ].
|
168
|
+
#
|
169
|
+
# For example,
|
170
|
+
#
|
171
|
+
# jump :to => 'author'
|
172
|
+
# # is equivalent to
|
173
|
+
# set 'field:__command__' => 'author'
|
174
|
+
#
|
175
|
+
# It is entirely OK to have a participant implementation that sets __command__
|
176
|
+
# by itself.
|
177
|
+
#
|
178
|
+
# class Reviewer
|
179
|
+
# include Ruote::LocalParticipant
|
180
|
+
#
|
181
|
+
# def consume (workitem)
|
182
|
+
# # somehow review the book
|
183
|
+
# if review == 'bad'
|
184
|
+
# #workitem.fields['__command__'] = [ 'rewind' ] # old style
|
185
|
+
# workitem.command = 'rewind' # new style
|
186
|
+
# else
|
187
|
+
# # let it go
|
188
|
+
# end
|
189
|
+
# reply_to_engine(workitem)
|
190
|
+
# end
|
191
|
+
#
|
192
|
+
# def cancel (fei, flavour)
|
193
|
+
# # cancel if review is still going on...
|
194
|
+
# end
|
195
|
+
# end
|
196
|
+
#
|
197
|
+
# This example uses the Ruote::Workitem#command= method which can be fed
|
198
|
+
# strings like 'rewind', 'skip 2', 'jump to author' or the equivalent arrays
|
199
|
+
# [ 'rewind' ], [ 'skip', 2 ], [ 'jump', 'author' ].
|
200
|
+
#
|
201
|
+
#
|
162
202
|
# == :break_if / :rewind_if
|
163
203
|
#
|
164
204
|
# As an attribute of the cursor/repeat expression, you can set a :break_if.
|
@@ -208,6 +248,8 @@ module Ruote::Exp
|
|
208
248
|
|
209
249
|
protected
|
210
250
|
|
251
|
+
# Determines which child expression of the cursor is to be applied next.
|
252
|
+
#
|
211
253
|
def move_on (workitem=h.applied_workitem)
|
212
254
|
|
213
255
|
position = workitem['fei'] == h.fei ?
|
@@ -234,6 +276,8 @@ module Ruote::Exp
|
|
234
276
|
end
|
235
277
|
end
|
236
278
|
|
279
|
+
# Will return true if this instance is about a 'loop' or a 'repeat'.
|
280
|
+
#
|
237
281
|
def is_loop?
|
238
282
|
|
239
283
|
name == 'loop' || name == 'repeat'
|
@@ -254,8 +298,8 @@ module Ruote::Exp
|
|
254
298
|
ref = c[1]['ref']
|
255
299
|
tag = c[1]['tag']
|
256
300
|
|
257
|
-
ref =
|
258
|
-
tag =
|
301
|
+
ref = @context.dollar_sub.s(ref, self, workitem) if ref
|
302
|
+
tag = @context.dollar_sub.s(tag, self, workitem) if tag
|
259
303
|
|
260
304
|
next if exp_name != arg && ref != arg && tag != arg
|
261
305
|
|