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.
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
+