ruote-maestrodev 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (265) hide show
  1. data/CHANGELOG.txt +290 -0
  2. data/CREDITS.txt +99 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.rdoc +88 -0
  5. data/Rakefile +108 -0
  6. data/TODO.txt +488 -0
  7. data/lib/ruote.rb +7 -0
  8. data/lib/ruote/context.rb +194 -0
  9. data/lib/ruote/engine.rb +1062 -0
  10. data/lib/ruote/engine/process_error.rb +122 -0
  11. data/lib/ruote/engine/process_status.rb +448 -0
  12. data/lib/ruote/exp/command.rb +87 -0
  13. data/lib/ruote/exp/commanded.rb +69 -0
  14. data/lib/ruote/exp/condition.rb +227 -0
  15. data/lib/ruote/exp/fe_add_branches.rb +138 -0
  16. data/lib/ruote/exp/fe_apply.rb +154 -0
  17. data/lib/ruote/exp/fe_cancel_process.rb +78 -0
  18. data/lib/ruote/exp/fe_command.rb +156 -0
  19. data/lib/ruote/exp/fe_concurrence.rb +321 -0
  20. data/lib/ruote/exp/fe_concurrent_iterator.rb +219 -0
  21. data/lib/ruote/exp/fe_cron.rb +141 -0
  22. data/lib/ruote/exp/fe_cursor.rb +324 -0
  23. data/lib/ruote/exp/fe_define.rb +112 -0
  24. data/lib/ruote/exp/fe_echo.rb +60 -0
  25. data/lib/ruote/exp/fe_equals.rb +115 -0
  26. data/lib/ruote/exp/fe_error.rb +82 -0
  27. data/lib/ruote/exp/fe_filter.rb +648 -0
  28. data/lib/ruote/exp/fe_forget.rb +88 -0
  29. data/lib/ruote/exp/fe_given.rb +154 -0
  30. data/lib/ruote/exp/fe_if.rb +127 -0
  31. data/lib/ruote/exp/fe_inc.rb +205 -0
  32. data/lib/ruote/exp/fe_iterator.rb +234 -0
  33. data/lib/ruote/exp/fe_let.rb +75 -0
  34. data/lib/ruote/exp/fe_listen.rb +304 -0
  35. data/lib/ruote/exp/fe_lose.rb +110 -0
  36. data/lib/ruote/exp/fe_noop.rb +45 -0
  37. data/lib/ruote/exp/fe_once.rb +215 -0
  38. data/lib/ruote/exp/fe_participant.rb +287 -0
  39. data/lib/ruote/exp/fe_read.rb +69 -0
  40. data/lib/ruote/exp/fe_redo.rb +82 -0
  41. data/lib/ruote/exp/fe_ref.rb +152 -0
  42. data/lib/ruote/exp/fe_registerp.rb +110 -0
  43. data/lib/ruote/exp/fe_reserve.rb +126 -0
  44. data/lib/ruote/exp/fe_restore.rb +102 -0
  45. data/lib/ruote/exp/fe_save.rb +72 -0
  46. data/lib/ruote/exp/fe_sequence.rb +59 -0
  47. data/lib/ruote/exp/fe_set.rb +154 -0
  48. data/lib/ruote/exp/fe_subprocess.rb +211 -0
  49. data/lib/ruote/exp/fe_that.rb +92 -0
  50. data/lib/ruote/exp/fe_undo.rb +67 -0
  51. data/lib/ruote/exp/fe_unregisterp.rb +69 -0
  52. data/lib/ruote/exp/fe_wait.rb +95 -0
  53. data/lib/ruote/exp/flowexpression.rb +886 -0
  54. data/lib/ruote/exp/iterator.rb +81 -0
  55. data/lib/ruote/exp/merge.rb +118 -0
  56. data/lib/ruote/exp/ro_attributes.rb +212 -0
  57. data/lib/ruote/exp/ro_filters.rb +136 -0
  58. data/lib/ruote/exp/ro_persist.rb +154 -0
  59. data/lib/ruote/exp/ro_variables.rb +189 -0
  60. data/lib/ruote/exp/ro_vf.rb +68 -0
  61. data/lib/ruote/fei.rb +260 -0
  62. data/lib/ruote/id/mnemo_wfid_generator.rb +43 -0
  63. data/lib/ruote/id/wfid_generator.rb +81 -0
  64. data/lib/ruote/log/default_history.rb +122 -0
  65. data/lib/ruote/log/pretty.rb +176 -0
  66. data/lib/ruote/log/storage_history.rb +159 -0
  67. data/lib/ruote/log/test_logger.rb +208 -0
  68. data/lib/ruote/log/wait_logger.rb +64 -0
  69. data/lib/ruote/part/block_participant.rb +137 -0
  70. data/lib/ruote/part/code_participant.rb +81 -0
  71. data/lib/ruote/part/engine_participant.rb +189 -0
  72. data/lib/ruote/part/local_participant.rb +138 -0
  73. data/lib/ruote/part/no_op_participant.rb +60 -0
  74. data/lib/ruote/part/null_participant.rb +54 -0
  75. data/lib/ruote/part/rev_participant.rb +169 -0
  76. data/lib/ruote/part/smtp_participant.rb +116 -0
  77. data/lib/ruote/part/storage_participant.rb +392 -0
  78. data/lib/ruote/part/template.rb +84 -0
  79. data/lib/ruote/participant.rb +7 -0
  80. data/lib/ruote/reader.rb +278 -0
  81. data/lib/ruote/reader/json.rb +49 -0
  82. data/lib/ruote/reader/radial.rb +290 -0
  83. data/lib/ruote/reader/ruby_dsl.rb +186 -0
  84. data/lib/ruote/reader/xml.rb +99 -0
  85. data/lib/ruote/receiver/base.rb +212 -0
  86. data/lib/ruote/storage/base.rb +364 -0
  87. data/lib/ruote/storage/composite_storage.rb +121 -0
  88. data/lib/ruote/storage/fs_storage.rb +139 -0
  89. data/lib/ruote/storage/hash_storage.rb +211 -0
  90. data/lib/ruote/svc/dispatch_pool.rb +158 -0
  91. data/lib/ruote/svc/dollar_sub.rb +298 -0
  92. data/lib/ruote/svc/error_handler.rb +138 -0
  93. data/lib/ruote/svc/expression_map.rb +97 -0
  94. data/lib/ruote/svc/participant_list.rb +397 -0
  95. data/lib/ruote/svc/tracker.rb +172 -0
  96. data/lib/ruote/svc/treechecker.rb +141 -0
  97. data/lib/ruote/tree_dot.rb +85 -0
  98. data/lib/ruote/util/filter.rb +525 -0
  99. data/lib/ruote/util/hashdot.rb +79 -0
  100. data/lib/ruote/util/look.rb +128 -0
  101. data/lib/ruote/util/lookup.rb +127 -0
  102. data/lib/ruote/util/misc.rb +167 -0
  103. data/lib/ruote/util/ometa.rb +71 -0
  104. data/lib/ruote/util/serializer.rb +103 -0
  105. data/lib/ruote/util/subprocess.rb +88 -0
  106. data/lib/ruote/util/time.rb +100 -0
  107. data/lib/ruote/util/tree.rb +58 -0
  108. data/lib/ruote/version.rb +29 -0
  109. data/lib/ruote/worker.rb +386 -0
  110. data/lib/ruote/workitem.rb +394 -0
  111. data/phil.txt +14 -0
  112. data/ruote.gemspec +44 -0
  113. data/test/bm/ci.rb +55 -0
  114. data/test/bm/ici.rb +71 -0
  115. data/test/bm/juuman.rb +54 -0
  116. data/test/bm/launch_bench.rb +37 -0
  117. data/test/bm/load_26c.rb +97 -0
  118. data/test/bm/mega.rb +64 -0
  119. data/test/bm/seq_thousand.rb +31 -0
  120. data/test/bm/t.rb +35 -0
  121. data/test/functional/base.rb +247 -0
  122. data/test/functional/concurrent_base.rb +98 -0
  123. data/test/functional/crunner.rb +31 -0
  124. data/test/functional/ct_0_concurrence.rb +65 -0
  125. data/test/functional/ct_1_iterator.rb +67 -0
  126. data/test/functional/ct_2_cancel.rb +81 -0
  127. data/test/functional/eft_0_process_definition.rb +65 -0
  128. data/test/functional/eft_10_cancel_process.rb +46 -0
  129. data/test/functional/eft_11_wait.rb +109 -0
  130. data/test/functional/eft_12_listen.rb +500 -0
  131. data/test/functional/eft_13_iterator.rb +342 -0
  132. data/test/functional/eft_14_cursor.rb +456 -0
  133. data/test/functional/eft_15_loop.rb +69 -0
  134. data/test/functional/eft_16_if.rb +183 -0
  135. data/test/functional/eft_17_equals.rb +55 -0
  136. data/test/functional/eft_18_concurrent_iterator.rb +410 -0
  137. data/test/functional/eft_19_reserve.rb +136 -0
  138. data/test/functional/eft_1_echo.rb +68 -0
  139. data/test/functional/eft_20_save.rb +116 -0
  140. data/test/functional/eft_21_restore.rb +61 -0
  141. data/test/functional/eft_22_noop.rb +28 -0
  142. data/test/functional/eft_23_apply.rb +168 -0
  143. data/test/functional/eft_24_add_branches.rb +98 -0
  144. data/test/functional/eft_25_command.rb +28 -0
  145. data/test/functional/eft_26_error.rb +77 -0
  146. data/test/functional/eft_27_inc.rb +280 -0
  147. data/test/functional/eft_28_once.rb +135 -0
  148. data/test/functional/eft_29_cron.rb +64 -0
  149. data/test/functional/eft_2_sequence.rb +58 -0
  150. data/test/functional/eft_30_ref.rb +155 -0
  151. data/test/functional/eft_31_registerp.rb +130 -0
  152. data/test/functional/eft_32_lose.rb +93 -0
  153. data/test/functional/eft_33_let.rb +31 -0
  154. data/test/functional/eft_34_given.rb +123 -0
  155. data/test/functional/eft_35_filter.rb +375 -0
  156. data/test/functional/eft_36_read.rb +95 -0
  157. data/test/functional/eft_3_participant.rb +149 -0
  158. data/test/functional/eft_4_set.rb +296 -0
  159. data/test/functional/eft_5_subprocess.rb +163 -0
  160. data/test/functional/eft_6_concurrence.rb +304 -0
  161. data/test/functional/eft_7_forget.rb +61 -0
  162. data/test/functional/eft_8_undo.rb +114 -0
  163. data/test/functional/eft_9_redo.rb +138 -0
  164. data/test/functional/ft_0_worker.rb +65 -0
  165. data/test/functional/ft_10_dollar.rb +304 -0
  166. data/test/functional/ft_11_recursion.rb +109 -0
  167. data/test/functional/ft_12_launchitem.rb +43 -0
  168. data/test/functional/ft_13_variables.rb +151 -0
  169. data/test/functional/ft_14_re_apply.rb +324 -0
  170. data/test/functional/ft_15_timeout.rb +226 -0
  171. data/test/functional/ft_16_participant_params.rb +98 -0
  172. data/test/functional/ft_17_conditional.rb +102 -0
  173. data/test/functional/ft_18_kill.rb +138 -0
  174. data/test/functional/ft_19_participant_code.rb +67 -0
  175. data/test/functional/ft_1_process_status.rb +796 -0
  176. data/test/functional/ft_20_storage_participant.rb +543 -0
  177. data/test/functional/ft_21_forget.rb +153 -0
  178. data/test/functional/ft_22_process_definitions.rb +90 -0
  179. data/test/functional/ft_23_load_defs.rb +79 -0
  180. data/test/functional/ft_24_block_participant.rb +235 -0
  181. data/test/functional/ft_25_receiver.rb +207 -0
  182. data/test/functional/ft_26_participant_rtimeout.rb +179 -0
  183. data/test/functional/ft_27_var_indirection.rb +128 -0
  184. data/test/functional/ft_28_null_noop_participants.rb +51 -0
  185. data/test/functional/ft_29_part_template.rb +60 -0
  186. data/test/functional/ft_2_errors.rb +380 -0
  187. data/test/functional/ft_30_smtp_participant.rb +122 -0
  188. data/test/functional/ft_31_part_blocking.rb +72 -0
  189. data/test/functional/ft_33_participant_subprocess_priority.rb +32 -0
  190. data/test/functional/ft_34_cursor_rewind.rb +101 -0
  191. data/test/functional/ft_35_add_service.rb +56 -0
  192. data/test/functional/ft_36_storage_history.rb +150 -0
  193. data/test/functional/ft_37_default_history.rb +109 -0
  194. data/test/functional/ft_38_participant_more.rb +193 -0
  195. data/test/functional/ft_39_wait_for.rb +136 -0
  196. data/test/functional/ft_3_participant_registration.rb +574 -0
  197. data/test/functional/ft_40_wait_logger.rb +62 -0
  198. data/test/functional/ft_41_participants.rb +91 -0
  199. data/test/functional/ft_42_storage_copy.rb +71 -0
  200. data/test/functional/ft_43_participant_on_reply.rb +87 -0
  201. data/test/functional/ft_44_var_participant.rb +35 -0
  202. data/test/functional/ft_45_participant_accept.rb +64 -0
  203. data/test/functional/ft_46_launch_single.rb +83 -0
  204. data/test/functional/ft_47_wfid_generator.rb +54 -0
  205. data/test/functional/ft_48_lose.rb +112 -0
  206. data/test/functional/ft_49_engine_on_error.rb +201 -0
  207. data/test/functional/ft_4_cancel.rb +132 -0
  208. data/test/functional/ft_50_engine_config.rb +22 -0
  209. data/test/functional/ft_51_misc.rb +67 -0
  210. data/test/functional/ft_52_case.rb +134 -0
  211. data/test/functional/ft_53_engine_on_terminate.rb +95 -0
  212. data/test/functional/ft_54_patterns.rb +104 -0
  213. data/test/functional/ft_55_engine_participant.rb +303 -0
  214. data/test/functional/ft_56_filter_attribute.rb +259 -0
  215. data/test/functional/ft_57_rev_participant.rb +252 -0
  216. data/test/functional/ft_58_workitem.rb +69 -0
  217. data/test/functional/ft_59_pause.rb +343 -0
  218. data/test/functional/ft_5_on_error.rb +384 -0
  219. data/test/functional/ft_60_code_participant.rb +45 -0
  220. data/test/functional/ft_61_trailing_fields.rb +34 -0
  221. data/test/functional/ft_62_exp_name_and_dollar_substitution.rb +35 -0
  222. data/test/functional/ft_6_on_cancel.rb +221 -0
  223. data/test/functional/ft_7_tags.rb +177 -0
  224. data/test/functional/ft_8_participant_consumption.rb +124 -0
  225. data/test/functional/ft_9_subprocesses.rb +146 -0
  226. data/test/functional/restart_base.rb +34 -0
  227. data/test/functional/rt_0_wait.rb +55 -0
  228. data/test/functional/rt_1_listen.rb +56 -0
  229. data/test/functional/rt_2_errors.rb +56 -0
  230. data/test/functional/rt_3_once.rb +70 -0
  231. data/test/functional/rt_4_cron.rb +64 -0
  232. data/test/functional/rt_5_timeout.rb +60 -0
  233. data/test/functional/rtest.rb +8 -0
  234. data/test/functional/storage_helper.rb +93 -0
  235. data/test/functional/test.rb +44 -0
  236. data/test/functional/vertical.rb +46 -0
  237. data/test/path_helper.rb +15 -0
  238. data/test/test.rb +13 -0
  239. data/test/test_helper.rb +28 -0
  240. data/test/unit/storage.rb +428 -0
  241. data/test/unit/storages.rb +37 -0
  242. data/test/unit/test.rb +28 -0
  243. data/test/unit/ut_0_ruby_reader.rb +223 -0
  244. data/test/unit/ut_11_lookup.rb +122 -0
  245. data/test/unit/ut_13_serializer.rb +65 -0
  246. data/test/unit/ut_14_is_uri.rb +28 -0
  247. data/test/unit/ut_15_util.rb +57 -0
  248. data/test/unit/ut_16_reader.rb +225 -0
  249. data/test/unit/ut_18_engine.rb +47 -0
  250. data/test/unit/ut_19_part_template.rb +86 -0
  251. data/test/unit/ut_1_fei.rb +165 -0
  252. data/test/unit/ut_20_composite_storage.rb +74 -0
  253. data/test/unit/ut_21_svc_participant_list.rb +46 -0
  254. data/test/unit/ut_22_filter.rb +1094 -0
  255. data/test/unit/ut_23_svc_tracker.rb +48 -0
  256. data/test/unit/ut_24_radial_reader.rb +332 -0
  257. data/test/unit/ut_25_merge.rb +113 -0
  258. data/test/unit/ut_3_wait_logger.rb +39 -0
  259. data/test/unit/ut_4_expmap.rb +20 -0
  260. data/test/unit/ut_5_tree.rb +54 -0
  261. data/test/unit/ut_6_condition.rb +303 -0
  262. data/test/unit/ut_7_workitem.rb +99 -0
  263. data/test/unit/ut_8_tree_to_dot.rb +72 -0
  264. data/test/unit/ut_9_xml_reader.rb +61 -0
  265. metadata +504 -0
@@ -0,0 +1,7 @@
1
+
2
+ require 'ruote/storage/hash_storage'
3
+ require 'ruote/worker'
4
+ require 'ruote/engine'
5
+ require 'ruote/participant'
6
+ require 'ruote/reader/ruby_dsl'
7
+
@@ -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
+
@@ -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
+