ruote-maestrodev 2.2.1
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 +290 -0
- data/CREDITS.txt +99 -0
- data/LICENSE.txt +21 -0
- data/README.rdoc +88 -0
- data/Rakefile +108 -0
- data/TODO.txt +488 -0
- data/lib/ruote.rb +7 -0
- data/lib/ruote/context.rb +194 -0
- data/lib/ruote/engine.rb +1062 -0
- data/lib/ruote/engine/process_error.rb +122 -0
- data/lib/ruote/engine/process_status.rb +448 -0
- data/lib/ruote/exp/command.rb +87 -0
- data/lib/ruote/exp/commanded.rb +69 -0
- data/lib/ruote/exp/condition.rb +227 -0
- data/lib/ruote/exp/fe_add_branches.rb +138 -0
- data/lib/ruote/exp/fe_apply.rb +154 -0
- data/lib/ruote/exp/fe_cancel_process.rb +78 -0
- data/lib/ruote/exp/fe_command.rb +156 -0
- data/lib/ruote/exp/fe_concurrence.rb +321 -0
- data/lib/ruote/exp/fe_concurrent_iterator.rb +219 -0
- data/lib/ruote/exp/fe_cron.rb +141 -0
- data/lib/ruote/exp/fe_cursor.rb +324 -0
- data/lib/ruote/exp/fe_define.rb +112 -0
- data/lib/ruote/exp/fe_echo.rb +60 -0
- data/lib/ruote/exp/fe_equals.rb +115 -0
- data/lib/ruote/exp/fe_error.rb +82 -0
- data/lib/ruote/exp/fe_filter.rb +648 -0
- data/lib/ruote/exp/fe_forget.rb +88 -0
- data/lib/ruote/exp/fe_given.rb +154 -0
- data/lib/ruote/exp/fe_if.rb +127 -0
- data/lib/ruote/exp/fe_inc.rb +205 -0
- data/lib/ruote/exp/fe_iterator.rb +234 -0
- data/lib/ruote/exp/fe_let.rb +75 -0
- data/lib/ruote/exp/fe_listen.rb +304 -0
- data/lib/ruote/exp/fe_lose.rb +110 -0
- data/lib/ruote/exp/fe_noop.rb +45 -0
- data/lib/ruote/exp/fe_once.rb +215 -0
- data/lib/ruote/exp/fe_participant.rb +287 -0
- data/lib/ruote/exp/fe_read.rb +69 -0
- data/lib/ruote/exp/fe_redo.rb +82 -0
- data/lib/ruote/exp/fe_ref.rb +152 -0
- data/lib/ruote/exp/fe_registerp.rb +110 -0
- data/lib/ruote/exp/fe_reserve.rb +126 -0
- data/lib/ruote/exp/fe_restore.rb +102 -0
- data/lib/ruote/exp/fe_save.rb +72 -0
- data/lib/ruote/exp/fe_sequence.rb +59 -0
- data/lib/ruote/exp/fe_set.rb +154 -0
- data/lib/ruote/exp/fe_subprocess.rb +211 -0
- data/lib/ruote/exp/fe_that.rb +92 -0
- data/lib/ruote/exp/fe_undo.rb +67 -0
- data/lib/ruote/exp/fe_unregisterp.rb +69 -0
- data/lib/ruote/exp/fe_wait.rb +95 -0
- data/lib/ruote/exp/flowexpression.rb +886 -0
- data/lib/ruote/exp/iterator.rb +81 -0
- data/lib/ruote/exp/merge.rb +118 -0
- data/lib/ruote/exp/ro_attributes.rb +212 -0
- data/lib/ruote/exp/ro_filters.rb +136 -0
- data/lib/ruote/exp/ro_persist.rb +154 -0
- data/lib/ruote/exp/ro_variables.rb +189 -0
- data/lib/ruote/exp/ro_vf.rb +68 -0
- data/lib/ruote/fei.rb +260 -0
- data/lib/ruote/id/mnemo_wfid_generator.rb +43 -0
- data/lib/ruote/id/wfid_generator.rb +81 -0
- data/lib/ruote/log/default_history.rb +122 -0
- data/lib/ruote/log/pretty.rb +176 -0
- data/lib/ruote/log/storage_history.rb +159 -0
- data/lib/ruote/log/test_logger.rb +208 -0
- data/lib/ruote/log/wait_logger.rb +64 -0
- data/lib/ruote/part/block_participant.rb +137 -0
- data/lib/ruote/part/code_participant.rb +81 -0
- data/lib/ruote/part/engine_participant.rb +189 -0
- data/lib/ruote/part/local_participant.rb +138 -0
- data/lib/ruote/part/no_op_participant.rb +60 -0
- data/lib/ruote/part/null_participant.rb +54 -0
- data/lib/ruote/part/rev_participant.rb +169 -0
- data/lib/ruote/part/smtp_participant.rb +116 -0
- data/lib/ruote/part/storage_participant.rb +392 -0
- data/lib/ruote/part/template.rb +84 -0
- data/lib/ruote/participant.rb +7 -0
- data/lib/ruote/reader.rb +278 -0
- data/lib/ruote/reader/json.rb +49 -0
- data/lib/ruote/reader/radial.rb +290 -0
- data/lib/ruote/reader/ruby_dsl.rb +186 -0
- data/lib/ruote/reader/xml.rb +99 -0
- data/lib/ruote/receiver/base.rb +212 -0
- data/lib/ruote/storage/base.rb +364 -0
- data/lib/ruote/storage/composite_storage.rb +121 -0
- data/lib/ruote/storage/fs_storage.rb +139 -0
- data/lib/ruote/storage/hash_storage.rb +211 -0
- data/lib/ruote/svc/dispatch_pool.rb +158 -0
- data/lib/ruote/svc/dollar_sub.rb +298 -0
- data/lib/ruote/svc/error_handler.rb +138 -0
- data/lib/ruote/svc/expression_map.rb +97 -0
- data/lib/ruote/svc/participant_list.rb +397 -0
- data/lib/ruote/svc/tracker.rb +172 -0
- data/lib/ruote/svc/treechecker.rb +141 -0
- data/lib/ruote/tree_dot.rb +85 -0
- data/lib/ruote/util/filter.rb +525 -0
- data/lib/ruote/util/hashdot.rb +79 -0
- data/lib/ruote/util/look.rb +128 -0
- data/lib/ruote/util/lookup.rb +127 -0
- data/lib/ruote/util/misc.rb +167 -0
- data/lib/ruote/util/ometa.rb +71 -0
- data/lib/ruote/util/serializer.rb +103 -0
- data/lib/ruote/util/subprocess.rb +88 -0
- data/lib/ruote/util/time.rb +100 -0
- data/lib/ruote/util/tree.rb +58 -0
- data/lib/ruote/version.rb +29 -0
- data/lib/ruote/worker.rb +386 -0
- data/lib/ruote/workitem.rb +394 -0
- data/phil.txt +14 -0
- data/ruote.gemspec +44 -0
- data/test/bm/ci.rb +55 -0
- data/test/bm/ici.rb +71 -0
- data/test/bm/juuman.rb +54 -0
- data/test/bm/launch_bench.rb +37 -0
- data/test/bm/load_26c.rb +97 -0
- data/test/bm/mega.rb +64 -0
- data/test/bm/seq_thousand.rb +31 -0
- data/test/bm/t.rb +35 -0
- data/test/functional/base.rb +247 -0
- data/test/functional/concurrent_base.rb +98 -0
- data/test/functional/crunner.rb +31 -0
- data/test/functional/ct_0_concurrence.rb +65 -0
- data/test/functional/ct_1_iterator.rb +67 -0
- data/test/functional/ct_2_cancel.rb +81 -0
- data/test/functional/eft_0_process_definition.rb +65 -0
- data/test/functional/eft_10_cancel_process.rb +46 -0
- data/test/functional/eft_11_wait.rb +109 -0
- data/test/functional/eft_12_listen.rb +500 -0
- data/test/functional/eft_13_iterator.rb +342 -0
- data/test/functional/eft_14_cursor.rb +456 -0
- data/test/functional/eft_15_loop.rb +69 -0
- data/test/functional/eft_16_if.rb +183 -0
- data/test/functional/eft_17_equals.rb +55 -0
- data/test/functional/eft_18_concurrent_iterator.rb +410 -0
- data/test/functional/eft_19_reserve.rb +136 -0
- data/test/functional/eft_1_echo.rb +68 -0
- data/test/functional/eft_20_save.rb +116 -0
- data/test/functional/eft_21_restore.rb +61 -0
- data/test/functional/eft_22_noop.rb +28 -0
- data/test/functional/eft_23_apply.rb +168 -0
- data/test/functional/eft_24_add_branches.rb +98 -0
- data/test/functional/eft_25_command.rb +28 -0
- data/test/functional/eft_26_error.rb +77 -0
- data/test/functional/eft_27_inc.rb +280 -0
- data/test/functional/eft_28_once.rb +135 -0
- data/test/functional/eft_29_cron.rb +64 -0
- data/test/functional/eft_2_sequence.rb +58 -0
- data/test/functional/eft_30_ref.rb +155 -0
- data/test/functional/eft_31_registerp.rb +130 -0
- data/test/functional/eft_32_lose.rb +93 -0
- data/test/functional/eft_33_let.rb +31 -0
- data/test/functional/eft_34_given.rb +123 -0
- data/test/functional/eft_35_filter.rb +375 -0
- data/test/functional/eft_36_read.rb +95 -0
- data/test/functional/eft_3_participant.rb +149 -0
- data/test/functional/eft_4_set.rb +296 -0
- data/test/functional/eft_5_subprocess.rb +163 -0
- data/test/functional/eft_6_concurrence.rb +304 -0
- data/test/functional/eft_7_forget.rb +61 -0
- data/test/functional/eft_8_undo.rb +114 -0
- data/test/functional/eft_9_redo.rb +138 -0
- data/test/functional/ft_0_worker.rb +65 -0
- data/test/functional/ft_10_dollar.rb +304 -0
- data/test/functional/ft_11_recursion.rb +109 -0
- data/test/functional/ft_12_launchitem.rb +43 -0
- data/test/functional/ft_13_variables.rb +151 -0
- data/test/functional/ft_14_re_apply.rb +324 -0
- data/test/functional/ft_15_timeout.rb +226 -0
- data/test/functional/ft_16_participant_params.rb +98 -0
- data/test/functional/ft_17_conditional.rb +102 -0
- data/test/functional/ft_18_kill.rb +138 -0
- data/test/functional/ft_19_participant_code.rb +67 -0
- data/test/functional/ft_1_process_status.rb +796 -0
- data/test/functional/ft_20_storage_participant.rb +543 -0
- data/test/functional/ft_21_forget.rb +153 -0
- data/test/functional/ft_22_process_definitions.rb +90 -0
- data/test/functional/ft_23_load_defs.rb +79 -0
- data/test/functional/ft_24_block_participant.rb +235 -0
- data/test/functional/ft_25_receiver.rb +207 -0
- data/test/functional/ft_26_participant_rtimeout.rb +179 -0
- data/test/functional/ft_27_var_indirection.rb +128 -0
- data/test/functional/ft_28_null_noop_participants.rb +51 -0
- data/test/functional/ft_29_part_template.rb +60 -0
- data/test/functional/ft_2_errors.rb +380 -0
- data/test/functional/ft_30_smtp_participant.rb +122 -0
- data/test/functional/ft_31_part_blocking.rb +72 -0
- data/test/functional/ft_33_participant_subprocess_priority.rb +32 -0
- data/test/functional/ft_34_cursor_rewind.rb +101 -0
- data/test/functional/ft_35_add_service.rb +56 -0
- data/test/functional/ft_36_storage_history.rb +150 -0
- data/test/functional/ft_37_default_history.rb +109 -0
- data/test/functional/ft_38_participant_more.rb +193 -0
- data/test/functional/ft_39_wait_for.rb +136 -0
- data/test/functional/ft_3_participant_registration.rb +574 -0
- data/test/functional/ft_40_wait_logger.rb +62 -0
- data/test/functional/ft_41_participants.rb +91 -0
- data/test/functional/ft_42_storage_copy.rb +71 -0
- data/test/functional/ft_43_participant_on_reply.rb +87 -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 +83 -0
- data/test/functional/ft_47_wfid_generator.rb +54 -0
- data/test/functional/ft_48_lose.rb +112 -0
- data/test/functional/ft_49_engine_on_error.rb +201 -0
- data/test/functional/ft_4_cancel.rb +132 -0
- data/test/functional/ft_50_engine_config.rb +22 -0
- data/test/functional/ft_51_misc.rb +67 -0
- data/test/functional/ft_52_case.rb +134 -0
- data/test/functional/ft_53_engine_on_terminate.rb +95 -0
- data/test/functional/ft_54_patterns.rb +104 -0
- data/test/functional/ft_55_engine_participant.rb +303 -0
- data/test/functional/ft_56_filter_attribute.rb +259 -0
- data/test/functional/ft_57_rev_participant.rb +252 -0
- data/test/functional/ft_58_workitem.rb +69 -0
- data/test/functional/ft_59_pause.rb +343 -0
- data/test/functional/ft_5_on_error.rb +384 -0
- data/test/functional/ft_60_code_participant.rb +45 -0
- data/test/functional/ft_61_trailing_fields.rb +34 -0
- data/test/functional/ft_62_exp_name_and_dollar_substitution.rb +35 -0
- data/test/functional/ft_6_on_cancel.rb +221 -0
- data/test/functional/ft_7_tags.rb +177 -0
- data/test/functional/ft_8_participant_consumption.rb +124 -0
- data/test/functional/ft_9_subprocesses.rb +146 -0
- data/test/functional/restart_base.rb +34 -0
- data/test/functional/rt_0_wait.rb +55 -0
- data/test/functional/rt_1_listen.rb +56 -0
- data/test/functional/rt_2_errors.rb +56 -0
- data/test/functional/rt_3_once.rb +70 -0
- data/test/functional/rt_4_cron.rb +64 -0
- data/test/functional/rt_5_timeout.rb +60 -0
- data/test/functional/rtest.rb +8 -0
- data/test/functional/storage_helper.rb +93 -0
- data/test/functional/test.rb +44 -0
- data/test/functional/vertical.rb +46 -0
- data/test/path_helper.rb +15 -0
- data/test/test.rb +13 -0
- data/test/test_helper.rb +28 -0
- data/test/unit/storage.rb +428 -0
- data/test/unit/storages.rb +37 -0
- data/test/unit/test.rb +28 -0
- data/test/unit/ut_0_ruby_reader.rb +223 -0
- data/test/unit/ut_11_lookup.rb +122 -0
- data/test/unit/ut_13_serializer.rb +65 -0
- data/test/unit/ut_14_is_uri.rb +28 -0
- data/test/unit/ut_15_util.rb +57 -0
- data/test/unit/ut_16_reader.rb +225 -0
- data/test/unit/ut_18_engine.rb +47 -0
- data/test/unit/ut_19_part_template.rb +86 -0
- data/test/unit/ut_1_fei.rb +165 -0
- data/test/unit/ut_20_composite_storage.rb +74 -0
- data/test/unit/ut_21_svc_participant_list.rb +46 -0
- data/test/unit/ut_22_filter.rb +1094 -0
- data/test/unit/ut_23_svc_tracker.rb +48 -0
- data/test/unit/ut_24_radial_reader.rb +332 -0
- data/test/unit/ut_25_merge.rb +113 -0
- data/test/unit/ut_3_wait_logger.rb +39 -0
- data/test/unit/ut_4_expmap.rb +20 -0
- data/test/unit/ut_5_tree.rb +54 -0
- data/test/unit/ut_6_condition.rb +303 -0
- data/test/unit/ut_7_workitem.rb +99 -0
- data/test/unit/ut_8_tree_to_dot.rb +72 -0
- data/test/unit/ut_9_xml_reader.rb +61 -0
- metadata +504 -0
data/lib/ruote.rb
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2005-2011, John Mettraux, jmettraux@gmail.com
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
9
|
+
# furnished to do so, subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
12
|
+
# all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
# THE SOFTWARE.
|
21
|
+
#
|
22
|
+
# Made in Japan.
|
23
|
+
#++
|
24
|
+
|
25
|
+
require 'ruote/util/misc'
|
26
|
+
|
27
|
+
|
28
|
+
module Ruote
|
29
|
+
|
30
|
+
#
|
31
|
+
# A sort of internal registry, via a shared instance of this class, the worker
|
32
|
+
# and the engine can access subservices like reader, treechecker,
|
33
|
+
# wfid_generator and so on.
|
34
|
+
#
|
35
|
+
class Context
|
36
|
+
|
37
|
+
SERVICE_PREFIX = /^s\_/
|
38
|
+
|
39
|
+
attr_reader :storage
|
40
|
+
attr_accessor :worker
|
41
|
+
attr_accessor :engine
|
42
|
+
|
43
|
+
def initialize(storage, worker=nil)
|
44
|
+
|
45
|
+
@storage = storage
|
46
|
+
@storage.context = self
|
47
|
+
|
48
|
+
@engine = nil
|
49
|
+
@worker = worker
|
50
|
+
|
51
|
+
@services = {}
|
52
|
+
|
53
|
+
initialize_services
|
54
|
+
end
|
55
|
+
|
56
|
+
# A trick : in order to avoid
|
57
|
+
#
|
58
|
+
# @context = o.respond_to?(:context) ? o.context : o
|
59
|
+
# # or
|
60
|
+
# #@context = o.is_a?(Ruote::Context) ? o : o.context
|
61
|
+
#
|
62
|
+
# simply letting a context say its context is itself.
|
63
|
+
#
|
64
|
+
def context
|
65
|
+
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the engine_id (as set in the configuration under the key
|
70
|
+
# "engine_id"), or, by default, "engine".
|
71
|
+
#
|
72
|
+
def engine_id
|
73
|
+
|
74
|
+
get_conf['engine_id'] || 'engine'
|
75
|
+
end
|
76
|
+
|
77
|
+
# Used for things like
|
78
|
+
#
|
79
|
+
# if @context['ruby_eval_allowed']
|
80
|
+
# # ...
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
def [](key)
|
84
|
+
|
85
|
+
SERVICE_PREFIX.match(key) ? @services[key] : get_conf[key]
|
86
|
+
end
|
87
|
+
|
88
|
+
# Mostly used by engine#configure
|
89
|
+
#
|
90
|
+
def []=(key, value)
|
91
|
+
|
92
|
+
raise(
|
93
|
+
ArgumentError.new('use context#add_service to register services')
|
94
|
+
) if SERVICE_PREFIX.match(key)
|
95
|
+
|
96
|
+
cf = get_conf
|
97
|
+
cf[key] = value
|
98
|
+
@storage.put(cf)
|
99
|
+
|
100
|
+
value
|
101
|
+
end
|
102
|
+
|
103
|
+
def keys
|
104
|
+
|
105
|
+
get_conf.keys
|
106
|
+
end
|
107
|
+
|
108
|
+
def add_service(key, *args)
|
109
|
+
|
110
|
+
path, klass, opts = args
|
111
|
+
|
112
|
+
key = "s_#{key}" unless SERVICE_PREFIX.match(key)
|
113
|
+
|
114
|
+
service = if klass
|
115
|
+
|
116
|
+
require(path) if path
|
117
|
+
|
118
|
+
aa = [ self ]
|
119
|
+
aa << opts if opts
|
120
|
+
|
121
|
+
@services[key] = Ruote.constantize(klass).new(*aa)
|
122
|
+
else
|
123
|
+
|
124
|
+
@services[key] = path
|
125
|
+
end
|
126
|
+
|
127
|
+
self.class.class_eval %{ def #{key[2..-1]}; @services['#{key}']; end }
|
128
|
+
#
|
129
|
+
# This 'one-liner' will add an instance method to Context for this
|
130
|
+
# service.
|
131
|
+
#
|
132
|
+
# If the service key is 's_dishwasher', then the service will be
|
133
|
+
# available via Context#dishwasher.
|
134
|
+
#
|
135
|
+
# I.e. dishwasher = engine.context.dishwasher
|
136
|
+
|
137
|
+
service
|
138
|
+
end
|
139
|
+
|
140
|
+
# Takes care of shutting down every service registered in this context.
|
141
|
+
#
|
142
|
+
def shutdown
|
143
|
+
|
144
|
+
@worker.shutdown if @worker
|
145
|
+
@storage.shutdown if @storage.respond_to?(:shutdown)
|
146
|
+
|
147
|
+
@services.values.each { |s| s.shutdown if s.respond_to?(:shutdown) }
|
148
|
+
end
|
149
|
+
|
150
|
+
protected
|
151
|
+
|
152
|
+
def get_conf
|
153
|
+
|
154
|
+
@storage.get_configuration('engine') || {}
|
155
|
+
end
|
156
|
+
|
157
|
+
def initialize_services
|
158
|
+
|
159
|
+
default_conf.merge(get_conf).each do |key, value|
|
160
|
+
|
161
|
+
next unless SERVICE_PREFIX.match(key)
|
162
|
+
|
163
|
+
add_service(key, *value)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def default_conf
|
168
|
+
|
169
|
+
{ 's_wfidgen' => [
|
170
|
+
'ruote/id/mnemo_wfid_generator', 'Ruote::MnemoWfidGenerator' ],
|
171
|
+
's_reader' => [
|
172
|
+
'ruote/reader', 'Ruote::Reader' ],
|
173
|
+
's_treechecker' => [
|
174
|
+
'ruote/svc/treechecker', 'Ruote::TreeChecker' ],
|
175
|
+
's_expmap' => [
|
176
|
+
'ruote/svc/expression_map', 'Ruote::ExpressionMap' ],
|
177
|
+
's_tracker' => [
|
178
|
+
'ruote/svc/tracker', 'Ruote::Tracker' ],
|
179
|
+
's_plist' => [
|
180
|
+
'ruote/svc/participant_list', 'Ruote::ParticipantList' ],
|
181
|
+
's_dispatch_pool' => [
|
182
|
+
'ruote/svc/dispatch_pool', 'Ruote::DispatchPool' ],
|
183
|
+
's_dollar_sub' => [
|
184
|
+
'ruote/svc/dollar_sub', 'Ruote::DollarSubstitution' ],
|
185
|
+
's_error_handler' => [
|
186
|
+
'ruote/svc/error_handler', 'Ruote::ErrorHandler' ],
|
187
|
+
's_logger' => [
|
188
|
+
'ruote/log/wait_logger', 'Ruote::WaitLogger' ],
|
189
|
+
's_history' => [
|
190
|
+
'ruote/log/default_history', 'Ruote::DefaultHistory' ] }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
data/lib/ruote/engine.rb
ADDED
@@ -0,0 +1,1062 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2005-2011, John Mettraux, jmettraux@gmail.com
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
9
|
+
# furnished to do so, subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
12
|
+
# all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
# THE SOFTWARE.
|
21
|
+
#
|
22
|
+
# Made in Japan.
|
23
|
+
#++
|
24
|
+
|
25
|
+
require 'ruote/context'
|
26
|
+
require 'ruote/engine/process_status'
|
27
|
+
require 'ruote/receiver/base'
|
28
|
+
|
29
|
+
|
30
|
+
module Ruote
|
31
|
+
|
32
|
+
#
|
33
|
+
# This class holds the 'engine' name, perhaps 'dashboard' would have been
|
34
|
+
# a better name. Anyway, the methods here allow to launch processes
|
35
|
+
# and to query about their status. There are also methods for fixing
|
36
|
+
# issues with stalled processes or processes stuck in errors.
|
37
|
+
#
|
38
|
+
# NOTE : the methods #launch and #reply are implemented in
|
39
|
+
# Ruote::ReceiverMixin (this Engine class has all the methods of a Receiver).
|
40
|
+
#
|
41
|
+
class Engine
|
42
|
+
|
43
|
+
include ReceiverMixin
|
44
|
+
|
45
|
+
attr_reader :context
|
46
|
+
attr_reader :variables
|
47
|
+
|
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 will
|
59
|
+
# be started and run in the current thread (and the initialize method
|
60
|
+
# will not return).
|
61
|
+
#
|
62
|
+
def initialize(worker_or_storage, opts=true)
|
63
|
+
|
64
|
+
@context = worker_or_storage.context
|
65
|
+
@context.engine = self
|
66
|
+
|
67
|
+
@variables = EngineVariables.new(@context.storage)
|
68
|
+
|
69
|
+
if @context.worker
|
70
|
+
if opts == true
|
71
|
+
@context.worker.run_in_thread
|
72
|
+
# runs worker in its own thread
|
73
|
+
elsif opts == { :join => true }
|
74
|
+
@context.worker.run
|
75
|
+
# runs worker in current thread (and doesn't return)
|
76
|
+
#else
|
77
|
+
# worker is not run
|
78
|
+
end
|
79
|
+
#else
|
80
|
+
# no worker
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns the storage this engine works with passed at engine
|
85
|
+
# initialization.
|
86
|
+
#
|
87
|
+
def storage
|
88
|
+
|
89
|
+
@context.storage
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns the worker nested inside this engine (passed at initialization).
|
93
|
+
# Returns nil if this engine is only linked to a storage (and the worker
|
94
|
+
# is running somewhere else (hopefully)).
|
95
|
+
#
|
96
|
+
def worker
|
97
|
+
|
98
|
+
@context.worker
|
99
|
+
end
|
100
|
+
|
101
|
+
# A shortcut for engine.context.history
|
102
|
+
#
|
103
|
+
def history
|
104
|
+
|
105
|
+
@context.history
|
106
|
+
end
|
107
|
+
|
108
|
+
# Quick note : the implementation of launch is found in the module
|
109
|
+
# Ruote::ReceiverMixin that the engine includes.
|
110
|
+
#
|
111
|
+
# Some processes have to have one and only one instance of themselves
|
112
|
+
# running, these are called 'singles' ('singleton' is too object-oriented).
|
113
|
+
#
|
114
|
+
# When called, this method will check if an instance of the pdef is
|
115
|
+
# already running (it uses the process definition name attribute), if
|
116
|
+
# yes, it will return without having launched anything. If there is no
|
117
|
+
# such process running, it will launch it (and register it).
|
118
|
+
#
|
119
|
+
# Returns the wfid (workflow instance id) of the running single.
|
120
|
+
#
|
121
|
+
def launch_single(process_definition, fields={}, variables={})
|
122
|
+
|
123
|
+
tree = @context.reader.read(process_definition)
|
124
|
+
name = tree[1]['name'] || (tree[1].find { |k, v| v.nil? } || []).first
|
125
|
+
|
126
|
+
raise ArgumentError.new(
|
127
|
+
'process definition is missing a name, cannot launch as single'
|
128
|
+
) unless name
|
129
|
+
|
130
|
+
singles = @context.storage.get('variables', 'singles') || {
|
131
|
+
'_id' => 'singles', 'type' => 'variables', 'h' => {}
|
132
|
+
}
|
133
|
+
wfid, timestamp = singles['h'][name]
|
134
|
+
|
135
|
+
return wfid if wfid && (ps(wfid) || Time.now.to_f - timestamp < 1.0)
|
136
|
+
# return wfid if 'singleton' process is already running
|
137
|
+
|
138
|
+
wfid = @context.wfidgen.generate
|
139
|
+
|
140
|
+
singles['h'][name] = [ wfid, Time.now.to_f ]
|
141
|
+
|
142
|
+
r = @context.storage.put(singles)
|
143
|
+
|
144
|
+
return launch_single(tree, fields, variables) unless r.nil?
|
145
|
+
#
|
146
|
+
# the put failed, back to the start...
|
147
|
+
#
|
148
|
+
# all this to prevent races between multiple engines,
|
149
|
+
# multiple launch_single calls (from different Ruby runtimes)
|
150
|
+
|
151
|
+
# ... green for launch
|
152
|
+
|
153
|
+
@context.storage.put_msg(
|
154
|
+
'launch',
|
155
|
+
'wfid' => wfid,
|
156
|
+
'tree' => tree,
|
157
|
+
'workitem' => { 'fields' => fields },
|
158
|
+
'variables' => variables)
|
159
|
+
|
160
|
+
wfid
|
161
|
+
end
|
162
|
+
|
163
|
+
# Given a workitem or a fei, will do a cancel_expression,
|
164
|
+
# else it's a wfid and it does a cancel_process.
|
165
|
+
#
|
166
|
+
def cancel(wi_or_fei_or_wfid)
|
167
|
+
|
168
|
+
do_misc('cancel', wi_or_fei_or_wfid, {})
|
169
|
+
end
|
170
|
+
|
171
|
+
alias cancel_process cancel
|
172
|
+
alias cancel_expression cancel
|
173
|
+
|
174
|
+
# Given a workitem or a fei, will do a kill_expression,
|
175
|
+
# else it's a wfid and it does a kill_process.
|
176
|
+
#
|
177
|
+
def kill(wi_or_fei_or_wfid)
|
178
|
+
|
179
|
+
do_misc('kill', wi_or_fei_or_wfid, {})
|
180
|
+
end
|
181
|
+
|
182
|
+
alias kill_process kill
|
183
|
+
alias kill_expression kill
|
184
|
+
|
185
|
+
# Given a wfid, will [attempt to] pause the corresponding process instance.
|
186
|
+
# Given an expression id (fei) will [attempt to] pause the expression
|
187
|
+
# and its children.
|
188
|
+
#
|
189
|
+
# The only known option for now is :breakpoint => true, which lets
|
190
|
+
# the engine only pause the targetted expression.
|
191
|
+
#
|
192
|
+
#
|
193
|
+
# == fei and :breakpoint => true
|
194
|
+
#
|
195
|
+
# By default, pausing an expression will pause that expression and
|
196
|
+
# all its children.
|
197
|
+
#
|
198
|
+
# engine.pause(fei, :breakpoint => true)
|
199
|
+
#
|
200
|
+
# will only flag as paused the given fei. When the children of that
|
201
|
+
# expression will reply to it, the execution for this branch of the
|
202
|
+
# process will stop, much like a break point.
|
203
|
+
#
|
204
|
+
def pause(wi_or_fei_or_wfid, opts={})
|
205
|
+
|
206
|
+
raise ArgumentError.new(
|
207
|
+
':breakpoint option only valid when passing a workitem or a fei'
|
208
|
+
) if opts[:breakpoint] and wi_or_fei_or_wfid.is_a?(String)
|
209
|
+
|
210
|
+
do_misc('pause', wi_or_fei_or_wfid, opts)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Given a wfid will [attempt to] resume the process instance.
|
214
|
+
# Given an expression id (fei) will [attempt to] to resume the expression
|
215
|
+
# and its children.
|
216
|
+
#
|
217
|
+
# Note : this is supposed to be called on paused expressions / instances,
|
218
|
+
# this is NOT meant to be called to unstuck / unhang a process.
|
219
|
+
#
|
220
|
+
# == resume(wfid, :anyway => true)
|
221
|
+
#
|
222
|
+
# Resuming a process instance is equivalent to calling resume on its
|
223
|
+
# root expression. If the root is not paused itself, this will have no
|
224
|
+
# effect.
|
225
|
+
#
|
226
|
+
# engine.resume(wfid, :anyway => true)
|
227
|
+
#
|
228
|
+
# will make sure to call resume on each of the paused branch within the
|
229
|
+
# process instance (tree), effectively resuming the whole process.
|
230
|
+
#
|
231
|
+
def resume(wi_or_fei_or_wfid, opts={})
|
232
|
+
|
233
|
+
do_misc('resume', wi_or_fei_or_wfid, opts)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Replays at a given error (hopefully you fixed the cause of the error
|
237
|
+
# before replaying...)
|
238
|
+
#
|
239
|
+
def replay_at_error(err)
|
240
|
+
|
241
|
+
err = error(err) unless err.is_a?(Ruote::ProcessError)
|
242
|
+
|
243
|
+
msg = err.msg.dup
|
244
|
+
|
245
|
+
if tree = msg['tree']
|
246
|
+
#
|
247
|
+
# as soon as there is a tree, it means it's a re_apply
|
248
|
+
|
249
|
+
re_apply(msg['fei'], 'tree' => tree, 'replay_at_error' => true)
|
250
|
+
|
251
|
+
else
|
252
|
+
|
253
|
+
action = msg.delete('action')
|
254
|
+
|
255
|
+
msg['replay_at_error'] = true
|
256
|
+
# just an indication
|
257
|
+
|
258
|
+
@context.storage.delete(err.to_h) # remove error
|
259
|
+
@context.storage.put_msg(action, msg) # trigger replay
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Re-applies an expression (given via its FlowExpressionId).
|
264
|
+
#
|
265
|
+
# That will cancel the expression and, once the cancel operation is over
|
266
|
+
# (all the children have been cancelled), the expression will get
|
267
|
+
# re-applied.
|
268
|
+
#
|
269
|
+
# The fei parameter may be a hash, a Ruote::FlowExpressionId instance,
|
270
|
+
# a Ruote::Workitem instance or a sid string.
|
271
|
+
#
|
272
|
+
# == options
|
273
|
+
#
|
274
|
+
# :tree is used to completely change the tree of the expression at re_apply
|
275
|
+
#
|
276
|
+
# engine.re_apply(fei, :tree => [ 'participant', { 'ref' => 'bob' }, [] ])
|
277
|
+
#
|
278
|
+
# :fields is used to replace the fields of the workitem at re_apply
|
279
|
+
#
|
280
|
+
# engine.re_apply(fei, :fields => { 'customer' => 'bob' })
|
281
|
+
#
|
282
|
+
# :merge_in_fields is used to add / override fields
|
283
|
+
#
|
284
|
+
# engine.re_apply(fei, :merge_in_fields => { 'customer' => 'bob' })
|
285
|
+
#
|
286
|
+
def re_apply(fei, opts={})
|
287
|
+
|
288
|
+
@context.storage.put_msg(
|
289
|
+
'cancel',
|
290
|
+
'fei' => FlowExpressionId.extract_h(fei),
|
291
|
+
're_apply' => Ruote.keys_to_s(opts))
|
292
|
+
end
|
293
|
+
|
294
|
+
# Returns a ProcessStatus instance describing the current status of
|
295
|
+
# a process instance.
|
296
|
+
#
|
297
|
+
def process(wfid)
|
298
|
+
|
299
|
+
statuses([ wfid ], {}).first
|
300
|
+
end
|
301
|
+
|
302
|
+
# Returns an array of ProcessStatus instances.
|
303
|
+
#
|
304
|
+
# WARNING : this is an expensive operation, but it understands :skip
|
305
|
+
# and :limit, so pagination is our friend.
|
306
|
+
#
|
307
|
+
# Please note, if you're interested only in processes that have errors,
|
308
|
+
# Engine#errors is a more efficient means.
|
309
|
+
#
|
310
|
+
# To simply list the wfids of the currently running, Engine#process_wfids
|
311
|
+
# is way cheaper to call.
|
312
|
+
#
|
313
|
+
def processes(opts={})
|
314
|
+
|
315
|
+
wfids = @context.storage.expression_wfids(opts)
|
316
|
+
|
317
|
+
opts[:count] ? wfids.size : statuses(wfids, opts)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Returns a list of processes or the process status of a given process
|
321
|
+
# instance.
|
322
|
+
#
|
323
|
+
def ps(wfid=nil)
|
324
|
+
|
325
|
+
wfid == nil ? processes : process(wfid)
|
326
|
+
end
|
327
|
+
|
328
|
+
# Returns an array of current errors (hashes)
|
329
|
+
#
|
330
|
+
# Can be called in two ways :
|
331
|
+
#
|
332
|
+
# engine.errors(wfid)
|
333
|
+
#
|
334
|
+
# and
|
335
|
+
#
|
336
|
+
# engine.errors(:skip => 100, :limit => 100)
|
337
|
+
#
|
338
|
+
def errors(wfid=nil)
|
339
|
+
|
340
|
+
wfid, options = wfid.is_a?(Hash) ? [ nil, wfid ] : [ wfid, {} ]
|
341
|
+
|
342
|
+
errs = wfid.nil? ?
|
343
|
+
@context.storage.get_many('errors', nil, options) :
|
344
|
+
@context.storage.get_many('errors', wfid)
|
345
|
+
|
346
|
+
return errs if options[:count]
|
347
|
+
|
348
|
+
errs.collect { |err| ProcessError.new(err) }
|
349
|
+
end
|
350
|
+
|
351
|
+
# Given a workitem or a fei (or a String version of a fei), returns
|
352
|
+
# the corresponding error (or nil if there is no other).
|
353
|
+
#
|
354
|
+
def error(wi_or_fei)
|
355
|
+
|
356
|
+
fei = Ruote.extract_fei(wi_or_fei)
|
357
|
+
err = @context.storage.get('errors', "err_#{fei.sid}")
|
358
|
+
|
359
|
+
err ? ProcessError.new(err) : nil
|
360
|
+
end
|
361
|
+
|
362
|
+
# Returns an array of schedules. Those schedules are open structs
|
363
|
+
# with various properties, like target, owner, at, put_at, ...
|
364
|
+
#
|
365
|
+
# Introduced mostly for ruote-kit.
|
366
|
+
#
|
367
|
+
# Can be called in two ways :
|
368
|
+
#
|
369
|
+
# engine.schedules(wfid)
|
370
|
+
#
|
371
|
+
# and
|
372
|
+
#
|
373
|
+
# engine.schedules(:skip => 100, :limit => 100)
|
374
|
+
#
|
375
|
+
def schedules(wfid=nil)
|
376
|
+
|
377
|
+
wfid, options = wfid.is_a?(Hash) ? [ nil, wfid ] : [ wfid, {} ]
|
378
|
+
|
379
|
+
scheds = wfid.nil? ?
|
380
|
+
@context.storage.get_many('schedules', nil, options) :
|
381
|
+
@context.storage.get_many('schedules', /!#{wfid}-\d+$/)
|
382
|
+
|
383
|
+
return scheds if options[:count]
|
384
|
+
|
385
|
+
scheds.collect { |s| Ruote.schedule_to_h(s) }.sort_by { |s| s['wfid'] }
|
386
|
+
end
|
387
|
+
|
388
|
+
# Returns a [sorted] list of wfids of the process instances currently
|
389
|
+
# running in the engine.
|
390
|
+
#
|
391
|
+
# This operation is substantially less costly than Engine#processes (though
|
392
|
+
# the 'how substantially' depends on the storage chosen).
|
393
|
+
#
|
394
|
+
def process_ids
|
395
|
+
|
396
|
+
@context.storage.expression_wfids({})
|
397
|
+
end
|
398
|
+
|
399
|
+
alias process_wfids process_ids
|
400
|
+
|
401
|
+
# Warning : expensive operation.
|
402
|
+
#
|
403
|
+
# Leftovers are workitems, errors and schedules belonging to process
|
404
|
+
# instances for which there are no more expressions left.
|
405
|
+
#
|
406
|
+
# Better delete them or investigate why they are left here.
|
407
|
+
#
|
408
|
+
# The result is a list of documents (hashes) as found in the storage. Each
|
409
|
+
# of them might represent a workitem, an error or a schedule.
|
410
|
+
#
|
411
|
+
# If you want to delete one of them you can do
|
412
|
+
#
|
413
|
+
# engine.storage.delete(doc)
|
414
|
+
#
|
415
|
+
def leftovers
|
416
|
+
|
417
|
+
wfids = @context.storage.expression_wfids({})
|
418
|
+
|
419
|
+
wis = @context.storage.get_many('workitems').compact
|
420
|
+
ers = @context.storage.get_many('errors').compact
|
421
|
+
scs = @context.storage.get_many('schedules').compact
|
422
|
+
# some slow storages need the compaction... [c]ouch...
|
423
|
+
|
424
|
+
(wis + ers + scs).reject { |doc| wfids.include?(doc['fei']['wfid']) }
|
425
|
+
end
|
426
|
+
|
427
|
+
# Shuts down the engine, mostly passes the shutdown message to the other
|
428
|
+
# services and hope they'll shut down properly.
|
429
|
+
#
|
430
|
+
def shutdown
|
431
|
+
|
432
|
+
@context.shutdown
|
433
|
+
end
|
434
|
+
|
435
|
+
# This method expects there to be a logger with a wait_for method in the
|
436
|
+
# context, else it will raise an exception.
|
437
|
+
#
|
438
|
+
# *WARNING* : wait_for() is meant for environments where there is a unique
|
439
|
+
# worker and that worker is nested in this engine. In a multiple worker
|
440
|
+
# environment wait_for doesn't see events handled by 'other' workers.
|
441
|
+
#
|
442
|
+
# This method is only useful for test/quickstart/examples environments.
|
443
|
+
#
|
444
|
+
# engine.wait_for(:alpha)
|
445
|
+
# # will make the current thread block until a workitem is delivered
|
446
|
+
# # to the participant named 'alpha'
|
447
|
+
#
|
448
|
+
# engine.wait_for('123432123-9043')
|
449
|
+
# # will make the current thread block until the processed whose
|
450
|
+
# # wfid is given (String) terminates or produces an error.
|
451
|
+
#
|
452
|
+
# engine.wait_for(5)
|
453
|
+
# # will make the current thread block until 5 messages have been
|
454
|
+
# # processed on the workqueue...
|
455
|
+
#
|
456
|
+
# engine.wait_for(:empty)
|
457
|
+
# # will return as soon as the engine/storage is empty, ie as soon
|
458
|
+
# # as there are no more processes running in the engine (no more
|
459
|
+
# # expressions placed in the storage)
|
460
|
+
#
|
461
|
+
# It's OK to wait for multiple wfids :
|
462
|
+
#
|
463
|
+
# engine.wait_for('20100612-bezerijozo', '20100612-yakisoba')
|
464
|
+
#
|
465
|
+
def wait_for(*items)
|
466
|
+
|
467
|
+
logger = @context['s_logger']
|
468
|
+
|
469
|
+
raise(
|
470
|
+
"can't wait_for, there is no logger that responds to that call"
|
471
|
+
) unless logger.respond_to?(:wait_for)
|
472
|
+
|
473
|
+
logger.wait_for(items)
|
474
|
+
end
|
475
|
+
|
476
|
+
# Joins the worker thread. If this engine has no nested worker, calling
|
477
|
+
# this method will simply return immediately.
|
478
|
+
#
|
479
|
+
def join
|
480
|
+
|
481
|
+
worker.join if worker
|
482
|
+
end
|
483
|
+
|
484
|
+
# Loads (and turns into a tree) the process definition at the given path.
|
485
|
+
#
|
486
|
+
def load_definition(path)
|
487
|
+
|
488
|
+
@context.reader.read(path)
|
489
|
+
end
|
490
|
+
|
491
|
+
# Registers a participant in the engine.
|
492
|
+
#
|
493
|
+
# Takes the form
|
494
|
+
#
|
495
|
+
# engine.register_participant name_or_regex, klass, opts={}
|
496
|
+
#
|
497
|
+
# With the form
|
498
|
+
#
|
499
|
+
# engine.register_participant name_or_regex do |workitem|
|
500
|
+
# # ...
|
501
|
+
# end
|
502
|
+
#
|
503
|
+
# A BlockParticipant is automatically created.
|
504
|
+
#
|
505
|
+
#
|
506
|
+
# == name or regex
|
507
|
+
#
|
508
|
+
# When registering participants, strings or regexes are accepted. Behind
|
509
|
+
# the scenes, a regex is kept.
|
510
|
+
#
|
511
|
+
# Passing a string like "alain" will get ruote to automatically turn it
|
512
|
+
# into the following regex : /^alain$/.
|
513
|
+
#
|
514
|
+
# For finer control over this, pass a regex directly
|
515
|
+
#
|
516
|
+
# engine.register_participant /^user-/, MyParticipant
|
517
|
+
# # will match all workitems whose participant name starts with "user-"
|
518
|
+
#
|
519
|
+
#
|
520
|
+
# == some examples
|
521
|
+
#
|
522
|
+
# engine.register_participant 'compute_sum' do |wi|
|
523
|
+
# wi.fields['sum'] = wi.fields['articles'].inject(0) do |s, (c, v)|
|
524
|
+
# s + c * v # sum + count * value
|
525
|
+
# end
|
526
|
+
# # a block participant implicitely replies to the engine immediately
|
527
|
+
# end
|
528
|
+
#
|
529
|
+
# class MyParticipant
|
530
|
+
# def initialize(opts)
|
531
|
+
# @name = opts['name']
|
532
|
+
# end
|
533
|
+
# def consume(workitem)
|
534
|
+
# workitem.fields['rocket_name'] = @name
|
535
|
+
# send_to_the_moon(workitem)
|
536
|
+
# end
|
537
|
+
# def cancel(fei, flavour)
|
538
|
+
# # do nothing
|
539
|
+
# end
|
540
|
+
# end
|
541
|
+
#
|
542
|
+
# engine.register_participant(
|
543
|
+
# /^moon-.+/, MyParticipant, 'name' => 'Saturn-V')
|
544
|
+
#
|
545
|
+
# # computing the total for a invoice being passed in the workitem.
|
546
|
+
# #
|
547
|
+
# class TotalParticipant
|
548
|
+
# include Ruote::LocalParticipant
|
549
|
+
#
|
550
|
+
# def consume(workitem)
|
551
|
+
# workitem['total'] = workitem.fields['items'].inject(0.0) { |t, item|
|
552
|
+
# t + item['count'] * PricingService.lookup(item['id'])
|
553
|
+
# }
|
554
|
+
# reply_to_engine(workitem)
|
555
|
+
# end
|
556
|
+
# end
|
557
|
+
# engine.register_participant 'total', TotalParticipant
|
558
|
+
#
|
559
|
+
# Remember that the options (the hash that follows the class name), must be
|
560
|
+
# serializable via JSON.
|
561
|
+
#
|
562
|
+
#
|
563
|
+
# == require_path and load_path
|
564
|
+
#
|
565
|
+
# It's OK to register a participant by passing its full classname as a
|
566
|
+
# String.
|
567
|
+
#
|
568
|
+
# engine.register_participant(
|
569
|
+
# 'auditor', 'AuditParticipant', 'require_path' => 'part/audit.rb')
|
570
|
+
# engine.register_participant(
|
571
|
+
# 'auto_decision', 'DecParticipant', 'load_path' => 'part/dec.rb')
|
572
|
+
#
|
573
|
+
# Note the option load_path / require_path that point to the ruby file
|
574
|
+
# containing the participant implementation. 'require' will load and eval
|
575
|
+
# the ruby code only once, 'load' each time.
|
576
|
+
#
|
577
|
+
#
|
578
|
+
# == :override => false
|
579
|
+
#
|
580
|
+
# By default, when registering a participant, if this results in a regex
|
581
|
+
# that is already used, the previously registered participant gets
|
582
|
+
# unregistered.
|
583
|
+
#
|
584
|
+
# engine.register_participant 'alpha', AaParticipant
|
585
|
+
# engine.register_participant 'alpha', BbParticipant, :override => false
|
586
|
+
#
|
587
|
+
# This can be useful when the #accept? method of participants are in use.
|
588
|
+
#
|
589
|
+
# Note that using the #register(&block) method, :override => false is
|
590
|
+
# automatically enforced.
|
591
|
+
#
|
592
|
+
# engine.register do
|
593
|
+
# alpha AaParticipant
|
594
|
+
# alpha BbParticipant
|
595
|
+
# end
|
596
|
+
#
|
597
|
+
#
|
598
|
+
# == :position / :pos => 'last' / 'first' / 'before' / 'after' / 'over'
|
599
|
+
#
|
600
|
+
# One can specify the position where the participant should be inserted
|
601
|
+
# in the participant list.
|
602
|
+
#
|
603
|
+
# engine.register_participant 'auditor', AuditParticipant, :pos => 'last'
|
604
|
+
#
|
605
|
+
# * last : it's the default, places the participant at the end of the list
|
606
|
+
# * first : top of the list
|
607
|
+
# * before : implies :override => false, places before the existing
|
608
|
+
# participant with the same regex
|
609
|
+
# * after : implies :override => false, places after the last existing
|
610
|
+
# participant with the same regex
|
611
|
+
# * over : overrides in the same position (while the regular, default
|
612
|
+
# overide removes and then places the new participant at the end of
|
613
|
+
# the list)
|
614
|
+
#
|
615
|
+
def register_participant(regex, participant=nil, opts={}, &block)
|
616
|
+
|
617
|
+
if participant.is_a?(Hash)
|
618
|
+
opts = participant
|
619
|
+
participant = nil
|
620
|
+
end
|
621
|
+
|
622
|
+
pa = @context.plist.register(regex, participant, opts, block)
|
623
|
+
|
624
|
+
@context.storage.put_msg(
|
625
|
+
'participant_registered',
|
626
|
+
'regex' => regex.is_a?(Regexp) ? regex.inspect : regex.to_s)
|
627
|
+
|
628
|
+
pa
|
629
|
+
end
|
630
|
+
|
631
|
+
# A shorter version of #register_participant
|
632
|
+
#
|
633
|
+
# engine.register 'alice', MailParticipant, :target => 'alice@example.com'
|
634
|
+
#
|
635
|
+
# or a block registering mechanism.
|
636
|
+
#
|
637
|
+
# engine.register do
|
638
|
+
# alpha 'Participants::Alpha', 'flavour' => 'vanilla'
|
639
|
+
# participant 'bravo', 'Participants::Bravo', :flavour => 'peach'
|
640
|
+
# catchall ParticipantCharlie, 'flavour' => 'coconut'
|
641
|
+
# end
|
642
|
+
#
|
643
|
+
# Originally implemented in ruote-kit by Torsten Schoenebaum.
|
644
|
+
#
|
645
|
+
# == registration in block and :clear
|
646
|
+
#
|
647
|
+
# By default, when registering multiple participants in block, ruote
|
648
|
+
# considers you're wiping the participant list and re-adding them all.
|
649
|
+
#
|
650
|
+
# You can prevent the clearing by stating :clear => false like in :
|
651
|
+
#
|
652
|
+
# engine.register :clear => false do
|
653
|
+
# alpha 'Participants::Alpha', 'flavour' => 'vanilla'
|
654
|
+
# participant 'bravo', 'Participants::Bravo', :flavour => 'peach'
|
655
|
+
# catchall ParticipantCharlie, 'flavour' => 'coconut'
|
656
|
+
# end
|
657
|
+
#
|
658
|
+
def register(*args, &block)
|
659
|
+
|
660
|
+
clear = args.first.is_a?(Hash) ? args.pop[:clear] : true
|
661
|
+
|
662
|
+
if args.size > 0
|
663
|
+
register_participant(*args, &block)
|
664
|
+
else
|
665
|
+
@context.plist.clear if clear
|
666
|
+
proxy = ParticipantRegistrationProxy.new(self)
|
667
|
+
block.arity < 1 ? proxy.instance_eval(&block) : block.call(proxy)
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
# Removes/unregisters a participant from the engine.
|
672
|
+
#
|
673
|
+
def unregister_participant(name_or_participant)
|
674
|
+
|
675
|
+
re = @context.plist.unregister(name_or_participant)
|
676
|
+
|
677
|
+
raise(ArgumentError.new('participant not found')) unless re
|
678
|
+
|
679
|
+
@context.storage.put_msg(
|
680
|
+
'participant_unregistered',
|
681
|
+
'regex' => re.to_s)
|
682
|
+
end
|
683
|
+
|
684
|
+
alias :unregister :unregister_participant
|
685
|
+
|
686
|
+
# Returns a list of Ruote::ParticipantEntry instances.
|
687
|
+
#
|
688
|
+
# engine.register_participant :alpha, MyParticipant, 'message' => 'hello'
|
689
|
+
#
|
690
|
+
# # interrogate participant list
|
691
|
+
# #
|
692
|
+
# list = engine.participant_list
|
693
|
+
# participant = list.first
|
694
|
+
# p participant.regex
|
695
|
+
# # => "^alpha$"
|
696
|
+
# p participant.classname
|
697
|
+
# # => "MyParticipant"
|
698
|
+
# p participant.options
|
699
|
+
# # => {"message"=>"hello"}
|
700
|
+
#
|
701
|
+
# # update participant list
|
702
|
+
# #
|
703
|
+
# participant.regex = '^alfred$'
|
704
|
+
# engine.participant_list = list
|
705
|
+
#
|
706
|
+
def participant_list
|
707
|
+
|
708
|
+
@context.plist.list
|
709
|
+
end
|
710
|
+
|
711
|
+
# Accepts a list of Ruote::ParticipantEntry instances or a list of
|
712
|
+
# [ regex, [ classname, opts ] ] arrays.
|
713
|
+
#
|
714
|
+
# See Engine#participant_list
|
715
|
+
#
|
716
|
+
# Some examples :
|
717
|
+
#
|
718
|
+
# engine.participant_list = [
|
719
|
+
# [ '^charly$', [ 'Ruote::StorageParticipant', {} ] ],
|
720
|
+
# [ '.+', [ 'MyDefaultParticipant', { 'default' => true } ]
|
721
|
+
# ]
|
722
|
+
#
|
723
|
+
# This method writes the participant list in one go, it might be easier to
|
724
|
+
# use than to register participant one by ones.
|
725
|
+
#
|
726
|
+
def participant_list=(pl)
|
727
|
+
|
728
|
+
@context.plist.list = pl
|
729
|
+
end
|
730
|
+
|
731
|
+
# A convenience method for
|
732
|
+
#
|
733
|
+
# sp = Ruote::StorageParticipant.new(engine)
|
734
|
+
#
|
735
|
+
# simply do
|
736
|
+
#
|
737
|
+
# sp = engine.storage_participant
|
738
|
+
#
|
739
|
+
def storage_participant
|
740
|
+
|
741
|
+
@storage_participant ||= Ruote::StorageParticipant.new(self)
|
742
|
+
end
|
743
|
+
|
744
|
+
# Returns an instance of the participant registered under the given name.
|
745
|
+
# Returns nil if there is no participant registered for that name.
|
746
|
+
#
|
747
|
+
def participant(name)
|
748
|
+
|
749
|
+
@context.plist.lookup(name.to_s, nil)
|
750
|
+
end
|
751
|
+
|
752
|
+
# Adds a service locally (will not get propagated to other workers).
|
753
|
+
#
|
754
|
+
# tracer = Tracer.new
|
755
|
+
# @engine.add_service('tracer', tracer)
|
756
|
+
#
|
757
|
+
# or
|
758
|
+
#
|
759
|
+
# @engine.add_service('tracer', 'ruote/exp/tracer', 'Ruote::Exp::Tracer')
|
760
|
+
#
|
761
|
+
# This method returns the service instance it just bound.
|
762
|
+
#
|
763
|
+
def add_service(name, path_or_instance, classname=nil, opts=nil)
|
764
|
+
|
765
|
+
@context.add_service(name, path_or_instance, classname, opts)
|
766
|
+
end
|
767
|
+
|
768
|
+
# Sets a configuration option. Examples:
|
769
|
+
#
|
770
|
+
# # allow remote workflow definitions (for subprocesses or when launching
|
771
|
+
# # processes)
|
772
|
+
# @engine.configure('remote_definition_allowed', true)
|
773
|
+
#
|
774
|
+
# # allow ruby_eval
|
775
|
+
# @engine.configure('ruby_eval_allowed', true)
|
776
|
+
#
|
777
|
+
def configure(config_key, value)
|
778
|
+
|
779
|
+
@context[config_key] = value
|
780
|
+
end
|
781
|
+
|
782
|
+
# Returns a configuration value.
|
783
|
+
#
|
784
|
+
# engine.configure('ruby_eval_allowed', true)
|
785
|
+
#
|
786
|
+
# p engine.configuration('ruby_eval_allowed')
|
787
|
+
# # => true
|
788
|
+
#
|
789
|
+
def configuration(config_key)
|
790
|
+
|
791
|
+
@context[config_key]
|
792
|
+
end
|
793
|
+
|
794
|
+
# Returns the process tree that is triggered in case of error.
|
795
|
+
#
|
796
|
+
# Note that this 'on_error' doesn't trigger if an on_error is defined
|
797
|
+
# in the process itself.
|
798
|
+
#
|
799
|
+
# Returns nil if there is no 'on_error' set.
|
800
|
+
#
|
801
|
+
def on_error
|
802
|
+
|
803
|
+
@context.storage.get_trackers['trackers']['on_error']['msg']['tree']
|
804
|
+
|
805
|
+
rescue
|
806
|
+
nil
|
807
|
+
end
|
808
|
+
|
809
|
+
# Returns the process tree that is triggered in case of process termination.
|
810
|
+
#
|
811
|
+
# Note that a termination process doesn't raise a termination process when
|
812
|
+
# it terminates itself.
|
813
|
+
#
|
814
|
+
# Returns nil if there is no 'on_terminate' set.
|
815
|
+
#
|
816
|
+
def on_terminate
|
817
|
+
|
818
|
+
@context.storage.get_trackers['trackers']['on_terminate']['msg']['tree']
|
819
|
+
|
820
|
+
rescue
|
821
|
+
nil
|
822
|
+
end
|
823
|
+
|
824
|
+
# Sets a participant or subprocess to be triggered when an error occurs
|
825
|
+
# in a process instance.
|
826
|
+
#
|
827
|
+
# engine.on_error = participant_name
|
828
|
+
#
|
829
|
+
# engine.on_error = subprocess_name
|
830
|
+
#
|
831
|
+
# engine.on_error = Ruote.process_definition do
|
832
|
+
# alpha
|
833
|
+
# end
|
834
|
+
#
|
835
|
+
# Note that this 'on_error' doesn't trigger if an on_error is defined
|
836
|
+
# in the process itself.
|
837
|
+
#
|
838
|
+
def on_error=(target)
|
839
|
+
|
840
|
+
@context.tracker.add_tracker(
|
841
|
+
nil, # do not track a specific wfid
|
842
|
+
'error_intercepted', # react on 'error_intercepted' msgs
|
843
|
+
'on_error', # the identifier
|
844
|
+
nil, # no specific condition
|
845
|
+
{ 'action' => 'launch',
|
846
|
+
'wfid' => 'replace',
|
847
|
+
'tree' => target.is_a?(String) ?
|
848
|
+
[ 'define', {}, [ [ target, {}, [] ] ] ] : target,
|
849
|
+
'workitem' => 'replace',
|
850
|
+
'variables' => 'compile' })
|
851
|
+
end
|
852
|
+
|
853
|
+
# Sets a participant or a subprocess that is to be launched/called whenever
|
854
|
+
# a regular process terminates.
|
855
|
+
#
|
856
|
+
# engine.on_terminate = participant_name
|
857
|
+
#
|
858
|
+
# engine.on_terminate = subprocess_name
|
859
|
+
#
|
860
|
+
# engine.on_terminate = Ruote.define do
|
861
|
+
# alpha
|
862
|
+
# bravo
|
863
|
+
# end
|
864
|
+
#
|
865
|
+
# Note that a termination process doesn't raise a termination process when
|
866
|
+
# it terminates itself.
|
867
|
+
#
|
868
|
+
# on_terminate processes are not triggered for on_error processes.
|
869
|
+
# on_error processes are triggered for on_terminate processes as well.
|
870
|
+
#
|
871
|
+
def on_terminate=(target)
|
872
|
+
|
873
|
+
@context.tracker.add_tracker(
|
874
|
+
nil, # do not track a specific wfid
|
875
|
+
'terminated', # react on 'error_intercepted' msgs
|
876
|
+
'on_terminate', # the identifier
|
877
|
+
nil, # no specific condition
|
878
|
+
{ 'action' => 'launch',
|
879
|
+
'tree' => target.is_a?(String) ?
|
880
|
+
[ 'define', {}, [ [ target, {}, [] ] ] ] : target,
|
881
|
+
'workitem' => 'replace' })
|
882
|
+
end
|
883
|
+
|
884
|
+
# A debug helper :
|
885
|
+
#
|
886
|
+
# engine.noisy = true
|
887
|
+
#
|
888
|
+
# will let the engine (in fact the worker) pour all the details of the
|
889
|
+
# executing process instances to STDOUT.
|
890
|
+
#
|
891
|
+
def noisy=(b)
|
892
|
+
|
893
|
+
@context.logger.noisy = b
|
894
|
+
end
|
895
|
+
|
896
|
+
protected
|
897
|
+
|
898
|
+
# Used by #pause and #resume.
|
899
|
+
#
|
900
|
+
def do_misc(action, wi_or_fei_or_wfid, opts)
|
901
|
+
|
902
|
+
target = Ruote.extract_id(wi_or_fei_or_wfid)
|
903
|
+
|
904
|
+
if action == 'resume' && opts[:anyway]
|
905
|
+
#
|
906
|
+
# determines the roots of the branches that are paused
|
907
|
+
# sends the resume message to them.
|
908
|
+
|
909
|
+
exps = ps(target).expressions.select { |fexp| fexp.state == 'paused' }
|
910
|
+
feis = exps.collect { |fexp| fexp.fei }
|
911
|
+
|
912
|
+
roots = exps.inject([]) { |a, fexp|
|
913
|
+
a << fexp.fei.h unless feis.include?(fexp.parent_id)
|
914
|
+
a
|
915
|
+
}
|
916
|
+
|
917
|
+
roots.each { |fei| @context.storage.put_msg('resume', 'fei' => fei) }
|
918
|
+
|
919
|
+
elsif target.is_a?(String)
|
920
|
+
#
|
921
|
+
# action targets a process instance (a string wfid)
|
922
|
+
|
923
|
+
@context.storage.put_msg(
|
924
|
+
"#{action}_process", opts.merge('wfid' => target))
|
925
|
+
|
926
|
+
elsif action == 'kill'
|
927
|
+
|
928
|
+
@context.storage.put_msg(
|
929
|
+
'cancel', opts.merge('fei' => target, 'flavour' => 'kill'))
|
930
|
+
|
931
|
+
else
|
932
|
+
|
933
|
+
@context.storage.put_msg(
|
934
|
+
action, opts.merge('fei' => target))
|
935
|
+
end
|
936
|
+
end
|
937
|
+
|
938
|
+
# Used by #process and #processes
|
939
|
+
#
|
940
|
+
def statuses(wfids, opts)
|
941
|
+
|
942
|
+
swfids = wfids.collect { |wfid| /!#{wfid}-\d+$/ }
|
943
|
+
|
944
|
+
exps = @context.storage.get_many('expressions', wfids).compact
|
945
|
+
swis = @context.storage.get_many('workitems', wfids).compact
|
946
|
+
errs = @context.storage.get_many('errors', wfids).compact
|
947
|
+
schs = @context.storage.get_many('schedules', swfids).compact
|
948
|
+
# some slow storages need the compaction... couch...
|
949
|
+
|
950
|
+
errs = errs.collect { |err| ProcessError.new(err) }
|
951
|
+
schs = schs.collect { |sch| Ruote.schedule_to_h(sch) }
|
952
|
+
|
953
|
+
by_wfid = {}
|
954
|
+
|
955
|
+
exps.each do |exp|
|
956
|
+
(by_wfid[exp['fei']['wfid']] ||= [ [], [], [], [] ])[0] << exp
|
957
|
+
end
|
958
|
+
swis.each do |swi|
|
959
|
+
(by_wfid[swi['fei']['wfid']] ||= [ [], [], [], [] ])[1] << swi
|
960
|
+
end
|
961
|
+
errs.each do |err|
|
962
|
+
(by_wfid[err.wfid] ||= [ [], [], [], [] ])[2] << err
|
963
|
+
end
|
964
|
+
schs.each do |sch|
|
965
|
+
(by_wfid[sch['wfid']] ||= [ [], [], [], [] ])[3] << sch
|
966
|
+
end
|
967
|
+
|
968
|
+
wfids = by_wfid.keys.sort
|
969
|
+
wfids = wfids.reverse if opts[:descending]
|
970
|
+
# re-adjust list of wfids, only take what was found
|
971
|
+
|
972
|
+
wfids.inject([]) { |a, wfid|
|
973
|
+
info = by_wfid[wfid]
|
974
|
+
a << ProcessStatus.new(@context, *info) if info
|
975
|
+
a
|
976
|
+
}
|
977
|
+
end
|
978
|
+
end
|
979
|
+
|
980
|
+
#
|
981
|
+
# A wrapper class giving easy access to engine variables.
|
982
|
+
#
|
983
|
+
# There is one instance of this class for an Engine instance. It is
|
984
|
+
# returned when calling Engine#variables.
|
985
|
+
#
|
986
|
+
class EngineVariables
|
987
|
+
|
988
|
+
def initialize(storage)
|
989
|
+
|
990
|
+
@storage = storage
|
991
|
+
end
|
992
|
+
|
993
|
+
def [](k)
|
994
|
+
|
995
|
+
@storage.get_engine_variable(k)
|
996
|
+
end
|
997
|
+
|
998
|
+
def []=(k, v)
|
999
|
+
|
1000
|
+
@storage.put_engine_variable(k, v)
|
1001
|
+
end
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
#
|
1005
|
+
# Engine#register uses this proxy when it's passed a block.
|
1006
|
+
#
|
1007
|
+
# Originally written by Torsten Schoenebaum for ruote-kit.
|
1008
|
+
#
|
1009
|
+
class ParticipantRegistrationProxy
|
1010
|
+
|
1011
|
+
def initialize(engine)
|
1012
|
+
|
1013
|
+
@engine = engine
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
def participant(name, klass=nil, options={}, &block)
|
1017
|
+
|
1018
|
+
options.merge!(:override => false)
|
1019
|
+
|
1020
|
+
@engine.register_participant(name, klass, options, &block)
|
1021
|
+
end
|
1022
|
+
|
1023
|
+
def catchall(*args)
|
1024
|
+
|
1025
|
+
klass = args.empty? ? Ruote::StorageParticipant : args.first
|
1026
|
+
options = args[1] || {}
|
1027
|
+
|
1028
|
+
participant('.+', klass, options)
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
alias catch_all catchall
|
1032
|
+
|
1033
|
+
# Maybe a bit audacious...
|
1034
|
+
#
|
1035
|
+
def method_missing(method_name, *args, &block)
|
1036
|
+
|
1037
|
+
participant(method_name, *args, &block)
|
1038
|
+
end
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
# Refines a schedule as found in the ruote storage into something a bit
|
1042
|
+
# easier to present.
|
1043
|
+
#
|
1044
|
+
def self.schedule_to_h(sched)
|
1045
|
+
|
1046
|
+
h = sched.dup
|
1047
|
+
|
1048
|
+
h.delete('_rev')
|
1049
|
+
h.delete('type')
|
1050
|
+
msg = h.delete('msg')
|
1051
|
+
owner = h.delete('owner')
|
1052
|
+
|
1053
|
+
h['wfid'] = owner['wfid']
|
1054
|
+
h['action'] = msg['action']
|
1055
|
+
h['type'] = msg['flavour']
|
1056
|
+
h['owner'] = Ruote::FlowExpressionId.new(owner)
|
1057
|
+
h['target'] = Ruote::FlowExpressionId.new(msg['fei'])
|
1058
|
+
|
1059
|
+
h
|
1060
|
+
end
|
1061
|
+
end
|
1062
|
+
|