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