ruote 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (305) hide show
  1. data/CHANGELOG.txt +166 -1
  2. data/CREDITS.txt +36 -17
  3. data/LICENSE.txt +1 -1
  4. data/README.rdoc +1 -7
  5. data/Rakefile +38 -29
  6. data/TODO.txt +93 -52
  7. data/lib/ruote-fs.rb +3 -0
  8. data/lib/ruote.rb +5 -1
  9. data/lib/ruote/context.rb +140 -35
  10. data/lib/ruote/dashboard.rb +1247 -0
  11. data/lib/ruote/{engine → dboard}/process_error.rb +22 -2
  12. data/lib/ruote/dboard/process_status.rb +587 -0
  13. data/lib/ruote/engine.rb +6 -871
  14. data/lib/ruote/exp/command.rb +7 -2
  15. data/lib/ruote/exp/commanded.rb +2 -2
  16. data/lib/ruote/exp/condition.rb +38 -13
  17. data/lib/ruote/exp/fe_add_branches.rb +1 -1
  18. data/lib/ruote/exp/fe_apply.rb +1 -1
  19. data/lib/ruote/exp/fe_await.rb +357 -0
  20. data/lib/ruote/exp/fe_cancel_process.rb +17 -3
  21. data/lib/ruote/exp/fe_command.rb +8 -4
  22. data/lib/ruote/exp/fe_concurrence.rb +218 -18
  23. data/lib/ruote/exp/fe_concurrent_iterator.rb +71 -10
  24. data/lib/ruote/exp/fe_cron.rb +3 -10
  25. data/lib/ruote/exp/fe_cursor.rb +14 -4
  26. data/lib/ruote/exp/fe_define.rb +3 -1
  27. data/lib/ruote/exp/fe_echo.rb +1 -1
  28. data/lib/ruote/exp/fe_equals.rb +1 -1
  29. data/lib/ruote/exp/fe_error.rb +1 -1
  30. data/lib/ruote/exp/fe_filter.rb +163 -4
  31. data/lib/ruote/exp/fe_forget.rb +21 -4
  32. data/lib/ruote/exp/fe_given.rb +1 -1
  33. data/lib/ruote/exp/fe_if.rb +1 -1
  34. data/lib/ruote/exp/fe_inc.rb +102 -35
  35. data/lib/ruote/exp/fe_iterator.rb +47 -12
  36. data/lib/ruote/exp/fe_listen.rb +96 -11
  37. data/lib/ruote/exp/fe_lose.rb +31 -4
  38. data/lib/ruote/exp/fe_noop.rb +1 -1
  39. data/lib/ruote/exp/fe_on_error.rb +109 -0
  40. data/lib/ruote/exp/fe_once.rb +10 -19
  41. data/lib/ruote/exp/fe_participant.rb +90 -28
  42. data/lib/ruote/exp/fe_read.rb +69 -0
  43. data/lib/ruote/exp/fe_redo.rb +3 -2
  44. data/lib/ruote/exp/fe_ref.rb +57 -27
  45. data/lib/ruote/exp/fe_registerp.rb +1 -3
  46. data/lib/ruote/exp/fe_reserve.rb +1 -1
  47. data/lib/ruote/exp/fe_restore.rb +6 -6
  48. data/lib/ruote/exp/fe_save.rb +12 -19
  49. data/lib/ruote/exp/fe_sequence.rb +38 -2
  50. data/lib/ruote/exp/fe_set.rb +143 -40
  51. data/lib/ruote/exp/{fe_let.rb → fe_stall.rb} +7 -38
  52. data/lib/ruote/exp/fe_subprocess.rb +8 -2
  53. data/lib/ruote/exp/fe_that.rb +1 -1
  54. data/lib/ruote/exp/fe_undo.rb +40 -4
  55. data/lib/ruote/exp/fe_unregisterp.rb +1 -3
  56. data/lib/ruote/exp/fe_wait.rb +12 -25
  57. data/lib/ruote/exp/{flowexpression.rb → flow_expression.rb} +375 -229
  58. data/lib/ruote/exp/iterator.rb +2 -2
  59. data/lib/ruote/exp/merge.rb +78 -17
  60. data/lib/ruote/exp/ro_attributes.rb +46 -36
  61. data/lib/ruote/exp/ro_filters.rb +34 -8
  62. data/lib/ruote/exp/ro_on_x.rb +431 -0
  63. data/lib/ruote/exp/ro_persist.rb +19 -7
  64. data/lib/ruote/exp/ro_timers.rb +123 -0
  65. data/lib/ruote/exp/ro_variables.rb +90 -29
  66. data/lib/ruote/fei.rb +57 -3
  67. data/lib/ruote/fs.rb +3 -0
  68. data/lib/ruote/id/mnemo_wfid_generator.rb +30 -7
  69. data/lib/ruote/id/wfid_generator.rb +17 -38
  70. data/lib/ruote/log/default_history.rb +23 -9
  71. data/lib/ruote/log/fancy_printing.rb +265 -0
  72. data/lib/ruote/log/storage_history.rb +23 -13
  73. data/lib/ruote/log/wait_logger.rb +224 -17
  74. data/lib/ruote/observer.rb +82 -0
  75. data/lib/ruote/part/block_participant.rb +65 -28
  76. data/lib/ruote/part/code_participant.rb +81 -0
  77. data/lib/ruote/part/engine_participant.rb +7 -2
  78. data/lib/ruote/part/local_participant.rb +221 -21
  79. data/lib/ruote/part/no_op_participant.rb +1 -1
  80. data/lib/ruote/part/null_participant.rb +1 -1
  81. data/lib/ruote/part/participant.rb +50 -0
  82. data/lib/ruote/part/rev_participant.rb +178 -0
  83. data/lib/ruote/part/smtp_participant.rb +2 -2
  84. data/lib/ruote/part/storage_participant.rb +228 -60
  85. data/lib/ruote/part/template.rb +1 -1
  86. data/lib/ruote/participant.rb +2 -0
  87. data/lib/ruote/reader.rb +205 -68
  88. data/lib/ruote/reader/json.rb +49 -0
  89. data/lib/ruote/reader/radial.rb +303 -0
  90. data/lib/ruote/reader/ruby_dsl.rb +44 -9
  91. data/lib/ruote/reader/xml.rb +11 -8
  92. data/lib/ruote/receiver/base.rb +98 -45
  93. data/lib/ruote/storage/base.rb +104 -35
  94. data/lib/ruote/storage/composite_storage.rb +50 -60
  95. data/lib/ruote/storage/fs_storage.rb +25 -34
  96. data/lib/ruote/storage/hash_storage.rb +38 -36
  97. data/lib/ruote/svc/dispatch_pool.rb +104 -35
  98. data/lib/ruote/svc/dollar_sub.rb +10 -8
  99. data/lib/ruote/svc/error_handler.rb +108 -52
  100. data/lib/ruote/svc/expression_map.rb +3 -3
  101. data/lib/ruote/svc/participant_list.rb +160 -55
  102. data/lib/ruote/svc/tracker.rb +31 -31
  103. data/lib/ruote/svc/treechecker.rb +28 -16
  104. data/lib/ruote/tree_dot.rb +1 -1
  105. data/lib/ruote/util/deep.rb +143 -0
  106. data/lib/ruote/util/filter.rb +125 -18
  107. data/lib/ruote/util/hashdot.rb +15 -13
  108. data/lib/ruote/util/look.rb +1 -1
  109. data/lib/ruote/util/lookup.rb +60 -22
  110. data/lib/ruote/util/misc.rb +63 -18
  111. data/lib/ruote/util/mpatch.rb +53 -0
  112. data/lib/ruote/util/ometa.rb +1 -2
  113. data/lib/ruote/util/process_observer.rb +177 -0
  114. data/lib/ruote/util/subprocess.rb +1 -1
  115. data/lib/ruote/util/time.rb +2 -2
  116. data/lib/ruote/util/tree.rb +64 -2
  117. data/lib/ruote/version.rb +3 -2
  118. data/lib/ruote/worker.rb +421 -92
  119. data/lib/ruote/workitem.rb +157 -22
  120. data/ruote.gemspec +15 -9
  121. data/test/bm/ci.rb +0 -2
  122. data/test/bm/ici.rb +0 -2
  123. data/test/bm/load_26c.rb +0 -3
  124. data/test/bm/mega.rb +0 -2
  125. data/test/functional/base.rb +57 -43
  126. data/test/functional/concurrent_base.rb +16 -13
  127. data/test/functional/ct_0_concurrence.rb +7 -11
  128. data/test/functional/ct_1_iterator.rb +9 -11
  129. data/test/functional/ct_2_cancel.rb +28 -17
  130. data/test/functional/eft_0_flow_expression.rb +35 -0
  131. data/test/functional/eft_10_cancel_process.rb +1 -1
  132. data/test/functional/eft_11_wait.rb +13 -13
  133. data/test/functional/eft_12_listen.rb +199 -66
  134. data/test/functional/eft_13_iterator.rb +95 -29
  135. data/test/functional/eft_14_cursor.rb +74 -24
  136. data/test/functional/eft_15_loop.rb +7 -7
  137. data/test/functional/eft_16_if.rb +1 -1
  138. data/test/functional/eft_17_equals.rb +1 -1
  139. data/test/functional/eft_18_concurrent_iterator.rb +156 -68
  140. data/test/functional/eft_19_reserve.rb +15 -15
  141. data/test/functional/eft_1_echo.rb +1 -1
  142. data/test/functional/eft_20_save.rb +51 -9
  143. data/test/functional/eft_21_restore.rb +1 -1
  144. data/test/functional/eft_22_noop.rb +1 -1
  145. data/test/functional/eft_23_apply.rb +1 -1
  146. data/test/functional/eft_24_add_branches.rb +7 -8
  147. data/test/functional/eft_25_command.rb +1 -1
  148. data/test/functional/eft_26_error.rb +11 -11
  149. data/test/functional/eft_27_inc.rb +111 -67
  150. data/test/functional/eft_28_once.rb +16 -16
  151. data/test/functional/eft_29_cron.rb +9 -9
  152. data/test/functional/eft_2_sequence.rb +23 -4
  153. data/test/functional/eft_30_ref.rb +36 -24
  154. data/test/functional/eft_31_registerp.rb +24 -24
  155. data/test/functional/eft_32_lose.rb +46 -20
  156. data/test/functional/eft_34_given.rb +1 -1
  157. data/test/functional/eft_35_filter.rb +161 -7
  158. data/test/functional/eft_36_read.rb +97 -0
  159. data/test/functional/{eft_0_process_definition.rb → eft_37_process_definition.rb} +4 -4
  160. data/test/functional/eft_38_on_error.rb +195 -0
  161. data/test/functional/eft_39_stall.rb +35 -0
  162. data/test/functional/eft_3_participant.rb +77 -22
  163. data/test/functional/eft_40_await.rb +297 -0
  164. data/test/functional/eft_4_set.rb +110 -11
  165. data/test/functional/eft_5_subprocess.rb +27 -5
  166. data/test/functional/eft_6_concurrence.rb +299 -60
  167. data/test/functional/eft_7_forget.rb +24 -22
  168. data/test/functional/eft_8_undo.rb +52 -15
  169. data/test/functional/eft_9_redo.rb +18 -20
  170. data/test/functional/ft_0_worker.rb +122 -13
  171. data/test/functional/ft_10_dollar.rb +77 -16
  172. data/test/functional/ft_11_recursion.rb +9 -9
  173. data/test/functional/ft_12_launchitem.rb +7 -9
  174. data/test/functional/ft_13_variables.rb +125 -22
  175. data/test/functional/ft_14_re_apply.rb +112 -56
  176. data/test/functional/ft_15_timeout.rb +64 -33
  177. data/test/functional/ft_16_participant_params.rb +59 -6
  178. data/test/functional/ft_17_conditional.rb +68 -2
  179. data/test/functional/ft_18_kill.rb +48 -30
  180. data/test/functional/ft_19_participant_code.rb +67 -0
  181. data/test/functional/ft_1_process_status.rb +222 -150
  182. data/test/functional/ft_20_storage_participant.rb +445 -44
  183. data/test/functional/ft_21_forget.rb +21 -26
  184. data/test/functional/ft_22_process_definitions.rb +8 -6
  185. data/test/functional/ft_23_load_defs.rb +29 -5
  186. data/test/functional/ft_24_block_participant.rb +199 -20
  187. data/test/functional/ft_25_receiver.rb +98 -46
  188. data/test/functional/ft_26_participant_rtimeout.rb +34 -26
  189. data/test/functional/ft_27_var_indirection.rb +40 -5
  190. data/test/functional/ft_28_null_noop_participants.rb +5 -5
  191. data/test/functional/ft_29_part_template.rb +2 -2
  192. data/test/functional/ft_2_errors.rb +106 -74
  193. data/test/functional/ft_30_smtp_participant.rb +7 -7
  194. data/test/functional/ft_31_part_blocking.rb +11 -11
  195. data/test/functional/ft_32_scope.rb +50 -0
  196. data/test/functional/ft_33_participant_subprocess_priority.rb +3 -3
  197. data/test/functional/ft_34_cursor_rewind.rb +14 -14
  198. data/test/functional/ft_35_add_service.rb +67 -9
  199. data/test/functional/ft_36_storage_history.rb +92 -24
  200. data/test/functional/ft_37_default_history.rb +35 -23
  201. data/test/functional/ft_38_participant_more.rb +189 -32
  202. data/test/functional/ft_39_wait_for.rb +25 -25
  203. data/test/functional/ft_3_participant_registration.rb +235 -107
  204. data/test/functional/ft_40_wait_logger.rb +105 -18
  205. data/test/functional/ft_41_participants.rb +13 -12
  206. data/test/functional/ft_42_storage_copy.rb +12 -12
  207. data/test/functional/ft_43_participant_on_reply.rb +85 -11
  208. data/test/functional/ft_44_var_participant.rb +5 -5
  209. data/test/functional/ft_45_participant_accept.rb +3 -3
  210. data/test/functional/ft_46_launch_single.rb +17 -17
  211. data/test/functional/ft_47_wfids.rb +41 -0
  212. data/test/functional/ft_48_lose.rb +19 -25
  213. data/test/functional/ft_49_engine_on_error.rb +54 -70
  214. data/test/functional/ft_4_cancel.rb +84 -26
  215. data/test/functional/ft_50_engine_config.rb +4 -4
  216. data/test/functional/ft_51_misc.rb +12 -12
  217. data/test/functional/ft_52_case.rb +17 -17
  218. data/test/functional/ft_53_engine_on_terminate.rb +18 -21
  219. data/test/functional/ft_54_patterns.rb +18 -16
  220. data/test/functional/ft_55_engine_participant.rb +55 -55
  221. data/test/functional/ft_56_filter_attribute.rb +90 -52
  222. data/test/functional/ft_57_rev_participant.rb +252 -0
  223. data/test/functional/ft_58_workitem.rb +150 -0
  224. data/test/functional/ft_59_pause.rb +329 -0
  225. data/test/functional/ft_5_on_error.rb +430 -77
  226. data/test/functional/ft_60_code_participant.rb +65 -0
  227. data/test/functional/ft_61_trailing_fields.rb +34 -0
  228. data/test/functional/ft_62_exp_name_and_dollar_substitution.rb +35 -0
  229. data/test/functional/ft_63_participants_221.rb +458 -0
  230. data/test/functional/ft_64_stash.rb +41 -0
  231. data/test/functional/ft_65_timers.rb +313 -0
  232. data/test/functional/ft_66_flank.rb +133 -0
  233. data/test/functional/ft_67_radial_misc.rb +34 -0
  234. data/test/functional/ft_68_reput.rb +72 -0
  235. data/test/functional/ft_69_worker_info.rb +56 -0
  236. data/test/functional/ft_6_on_cancel.rb +189 -36
  237. data/test/functional/ft_70_take_and_discard_attributes.rb +94 -0
  238. data/test/functional/ft_71_retries.rb +144 -0
  239. data/test/functional/ft_72_on_terminate.rb +60 -0
  240. data/test/functional/ft_73_raise_msg.rb +107 -0
  241. data/test/functional/ft_74_respark.rb +106 -0
  242. data/test/functional/ft_75_context.rb +66 -0
  243. data/test/functional/ft_76_observer.rb +53 -0
  244. data/test/functional/ft_77_process_observer.rb +157 -0
  245. data/test/functional/ft_78_part_participant.rb +37 -0
  246. data/test/functional/ft_7_tags.rb +238 -50
  247. data/test/functional/ft_8_participant_consumption.rb +27 -21
  248. data/test/functional/ft_9_subprocesses.rb +48 -18
  249. data/test/functional/restart_base.rb +4 -6
  250. data/test/functional/rt_0_wait.rb +10 -10
  251. data/test/functional/rt_1_listen.rb +6 -6
  252. data/test/functional/rt_2_errors.rb +12 -12
  253. data/test/functional/rt_3_once.rb +17 -12
  254. data/test/functional/rt_4_cron.rb +17 -17
  255. data/test/functional/rt_5_timeout.rb +13 -13
  256. data/test/functional/signals.rb +103 -0
  257. data/test/functional/storage.rb +730 -0
  258. data/test/functional/storage_helper.rb +48 -35
  259. data/test/functional/test.rb +6 -2
  260. data/test/misc/idle.rb +21 -0
  261. data/test/misc/light.rb +29 -0
  262. data/test/path_helper.rb +1 -1
  263. data/test/test.rb +2 -5
  264. data/test/test_helper.rb +13 -0
  265. data/test/unit/test.rb +1 -4
  266. data/test/unit/ut_0_ruby_reader.rb +25 -9
  267. data/test/unit/ut_10_participants.rb +47 -0
  268. data/test/unit/ut_11_lookup.rb +59 -2
  269. data/test/unit/ut_12_wait_logger.rb +123 -0
  270. data/test/unit/ut_14_is_uri.rb +1 -1
  271. data/test/unit/ut_15_util.rb +1 -1
  272. data/test/unit/ut_16_reader.rb +136 -14
  273. data/test/unit/ut_17_merge.rb +155 -0
  274. data/test/unit/ut_19_part_template.rb +1 -1
  275. data/test/unit/ut_1_fei.rb +11 -2
  276. data/test/unit/ut_20_composite_storage.rb +27 -1
  277. data/test/unit/{ut_21_participant_list.rb → ut_21_svc_participant_list.rb} +2 -3
  278. data/test/unit/ut_22_filter.rb +231 -10
  279. data/test/unit/ut_23_svc_tracker.rb +48 -0
  280. data/test/unit/ut_24_radial_reader.rb +458 -0
  281. data/test/unit/ut_25_process_status.rb +143 -0
  282. data/test/unit/ut_26_deep.rb +131 -0
  283. data/test/unit/ut_2_dashboard.rb +114 -0
  284. data/test/unit/ut_3_worker.rb +54 -0
  285. data/test/unit/ut_4_expmap.rb +1 -1
  286. data/test/unit/ut_5_tree.rb +23 -23
  287. data/test/unit/ut_6_condition.rb +71 -29
  288. data/test/unit/ut_7_workitem.rb +18 -4
  289. data/test/unit/ut_8_tree_to_dot.rb +1 -1
  290. data/test/unit/ut_9_xml_reader.rb +1 -1
  291. metadata +142 -63
  292. data/jruby_issue.txt +0 -32
  293. data/lib/ruote/engine/process_status.rb +0 -403
  294. data/lib/ruote/log/pretty.rb +0 -165
  295. data/lib/ruote/log/test_logger.rb +0 -204
  296. data/lib/ruote/util/serializer.rb +0 -103
  297. data/phil.txt +0 -14
  298. data/test/functional/eft_33_let.rb +0 -31
  299. data/test/functional/ft_19_alias.rb +0 -33
  300. data/test/functional/ft_47_wfid_generator.rb +0 -54
  301. data/test/unit/storage.rb +0 -403
  302. data/test/unit/storages.rb +0 -37
  303. data/test/unit/ut_13_serializer.rb +0 -65
  304. data/test/unit/ut_18_engine.rb +0 -47
  305. data/test/unit/ut_3_wait_logger.rb +0 -39
@@ -0,0 +1,3 @@
1
+
2
+ require 'ruote/storage/fs_storage.rb'
3
+
@@ -1,7 +1,11 @@
1
1
 
2
+ require 'ruote/util/deep'
3
+ require 'ruote/util/lookup'
4
+ require 'ruote/util/mpatch'
2
5
  require 'ruote/storage/hash_storage'
3
6
  require 'ruote/worker'
4
- require 'ruote/engine'
7
+ require 'ruote/engine' # for backward compatibility
8
+ require 'ruote/dashboard'
5
9
  require 'ruote/participant'
6
10
  require 'ruote/reader/ruby_dsl'
7
11
 
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2005-2011, John Mettraux, jmettraux@gmail.com
2
+ # Copyright (c) 2005-2012, John Mettraux, jmettraux@gmail.com
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -37,19 +37,16 @@ module Ruote
37
37
  SERVICE_PREFIX = /^s\_/
38
38
 
39
39
  attr_reader :storage
40
- attr_accessor :worker
41
- attr_accessor :engine
40
+ attr_accessor :dashboard
42
41
 
43
- def initialize(storage, worker=nil)
42
+ def initialize(storage)
44
43
 
45
44
  @storage = storage
46
45
  @storage.context = self
47
46
 
48
- @engine = nil
49
- @worker = worker
47
+ @dashboard = nil
50
48
 
51
49
  @services = {}
52
-
53
50
  initialize_services
54
51
  end
55
52
 
@@ -66,12 +63,25 @@ module Ruote
66
63
  self
67
64
  end
68
65
 
66
+ # Let's make sure Context always responds to #storage, #dashboard (#engine)
67
+ # and #worker.
68
+ #
69
+ alias engine dashboard
70
+
71
+ # Let's make sure Context always responds to #storage, #dashboard (#engine)
72
+ # and #worker.
73
+ #
74
+ def worker
75
+
76
+ @services['s_worker']
77
+ end
78
+
69
79
  # Returns the engine_id (as set in the configuration under the key
70
80
  # "engine_id"), or, by default, "engine".
71
81
  #
72
82
  def engine_id
73
83
 
74
- get_conf['engine_id'] || 'engine'
84
+ conf['engine_id'] || 'engine'
75
85
  end
76
86
 
77
87
  # Used for things like
@@ -82,7 +92,11 @@ module Ruote
82
92
  #
83
93
  def [](key)
84
94
 
85
- SERVICE_PREFIX.match(key) ? @services[key] : get_conf[key]
95
+ if SERVICE_PREFIX.match(key)
96
+ @services[key]
97
+ else
98
+ conf[key]
99
+ end
86
100
  end
87
101
 
88
102
  # Mostly used by engine#configure
@@ -93,74 +107,140 @@ module Ruote
93
107
  ArgumentError.new('use context#add_service to register services')
94
108
  ) if SERVICE_PREFIX.match(key)
95
109
 
96
- cf = get_conf
97
- cf[key] = value
98
- @storage.put(cf)
110
+ @storage.put(conf.merge(key => value))
111
+ # TODO blindly trust the put ? retry in case of failure ?
99
112
 
100
113
  value
101
114
  end
102
115
 
116
+ # Configuration keys and service keys.
117
+ #
103
118
  def keys
104
119
 
105
- get_conf.keys
120
+ (@services.keys + conf.keys).uniq.sort
106
121
  end
107
122
 
123
+ # Called by Ruote::Dashboard#add_service
124
+ #
108
125
  def add_service(key, *args)
109
126
 
127
+ raise ArgumentError.new(
128
+ '#add_service: at least two arguments please'
129
+ ) if args.empty?
130
+
131
+ key = key.to_s
110
132
  path, klass, opts = args
111
133
 
112
134
  key = "s_#{key}" unless SERVICE_PREFIX.match(key)
113
135
 
114
- service = if klass
136
+ aa = [ self, opts ].compact
115
137
 
116
- require(path) if path
138
+ service = if klass
117
139
 
118
- aa = [ self ]
119
- aa << opts if opts
140
+ require(path)
120
141
 
121
142
  @services[key] = Ruote.constantize(klass).new(*aa)
143
+
144
+ elsif path.is_a?(Class)
145
+
146
+ @services[key] = path.new(*aa)
147
+
122
148
  else
123
149
 
124
150
  @services[key] = path
125
151
  end
126
152
 
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
153
+ (class << self; self; end).class_eval(
154
+ %{ def #{key[2..-1]}; @services['#{key}']; end })
155
+ #
156
+ # This 'two-liner' will add an instance method to Context for this
157
+ # service.
158
+ #
159
+ # If the service key is 's_dishwasher', then the service will be
160
+ # available via Context#dishwasher.
161
+ #
162
+ # I.e. dishwasher = engine.context.dishwasher
136
163
 
137
164
  service
138
165
  end
139
166
 
167
+ # This is kind of evil. Notifies services responding to #on_pre_msg
168
+ # with the msg before it gets processed.
169
+ #
170
+ # Might be useful in some cases. Use with great care.
171
+ #
172
+ def pre_notify(msg)
173
+
174
+ @services.select { |n, s|
175
+ s.respond_to?(:on_pre_msg)
176
+ }.sort_by { |n, s|
177
+ n
178
+ }.each { |n, s|
179
+ s.on_pre_msg(msg)
180
+ }
181
+ end
182
+
183
+ # This method is called by the worker each time it sucessfully processed
184
+ # a msg. This method calls in turn the #on_msg method for each of the
185
+ # services (that respond to that method).
186
+ #
187
+ # Makes sure that observers that respond to #wait_for are called last.
188
+ #
189
+ def notify(msg)
190
+
191
+ waiters, observers = @services.select { |n, s|
192
+ s.respond_to?(:on_msg)
193
+ }.sort_by { |n, s|
194
+ n
195
+ }.partition { |n, s|
196
+ s.respond_to?(:wait_for)
197
+ }
198
+
199
+ (observers + waiters).each { |n, s| s.on_msg(msg) }
200
+ end
201
+
140
202
  # Takes care of shutting down every service registered in this context.
141
203
  #
142
204
  def shutdown
143
205
 
144
- @worker.shutdown if @worker
145
- @storage.shutdown if @storage.respond_to?(:shutdown)
206
+ ([ @storage ] + @services.values).each do |s|
207
+ s.shutdown if s.respond_to?(:shutdown)
208
+ end
209
+ end
210
+
211
+ alias engine dashboard
212
+ alias engine= dashboard=
213
+
214
+ # Returns true if this context has a given service registered.
215
+ #
216
+ def has_service?(service_name)
217
+
218
+ service_name = service_name.to_s
219
+ service_name = "s_#{service_name}" if ! SERVICE_PREFIX.match(service_name)
220
+
221
+ @services.has_key?(service_name)
222
+ end
223
+
224
+ # List of services in this context, sorted by their name in alphabetical
225
+ # order.
226
+ #
227
+ def services
146
228
 
147
- @services.values.each { |s| s.shutdown if s.respond_to?(:shutdown) }
229
+ @services.keys.sort.collect { |k| @services[k] }
148
230
  end
149
231
 
150
232
  protected
151
233
 
152
- def get_conf
234
+ def conf
153
235
 
154
- @storage.get_configuration('engine') || {}
236
+ @storage.get_configuration('engine')
155
237
  end
156
238
 
157
239
  def initialize_services
158
240
 
159
- default_conf.merge(get_conf).each do |key, value|
241
+ default_conf.merge(conf).each do |key, value|
160
242
 
161
- next unless SERVICE_PREFIX.match(key)
162
-
163
- add_service(key, *value)
243
+ add_service(key, *value) if SERVICE_PREFIX.match(key)
164
244
  end
165
245
  end
166
246
 
@@ -190,5 +270,30 @@ module Ruote
190
270
  'ruote/log/default_history', 'Ruote::DefaultHistory' ] }
191
271
  end
192
272
  end
273
+
274
+ #
275
+ # A minimal context, useful for testing expressions in isolation.
276
+ #
277
+ class TestContext < Context
278
+
279
+ def initialize
280
+
281
+ @services = {}
282
+ initialize_services
283
+ end
284
+
285
+ protected
286
+
287
+ def conf
288
+
289
+ {}
290
+ end
291
+
292
+ def default_conf
293
+
294
+ { 's_dollar_sub' => [
295
+ 'ruote/svc/dollar_sub', 'Ruote::DollarSubstitution' ] }
296
+ end
297
+ end
193
298
  end
194
299
 
@@ -0,0 +1,1247 @@
1
+ #--
2
+ # Copyright (c) 2005-2012, 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/util/ometa'
27
+ require 'ruote/receiver/base'
28
+ require 'ruote/dboard/process_status'
29
+
30
+
31
+ module Ruote
32
+
33
+ #
34
+ # This class was once named 'Engine', but since ruote 2.x and its introduction
35
+ # of workers, the methods here are those of a "dashboard". The real engine
36
+ # being the set of workers.
37
+ #
38
+ # The methods here allow to launch processes
39
+ # and to query about their status. There are also methods for fixing
40
+ # issues with stalled processes or processes stuck in errors.
41
+ #
42
+ # NOTE : the methods #launch and #reply are implemented in
43
+ # Ruote::ReceiverMixin (this Engine class has all the methods of a Receiver).
44
+ #
45
+ class Dashboard
46
+
47
+ include ReceiverMixin
48
+
49
+ attr_reader :context
50
+ attr_reader :variables
51
+
52
+ # Creates an engine using either worker or storage.
53
+ #
54
+ # If a storage instance is given as the first argument, the engine will be
55
+ # able to manage processes (for example, launch and cancel workflows) but
56
+ # will not actually run any workflows.
57
+ #
58
+ # If a worker instance is given as the first argument and the second
59
+ # argument is true, engine will start the worker and will be able to both
60
+ # manage and run workflows.
61
+ #
62
+ # If the second options is set to { :join => true }, the worker will
63
+ # be started and run in the current thread (and the initialize method
64
+ # will not return).
65
+ #
66
+ def initialize(worker_or_storage, opts=true)
67
+
68
+ @context = worker_or_storage.context
69
+ @context.dashboard = self
70
+
71
+ @variables = EngineVariables.new(@context.storage)
72
+
73
+ workers = @context.services.select { |ser|
74
+ ser.respond_to?(:run) && ser.respond_to?(:run_in_thread)
75
+ }
76
+
77
+ return unless opts && workers.any?
78
+
79
+ # let's isolate a worker to join
80
+
81
+ worker = if opts.is_a?(Hash) && opts[:join]
82
+ workers.find { |wor| wor.name == 'worker' } || workers.first
83
+ else
84
+ nil
85
+ end
86
+
87
+ (workers - Array(worker)).each { |wor| wor.run_in_thread }
88
+ # launch their thread, but let's not join them
89
+
90
+ worker.run if worker
91
+ # and let's not return
92
+ end
93
+
94
+ # Returns the storage this engine works with passed at engine
95
+ # initialization.
96
+ #
97
+ def storage
98
+
99
+ @context.storage
100
+ end
101
+
102
+ # Returns the worker nested inside this engine (passed at initialization).
103
+ # Returns nil if this engine is only linked to a storage (and the worker
104
+ # is running somewhere else (hopefully)).
105
+ #
106
+ def worker
107
+
108
+ @context.worker
109
+ end
110
+
111
+ # A shortcut for engine.context.history
112
+ #
113
+ def history
114
+
115
+ @context.history
116
+ end
117
+
118
+ # A shortcut for engine.context.logger
119
+ #
120
+ def logger
121
+
122
+ @context.logger
123
+ end
124
+
125
+ # Quick note : the implementation of launch is found in the module
126
+ # Ruote::ReceiverMixin that the engine includes.
127
+ #
128
+ # Some processes have to have one and only one instance of themselves
129
+ # running, these are called 'singles' ('singleton' is too object-oriented).
130
+ #
131
+ # When called, this method will check if an instance of the pdef is
132
+ # already running (it uses the process definition name attribute), if
133
+ # yes, it will return without having launched anything. If there is no
134
+ # such process running, it will launch it (and register it).
135
+ #
136
+ # Returns the wfid (workflow instance id) of the running single.
137
+ #
138
+ def launch_single(process_definition, fields={}, variables={}, root_stash=nil)
139
+
140
+ tree = @context.reader.read(process_definition)
141
+ name = tree[1]['name'] || (tree[1].find { |k, v| v.nil? } || []).first
142
+
143
+ raise ArgumentError.new(
144
+ 'process definition is missing a name, cannot launch as single'
145
+ ) unless name
146
+
147
+ singles = @context.storage.get('variables', 'singles') || {
148
+ '_id' => 'singles', 'type' => 'variables', 'h' => {}
149
+ }
150
+ wfid, timestamp = singles['h'][name]
151
+
152
+ return wfid if wfid && (ps(wfid) || Time.now.to_f - timestamp < 1.0)
153
+ # return wfid if 'singleton' process is already running
154
+
155
+ wfid = @context.wfidgen.generate
156
+
157
+ singles['h'][name] = [ wfid, Time.now.to_f ]
158
+
159
+ r = @context.storage.put(singles)
160
+
161
+ return launch_single(tree, fields, variables, root_stash) unless r.nil?
162
+ #
163
+ # the put failed, back to the start...
164
+ #
165
+ # all this to prevent races between multiple engines,
166
+ # multiple launch_single calls (from different Ruby runtimes)
167
+
168
+ # ... green for launch
169
+
170
+ @context.storage.put_msg(
171
+ 'launch',
172
+ 'wfid' => wfid,
173
+ 'tree' => tree,
174
+ 'workitem' => { 'fields' => fields },
175
+ 'variables' => variables,
176
+ 'stash' => root_stash)
177
+
178
+ wfid
179
+ end
180
+
181
+ # Given a workitem or a fei, will do a cancel_expression,
182
+ # else it's a wfid and it does a cancel_process.
183
+ #
184
+ # == A note about opts
185
+ #
186
+ # They will get passed as is in the underlying 'msg',
187
+ # it can be useful to flag the message for historical purposes as in
188
+ #
189
+ # dashboard.cancel(wfid, 'reason' => 'cleanup', 'user' => current_user)
190
+ #
191
+ def cancel(wi_or_fei_or_wfid, opts={})
192
+
193
+ do_misc('cancel', wi_or_fei_or_wfid, opts)
194
+ end
195
+
196
+ alias cancel_process cancel
197
+ alias cancel_expression cancel
198
+
199
+ # Given a workitem or a fei, will do a kill_expression,
200
+ # else it's a wfid and it does a kill_process.
201
+ #
202
+ # (also see notes about opts for #cancel)
203
+ #
204
+ def kill(wi_or_fei_or_wfid, opts={})
205
+
206
+ do_misc('cancel', wi_or_fei_or_wfid, opts.merge('flavour' => 'kill'))
207
+ end
208
+
209
+ alias kill_process kill
210
+ alias kill_expression kill
211
+
212
+ # Removes a process by removing all its schedules, expressions, errors,
213
+ # workitems and trackers.
214
+ #
215
+ # Warning: will not trigger any cancel behaviours at all, just removes
216
+ # the process.
217
+ #
218
+ def remove_process(wfid)
219
+
220
+ @context.storage.remove_process(wfid)
221
+ end
222
+
223
+ # Given a wfid, will [attempt to] pause the corresponding process instance.
224
+ # Given an expression id (fei) will [attempt to] pause the expression
225
+ # and its children.
226
+ #
227
+ # The only known option for now is :breakpoint => true, which lets
228
+ # the engine only pause the targetted expression.
229
+ #
230
+ #
231
+ # == fei and :breakpoint => true
232
+ #
233
+ # By default, pausing an expression will pause that expression and
234
+ # all its children.
235
+ #
236
+ # engine.pause(fei, :breakpoint => true)
237
+ #
238
+ # will only flag as paused the given fei. When the children of that
239
+ # expression will reply to it, the execution for this branch of the
240
+ # process will stop, much like a break point.
241
+ #
242
+ def pause(wi_or_fei_or_wfid, opts={})
243
+
244
+ opts = Ruote.keys_to_s(opts)
245
+
246
+ raise ArgumentError.new(
247
+ ':breakpoint option only valid when passing a workitem or a fei'
248
+ ) if opts['breakpoint'] and wi_or_fei_or_wfid.is_a?(String)
249
+
250
+ do_misc('pause', wi_or_fei_or_wfid, opts)
251
+ end
252
+
253
+ # Given a wfid will [attempt to] resume the process instance.
254
+ # Given an expression id (fei) will [attempt to] to resume the expression
255
+ # and its children.
256
+ #
257
+ # Note : this is supposed to be called on paused expressions / instances,
258
+ # this is NOT meant to be called to unstuck / unhang a process.
259
+ #
260
+ # == resume(wfid, :anyway => true)
261
+ #
262
+ # Resuming a process instance is equivalent to calling resume on its
263
+ # root expression. If the root is not paused itself, this will have no
264
+ # effect.
265
+ #
266
+ # dashboard.resume(wfid, :anyway => true)
267
+ #
268
+ # will make sure to call resume on each of the paused branch within the
269
+ # process instance (tree), effectively resuming the whole process.
270
+ #
271
+ def resume(wi_or_fei_or_wfid, opts={})
272
+
273
+ do_misc('resume', wi_or_fei_or_wfid, opts)
274
+ end
275
+
276
+ # Replays at a given error (hopefully the cause of the error got fixed
277
+ # before replaying...)
278
+ #
279
+ def replay_at_error(err)
280
+
281
+ err = error(err) unless err.is_a?(Ruote::ProcessError)
282
+
283
+ msg = err.msg.dup
284
+
285
+ if tree = msg['tree']
286
+ #
287
+ # as soon as there is a tree, it means it's a re_apply
288
+
289
+ re_apply(
290
+ msg['fei'],
291
+ 'tree' => tree,
292
+ 'replay_at_error' => true,
293
+ 'workitem' => msg['workitem'])
294
+
295
+ else
296
+
297
+ action = msg.delete('action')
298
+
299
+ msg['replay_at_error'] = true
300
+ # just an indication
301
+
302
+ @context.storage.delete(err.to_h) # remove error
303
+ @context.storage.put_msg(action, msg) # trigger replay
304
+ end
305
+ end
306
+
307
+ # Re-applies an expression (given via its FlowExpressionId).
308
+ #
309
+ # That will cancel the expression and, once the cancel operation is over
310
+ # (all the children have been cancelled), the expression will get
311
+ # re-applied.
312
+ #
313
+ # The fei parameter may be a hash, a Ruote::FlowExpressionId instance,
314
+ # a Ruote::Workitem instance or a sid string.
315
+ #
316
+ # == options
317
+ #
318
+ # :tree is used to completely change the tree of the expression at re_apply
319
+ #
320
+ # dashboard.re_apply(
321
+ # fei, :tree => [ 'participant', { 'ref' => 'bob' }, [] ])
322
+ #
323
+ # :fields is used to replace the fields of the workitem at re_apply
324
+ #
325
+ # dashboard.re_apply(
326
+ # fei, :fields => { 'customer' => 'bob' })
327
+ #
328
+ # :merge_in_fields is used to add / override fields
329
+ #
330
+ # dashboard.re_apply(
331
+ # fei, :merge_in_fields => { 'customer' => 'bob' })
332
+ #
333
+ def re_apply(fei, opts={})
334
+
335
+ @context.storage.put_msg(
336
+ 'cancel',
337
+ 'fei' => FlowExpressionId.extract_h(fei),
338
+ 're_apply' => Ruote.keys_to_s(opts))
339
+ end
340
+
341
+ # This method re_apply all the leaves of a process instance. It's meant
342
+ # to be used against stalled workflows to give them back the spark of
343
+ # life.
344
+ #
345
+ # Stalled workflows can happen when msgs get lost. It also happens with
346
+ # some storage implementations where msgs are stored differently from
347
+ # expressions and co.
348
+ #
349
+ # By default, it doesn't re_apply leaves that are in error. If the
350
+ # 'errors_too' option is set to true, it will re_apply leaves in error
351
+ # as well. For example:
352
+ #
353
+ # $dashboard.respark(wfid, 'errors_too' => true)
354
+ #
355
+ def respark(wfid, opts={})
356
+
357
+ @context.storage.put_msg(
358
+ 'respark',
359
+ 'wfid' => wfid,
360
+ 'respark' => Ruote.keys_to_s(opts))
361
+ end
362
+
363
+ # Returns a ProcessStatus instance describing the current status of
364
+ # a process instance.
365
+ #
366
+ def process(wfid)
367
+
368
+ ProcessStatus.fetch(@context, [ wfid ], {}).first
369
+ end
370
+
371
+ # Returns an array of ProcessStatus instances.
372
+ #
373
+ # WARNING : this is an expensive operation, but it understands :skip
374
+ # and :limit, so pagination is our friend.
375
+ #
376
+ # Please note, if you're interested only in processes that have errors,
377
+ # Engine#errors is a more efficient means.
378
+ #
379
+ # To simply list the wfids of the currently running, Engine#process_wfids
380
+ # is way cheaper to call.
381
+ #
382
+ def processes(opts={})
383
+
384
+ wfids = @context.storage.expression_wfids(opts)
385
+
386
+ opts[:count] ? wfids.size : ProcessStatus.fetch(@context, wfids, opts)
387
+ end
388
+
389
+ # Returns a list of processes or the process status of a given process
390
+ # instance.
391
+ #
392
+ def ps(wfid=nil)
393
+
394
+ wfid == nil ? processes : process(wfid)
395
+ end
396
+
397
+ # Returns an array of current errors (hashes)
398
+ #
399
+ # Can be called in two ways :
400
+ #
401
+ # dashboard.errors(wfid)
402
+ #
403
+ # and
404
+ #
405
+ # dashboard.errors(:skip => 100, :limit => 100)
406
+ #
407
+ def errors(wfid=nil)
408
+
409
+ wfid, options = wfid.is_a?(Hash) ? [ nil, wfid ] : [ wfid, {} ]
410
+
411
+ errs = wfid.nil? ?
412
+ @context.storage.get_many('errors', nil, options) :
413
+ @context.storage.get_many('errors', wfid)
414
+
415
+ return errs if options[:count]
416
+
417
+ errs.collect { |err| ProcessError.new(err) }
418
+ end
419
+
420
+ # Given a workitem or a fei (or a String version of a fei), returns
421
+ # the corresponding error (or nil if there is no other).
422
+ #
423
+ def error(wi_or_fei)
424
+
425
+ fei = Ruote.extract_fei(wi_or_fei)
426
+ err = @context.storage.get('errors', "err_#{fei.sid}")
427
+
428
+ err ? ProcessError.new(err) : nil
429
+ end
430
+
431
+ # Returns an array of schedules. Those schedules are open structs
432
+ # with various properties, like target, owner, at, put_at, ...
433
+ #
434
+ # Introduced mostly for ruote-kit.
435
+ #
436
+ # Can be called in two ways :
437
+ #
438
+ # dashboard.schedules(wfid)
439
+ #
440
+ # and
441
+ #
442
+ # dashboard.schedules(:skip => 100, :limit => 100)
443
+ #
444
+ def schedules(wfid=nil)
445
+
446
+ wfid, options = wfid.is_a?(Hash) ? [ nil, wfid ] : [ wfid, {} ]
447
+
448
+ scheds = wfid.nil? ?
449
+ @context.storage.get_many('schedules', nil, options) :
450
+ @context.storage.get_many('schedules', /!#{wfid}-\d+$/)
451
+
452
+ return scheds if options[:count]
453
+
454
+ scheds.collect { |s| Ruote.schedule_to_h(s) }.sort_by { |s| s['wfid'] }
455
+ end
456
+
457
+ # Returns a [sorted] list of wfids of the process instances currently
458
+ # running in the engine.
459
+ #
460
+ # This operation is substantially less costly than Engine#processes (though
461
+ # the 'how substantially' depends on the storage chosen).
462
+ #
463
+ def process_ids
464
+
465
+ @context.storage.expression_wfids({})
466
+ end
467
+
468
+ alias process_wfids process_ids
469
+
470
+ # Warning : expensive operation.
471
+ #
472
+ # Leftovers are workitems, errors and schedules belonging to process
473
+ # instances for which there are no more expressions left.
474
+ #
475
+ # Better delete them or investigate why they are left here.
476
+ #
477
+ # The result is a list of documents (hashes) as found in the storage. Each
478
+ # of them might represent a workitem, an error or a schedule.
479
+ #
480
+ # If you want to delete one of them you can do
481
+ #
482
+ # dashboard.storage.delete(doc)
483
+ #
484
+ def leftovers
485
+
486
+ wfids = @context.storage.expression_wfids({})
487
+
488
+ wis = @context.storage.get_many('workitems').compact
489
+ ers = @context.storage.get_many('errors').compact
490
+ scs = @context.storage.get_many('schedules').compact
491
+ # some slow storages need the compaction... [c]ouch...
492
+
493
+ (wis + ers + scs).reject { |doc| wfids.include?(doc['fei']['wfid']) }
494
+ end
495
+
496
+ # Shuts down the engine, mostly passes the shutdown message to the other
497
+ # services and hope they'll shut down properly.
498
+ #
499
+ def shutdown
500
+
501
+ @context.shutdown
502
+ end
503
+
504
+ # This method expects there to be a logger with a wait_for method in the
505
+ # context, else it will raise an exception.
506
+ #
507
+ # *WARNING*: #wait_for() is meant for environments where there is a unique
508
+ # worker and that worker is nested in this engine. In a multiple worker
509
+ # environment wait_for doesn't see events handled by 'other' workers.
510
+ #
511
+ # This method is only useful for test/quickstart/examples environments.
512
+ #
513
+ # dashboard.wait_for(:alpha)
514
+ # # will make the current thread block until a workitem is delivered
515
+ # # to the participant named 'alpha'
516
+ #
517
+ # engine.wait_for('123432123-9043')
518
+ # # will make the current thread block until the processed whose
519
+ # # wfid is given (String) terminates or produces an error.
520
+ #
521
+ # engine.wait_for(5)
522
+ # # will make the current thread block until 5 messages have been
523
+ # # processed on the workqueue...
524
+ #
525
+ # engine.wait_for(:empty)
526
+ # # will return as soon as the engine/storage is empty, ie as soon
527
+ # # as there are no more processes running in the engine (no more
528
+ # # expressions placed in the storage)
529
+ #
530
+ # engine.wait_for('terminated')
531
+ # # will return as soon as any process has a 'terminated' event.
532
+ #
533
+ # It's OK to wait for multiple wfids:
534
+ #
535
+ # engine.wait_for('20100612-bezerijozo', '20100612-yakisoba')
536
+ #
537
+ # If one needs to wait for something else than a wfid but needs to break
538
+ # in case of error:
539
+ #
540
+ # engine.wait_for(:alpha, :or_error)
541
+ #
542
+ #
543
+ # == ruote 2.3.0 and wait_for(event)
544
+ #
545
+ # Ruote 2.3.0 introduced the ability to wait for an event given its name.
546
+ # Here is a quick list of event names and a their description:
547
+ #
548
+ # * 'launch' - [sub]process launch
549
+ # * 'terminated' - process terminated
550
+ # * 'ceased' - orphan process terminated
551
+ # * 'apply' - expression application
552
+ # * 'reply' - expression reply
553
+ # * 'dispatched' - emitted workitem towards participant
554
+ # * 'receive' - received workitem from participant
555
+ # * 'pause' - pause order
556
+ # * 'resume' - pause order
557
+ # * 'dispatch_cancel' - emitting a cancel order to a participant
558
+ # * 'dispatch_pause' - emitting a pause order to a participant
559
+ # * 'dispatch_resume' - emitting a resume order to a participant
560
+ #
561
+ # Names that are past participles are for notification events, while
562
+ # plain verbs are for action events. Most of the time, a notitication
563
+ # is emitted has the result of an action event, workers don't take any
564
+ # action on them, but services that are listening to the ruote activity
565
+ # might want to do something about them.
566
+ #
567
+ #
568
+ # == ruote 2.3.0 and wait_for(hash)
569
+ #
570
+ # For more precise testing, wait_for accepts hashes, for example:
571
+ #
572
+ # r = dashboard.wait_for('action' => 'apply', 'exp_name' => 'wait')
573
+ #
574
+ # will block until a wait expression is applied.
575
+ #
576
+ # If you know ruote msgs, you can pinpoint at will:
577
+ #
578
+ # r = dashboard.wait_for(
579
+ # 'action' => 'apply',
580
+ # 'exp_name' => 'wait',
581
+ # 'fei.wfid' => wfid)
582
+ #
583
+ # == what wait_for returns
584
+ #
585
+ # #wait_for returns the intercepted event. It's useful when testing/
586
+ # spec'ing, as in:
587
+ #
588
+ # it 'completes successfully' do
589
+ #
590
+ # definition = Ruote.define :on_error => 'charly' do
591
+ # alpha
592
+ # bravo
593
+ # end
594
+ #
595
+ # wfid = @board.launch(definition)
596
+ #
597
+ # r = @board.wait_for(wfid)
598
+ # # wait until process terminates or hits an error
599
+ #
600
+ # r['workitem'].should_not == nil
601
+ # r['workitem']['fields']['alpha'].should == 'was here'
602
+ # r['workitem']['fields']['bravo'].should == 'was here'
603
+ # r['workitem']['fields']['charly'].should == nil
604
+ # end
605
+ #
606
+ # == :timeout option
607
+ #
608
+ # One can pass a timeout value in seconds for the #wait_for call, as in:
609
+ #
610
+ # dashboard.wait_for(wfid, :timeout => 5 * 60)
611
+ #
612
+ # The default timeout is 60 (seconds). A nil or negative timeout disables
613
+ # the timeout.
614
+ #
615
+ def wait_for(*items)
616
+
617
+ opts = (items.size > 1 && items.last.is_a?(Hash)) ? items.pop : {}
618
+
619
+ @context.logger.wait_for(items, opts)
620
+ end
621
+
622
+ # Joins the worker thread. If this engine has no nested worker, calling
623
+ # this method will simply return immediately.
624
+ #
625
+ def join
626
+
627
+ worker.join if worker
628
+ end
629
+
630
+ # Loads (and turns into a tree) the process definition at the given path.
631
+ #
632
+ def load_definition(path)
633
+
634
+ @context.reader.read(path)
635
+ end
636
+
637
+ # Registers a participant in the engine.
638
+ #
639
+ # Takes the form
640
+ #
641
+ # dashboard.register_participant name_or_regex, klass, opts={}
642
+ #
643
+ # With the form
644
+ #
645
+ # dashboard.register_participant name_or_regex do |workitem|
646
+ # # ...
647
+ # end
648
+ #
649
+ # A BlockParticipant is automatically created.
650
+ #
651
+ #
652
+ # == name or regex
653
+ #
654
+ # When registering participants, strings or regexes are accepted. Behind
655
+ # the scenes, a regex is kept.
656
+ #
657
+ # Passing a string like "alain" will get ruote to automatically turn it
658
+ # into the following regex : /^alain$/.
659
+ #
660
+ # For finer control over this, pass a regex directly
661
+ #
662
+ # dashboard.register_participant /^user-/, MyParticipant
663
+ # # will match all workitems whose participant name starts with "user-"
664
+ #
665
+ #
666
+ # == some examples
667
+ #
668
+ # dashboard.register_participant 'compute_sum' do |wi|
669
+ # wi.fields['sum'] = wi.fields['articles'].inject(0) do |s, (c, v)|
670
+ # s + c * v # sum + count * value
671
+ # end
672
+ # # a block participant implicitely replies to the engine immediately
673
+ # end
674
+ #
675
+ # class MyParticipant
676
+ # def initialize(opts)
677
+ # @name = opts['name']
678
+ # end
679
+ # def consume(workitem)
680
+ # workitem.fields['rocket_name'] = @name
681
+ # send_to_the_moon(workitem)
682
+ # end
683
+ # def cancel(fei, flavour)
684
+ # # do nothing
685
+ # end
686
+ # end
687
+ #
688
+ # dashboard.register_participant(
689
+ # /^moon-.+/, MyParticipant, 'name' => 'Saturn-V')
690
+ #
691
+ # # computing the total for a invoice being passed in the workitem.
692
+ # #
693
+ # class TotalParticipant
694
+ # include Ruote::LocalParticipant
695
+ #
696
+ # def consume(workitem)
697
+ # workitem['total'] = workitem.fields['items'].inject(0.0) { |t, item|
698
+ # t + item['count'] * PricingService.lookup(item['id'])
699
+ # }
700
+ # reply_to_engine(workitem)
701
+ # end
702
+ # end
703
+ # dashboard.register_participant 'total', TotalParticipant
704
+ #
705
+ # Remember that the options (the hash that follows the class name), must be
706
+ # serializable via JSON.
707
+ #
708
+ #
709
+ # == require_path and load_path
710
+ #
711
+ # It's OK to register a participant by passing its full classname as a
712
+ # String.
713
+ #
714
+ # dashboard.register_participant(
715
+ # 'auditor', 'AuditParticipant', 'require_path' => 'part/audit.rb')
716
+ # dashboard.register_participant(
717
+ # 'auto_decision', 'DecParticipant', 'load_path' => 'part/dec.rb')
718
+ #
719
+ # Note the option load_path / require_path that point to the ruby file
720
+ # containing the participant implementation. 'require' will load and eval
721
+ # the ruby code only once, 'load' each time.
722
+ #
723
+ #
724
+ # == :override => false
725
+ #
726
+ # By default, when registering a participant, if this results in a regex
727
+ # that is already used, the previously registered participant gets
728
+ # unregistered.
729
+ #
730
+ # dashboard.register_participant 'alpha', AaParticipant
731
+ # dashboard.register_participant 'alpha', BbParticipant, :override => false
732
+ #
733
+ # This can be useful when the #accept? method of participants are in use.
734
+ #
735
+ # Note that using the #register(&block) method, :override => false is
736
+ # automatically enforced.
737
+ #
738
+ # dashboard.register do
739
+ # alpha AaParticipant
740
+ # alpha BbParticipant
741
+ # end
742
+ #
743
+ #
744
+ # == :position / :pos => 'last' / 'first' / 'before' / 'after' / 'over'
745
+ #
746
+ # One can specify the position where the participant should be inserted
747
+ # in the participant list.
748
+ #
749
+ # dashboard.register_participant 'auditor', AuditParticipant, :pos => 'last'
750
+ #
751
+ # * last : it's the default, places the participant at the end of the list
752
+ # * first : top of the list
753
+ # * before : implies :override => false, places before the existing
754
+ # participant with the same regex
755
+ # * after : implies :override => false, places after the last existing
756
+ # participant with the same regex
757
+ # * over : overrides in the same position (while the regular, default
758
+ # overide removes and then places the new participant at the end of
759
+ # the list)
760
+ #
761
+ def register_participant(regex, participant=nil, opts={}, &block)
762
+
763
+ if participant.is_a?(Hash)
764
+ opts = participant
765
+ participant = nil
766
+ end
767
+
768
+ pa = @context.plist.register(regex, participant, opts, block)
769
+
770
+ @context.storage.put_msg(
771
+ 'participant_registered',
772
+ 'regex' => regex.is_a?(Regexp) ? regex.inspect : regex.to_s)
773
+
774
+ pa
775
+ end
776
+
777
+ # A shorter version of #register_participant
778
+ #
779
+ # dashboard.register 'alice', MailParticipant, :target => 'alice@example.com'
780
+ #
781
+ # or a block registering mechanism.
782
+ #
783
+ # dashboard.register do
784
+ # alpha 'Participants::Alpha', 'flavour' => 'vanilla'
785
+ # participant 'bravo', 'Participants::Bravo', :flavour => 'peach'
786
+ # catchall ParticipantCharlie, 'flavour' => 'coconut'
787
+ # end
788
+ #
789
+ # Originally implemented in ruote-kit by Torsten Schoenebaum.
790
+ #
791
+ # == registration in block and :clear
792
+ #
793
+ # By default, when registering multiple participants in block, ruote
794
+ # considers you're wiping the participant list and re-adding them all.
795
+ #
796
+ # You can prevent the clearing by stating :clear => false like in :
797
+ #
798
+ # dashboard.register :clear => false do
799
+ # alpha 'Participants::Alpha', 'flavour' => 'vanilla'
800
+ # participant 'bravo', 'Participants::Bravo', :flavour => 'peach'
801
+ # catchall ParticipantCharlie, 'flavour' => 'coconut'
802
+ # end
803
+ #
804
+ def register(*args, &block)
805
+
806
+ clear = args.first.is_a?(Hash) ? args.pop[:clear] : true
807
+
808
+ if args.size > 0
809
+ register_participant(*args, &block)
810
+ else
811
+ proxy = ParticipantRegistrationProxy.new(self, clear)
812
+ block.arity < 1 ? proxy.instance_eval(&block) : block.call(proxy)
813
+ proxy._flush
814
+ end
815
+ end
816
+
817
+ # Removes/unregisters a participant from the engine.
818
+ #
819
+ def unregister_participant(name_or_participant)
820
+
821
+ re = @context.plist.unregister(name_or_participant)
822
+
823
+ raise(ArgumentError.new('participant not found')) unless re
824
+
825
+ @context.storage.put_msg(
826
+ 'participant_unregistered',
827
+ 'regex' => re.to_s)
828
+ end
829
+
830
+ alias :unregister :unregister_participant
831
+
832
+ # Returns a list of Ruote::ParticipantEntry instances.
833
+ #
834
+ # dashboard.register_participant :alpha, MyParticipant, 'message' => 'hello'
835
+ #
836
+ # # interrogate participant list
837
+ # #
838
+ # list = dashboard.participant_list
839
+ # participant = list.first
840
+ # p participant.regex
841
+ # # => "^alpha$"
842
+ # p participant.classname
843
+ # # => "MyParticipant"
844
+ # p participant.options
845
+ # # => {"message"=>"hello"}
846
+ #
847
+ # # update participant list
848
+ # #
849
+ # participant.regex = '^alfred$'
850
+ # dashboard.participant_list = list
851
+ #
852
+ def participant_list
853
+
854
+ @context.plist.list
855
+ end
856
+
857
+ # Accepts a list of Ruote::ParticipantEntry instances or a list of
858
+ # [ regex, [ classname, opts ] ] arrays.
859
+ #
860
+ # See Engine#participant_list
861
+ #
862
+ # Some examples :
863
+ #
864
+ # dashboard.participant_list = [
865
+ # [ '^charly$', [ 'Ruote::StorageParticipant', {} ] ],
866
+ # [ '.+', [ 'MyDefaultParticipant', { 'default' => true } ]
867
+ # ]
868
+ #
869
+ # This method writes the participant list in one go, it might be easier to
870
+ # use than to register participant one by ones.
871
+ #
872
+ def participant_list=(pl)
873
+
874
+ @context.plist.list = pl
875
+ end
876
+
877
+ # A convenience method for
878
+ #
879
+ # sp = Ruote::StorageParticipant.new(dashboard)
880
+ #
881
+ # simply do
882
+ #
883
+ # sp = dashboard.storage_participant
884
+ #
885
+ def storage_participant
886
+
887
+ @storage_participant ||= Ruote::StorageParticipant.new(self)
888
+ end
889
+
890
+ # #worklist or #storage_participant
891
+ #
892
+ alias worklist storage_participant
893
+
894
+ # Returns an instance of the participant registered under the given name.
895
+ # Returns nil if there is no participant registered for that name.
896
+ #
897
+ def participant(name)
898
+
899
+ @context.plist.lookup(name.to_s, nil)
900
+ end
901
+
902
+ # Adds a service locally (will not get propagated to other workers).
903
+ #
904
+ # tracer = Tracer.new
905
+ # @dashboard.add_service('tracer', tracer)
906
+ #
907
+ # or
908
+ #
909
+ # @dashboard.add_service(
910
+ # 'tracer', 'ruote/exp/tracer', 'Ruote::Exp::Tracer')
911
+ #
912
+ # This method returns the service instance it just bound.
913
+ #
914
+ def add_service(name, path_or_instance, classname=nil, opts=nil)
915
+
916
+ @context.add_service(name, path_or_instance, classname, opts)
917
+ end
918
+
919
+ # Sets a configuration option. Examples:
920
+ #
921
+ # # allow remote workflow definitions (for subprocesses or when launching
922
+ # # processes)
923
+ # @dashboard.configure('remote_definition_allowed', true)
924
+ #
925
+ # # allow ruby_eval
926
+ # @dashboard.configure('ruby_eval_allowed', true)
927
+ #
928
+ def configure(config_key, value)
929
+
930
+ @context[config_key] = value
931
+ end
932
+
933
+ # Returns a configuration value.
934
+ #
935
+ # dashboard.configure('ruby_eval_allowed', true)
936
+ #
937
+ # p dashboard.configuration('ruby_eval_allowed')
938
+ # # => true
939
+ #
940
+ def configuration(config_key)
941
+
942
+ @context[config_key]
943
+ end
944
+
945
+ # Returns the hash containing info about each worker connected to the
946
+ # storage.
947
+ #
948
+ def worker_info
949
+
950
+ (@context.storage.get('variables', 'workers') || {})['workers']
951
+ end
952
+
953
+ # Returns the state the workers are supposed to be in right now.
954
+ # It's usually 'running', but it could be 'stopped' or 'paused'.
955
+ #
956
+ def worker_state
957
+
958
+ doc =
959
+ @context.storage.get('variables', 'worker') ||
960
+ { 'type' => 'variables', '_id' => 'worker', 'state' => 'running' }
961
+
962
+ doc['state']
963
+ end
964
+
965
+ WORKER_STATES = %w[ running stopped paused ]
966
+
967
+ # Sets the [desired] worker state. The workers will check that target
968
+ # state at their next beat and switch to it.
969
+ #
970
+ # Setting the state to 'stopped' will force the workers to stop as soon
971
+ # as they notice the new state.
972
+ #
973
+ # Setting the state to 'paused' will force the workers to pause. They
974
+ # will not process msgs until the state is set back to 'running'.
975
+ #
976
+ # By default the [engine] option 'worker_state_enabled' is not set, so
977
+ # calling this method will result in a error, unless 'worker_state_enabled'
978
+ # was set to true when the storage was initialized.
979
+ #
980
+ def worker_state=(state)
981
+
982
+ raise RuntimeError.new(
983
+ "'worker_state_enabled' is not set, cannot change state"
984
+ ) unless @context['worker_state_enabled']
985
+
986
+ state = state.to_s
987
+
988
+ raise ArgumentError.new(
989
+ "#{state.inspect} not in #{WORKER_STATES.inspect}"
990
+ ) unless WORKER_STATES.include?(state)
991
+
992
+ doc =
993
+ @context.storage.get('variables', 'worker') ||
994
+ { 'type' => 'variables', '_id' => 'worker', 'state' => 'running' }
995
+
996
+ doc['state'] = state
997
+
998
+ @context.storage.put(doc) && worker_state=(state)
999
+ end
1000
+
1001
+ # Returns the process tree that is triggered in case of error.
1002
+ #
1003
+ # Note that this 'on_error' doesn't trigger if an on_error is defined
1004
+ # in the process itself.
1005
+ #
1006
+ # Returns nil if there is no 'on_error' set.
1007
+ #
1008
+ def on_error
1009
+
1010
+ @context.storage.get_trackers['trackers']['on_error']['msg']['tree']
1011
+
1012
+ rescue
1013
+ nil
1014
+ end
1015
+
1016
+ # Returns the process tree that is triggered in case of process termination.
1017
+ #
1018
+ # Note that a termination process doesn't raise a termination process when
1019
+ # it terminates itself.
1020
+ #
1021
+ # Returns nil if there is no 'on_terminate' set.
1022
+ #
1023
+ def on_terminate
1024
+
1025
+ @context.storage.get_trackers['trackers']['on_terminate']['msg']['tree']
1026
+
1027
+ rescue
1028
+ nil
1029
+ end
1030
+
1031
+ # Sets a participant or subprocess to be triggered when an error occurs
1032
+ # in a process instance.
1033
+ #
1034
+ # dashboard.on_error = participant_name
1035
+ #
1036
+ # dashboard.on_error = subprocess_name
1037
+ #
1038
+ # dashboard.on_error = Ruote.process_definition do
1039
+ # alpha
1040
+ # end
1041
+ #
1042
+ # Note that this 'on_error' doesn't trigger if an on_error is defined
1043
+ # in the process itself.
1044
+ #
1045
+ def on_error=(target)
1046
+
1047
+ @context.tracker.add_tracker(
1048
+ nil, # do not track a specific wfid
1049
+ 'error_intercepted', # react on 'error_intercepted' msgs
1050
+ 'on_error', # the identifier
1051
+ nil, # no specific condition
1052
+ { 'action' => 'launch',
1053
+ 'wfid' => 'replace',
1054
+ 'tree' => target.is_a?(String) ?
1055
+ [ 'define', {}, [ [ target, {}, [] ] ] ] : target,
1056
+ 'workitem' => 'replace',
1057
+ 'variables' => 'compile' })
1058
+ end
1059
+
1060
+ # Sets a participant or a subprocess that is to be launched/called whenever
1061
+ # a regular process terminates.
1062
+ #
1063
+ # dashboard.on_terminate = participant_name
1064
+ #
1065
+ # dashboard.on_terminate = subprocess_name
1066
+ #
1067
+ # dashboard.on_terminate = Ruote.define do
1068
+ # alpha
1069
+ # bravo
1070
+ # end
1071
+ #
1072
+ # Note that a termination process doesn't raise a termination process when
1073
+ # it terminates itself.
1074
+ #
1075
+ # on_terminate processes are not triggered for on_error processes.
1076
+ # on_error processes are triggered for on_terminate processes as well.
1077
+ #
1078
+ def on_terminate=(target)
1079
+
1080
+ @context.tracker.add_tracker(
1081
+ nil, # do not track a specific wfid
1082
+ 'terminated', # react on 'error_intercepted' msgs
1083
+ 'on_terminate', # the identifier
1084
+ nil, # no specific condition
1085
+ { 'action' => 'launch',
1086
+ 'tree' => target.is_a?(String) ?
1087
+ [ 'define', {}, [ [ target, {}, [] ] ] ] : target,
1088
+ 'workitem' => 'replace' })
1089
+ end
1090
+
1091
+ # A debug helper :
1092
+ #
1093
+ # dashboard.noisy = true
1094
+ #
1095
+ # will let the dashboard (in fact the worker) pour all the details of the
1096
+ # executing process instances to STDOUT.
1097
+ #
1098
+ def noisy=(b)
1099
+
1100
+ @context.logger.noisy = b
1101
+ end
1102
+
1103
+ protected
1104
+
1105
+ # Used by #pause and #resume.
1106
+ #
1107
+ def do_misc(action, wi_or_fei_or_wfid, opts)
1108
+
1109
+ opts = Ruote.keys_to_s(opts)
1110
+
1111
+ target = Ruote.extract_id(wi_or_fei_or_wfid)
1112
+
1113
+ if action == 'resume' && opts['anyway']
1114
+ #
1115
+ # determines the roots of the branches that are paused
1116
+ # sends the resume message to them.
1117
+
1118
+ exps = ps(target).expressions.select { |fexp| fexp.state == 'paused' }
1119
+ feis = exps.collect { |fexp| fexp.fei }
1120
+
1121
+ roots = exps.inject([]) { |a, fexp|
1122
+ a << fexp.fei.h unless feis.include?(fexp.parent_id)
1123
+ a
1124
+ }
1125
+
1126
+ roots.each { |fei| @context.storage.put_msg('resume', 'fei' => fei) }
1127
+
1128
+ elsif target.is_a?(String)
1129
+ #
1130
+ # action targets a process instance (a string wfid)
1131
+
1132
+ @context.storage.put_msg(
1133
+ "#{action}_process", opts.merge('wfid' => target))
1134
+
1135
+ else
1136
+
1137
+ @context.storage.put_msg(
1138
+ action, opts.merge('fei' => target))
1139
+ end
1140
+ end
1141
+ end
1142
+
1143
+ #
1144
+ # A wrapper class giving easy access to engine variables.
1145
+ #
1146
+ # There is one instance of this class for an Engine instance. It is
1147
+ # returned when calling Engine#variables.
1148
+ #
1149
+ class EngineVariables
1150
+
1151
+ def initialize(storage)
1152
+
1153
+ @storage = storage
1154
+ end
1155
+
1156
+ def [](k)
1157
+
1158
+ @storage.get_engine_variable(k)
1159
+ end
1160
+
1161
+ def []=(k, v)
1162
+
1163
+ @storage.put_engine_variable(k, v)
1164
+ end
1165
+ end
1166
+
1167
+ #
1168
+ # Engine#register uses this proxy when it's passed a block.
1169
+ #
1170
+ # Originally written by Torsten Schoenebaum for ruote-kit.
1171
+ #
1172
+ class ParticipantRegistrationProxy < Ruote::BlankSlate
1173
+
1174
+ def initialize(dashboard, clear)
1175
+
1176
+ @dashboard = dashboard
1177
+
1178
+ @dashboard.context.plist.clear if clear
1179
+
1180
+ @list = clear ? [] : nil
1181
+ end
1182
+
1183
+ def participant(name, klass=nil, options={}, &block)
1184
+
1185
+ if @list
1186
+
1187
+ @list <<
1188
+ @dashboard.context.plist.to_entry(name, klass, options, block)
1189
+
1190
+ else
1191
+
1192
+ @dashboard.register_participant(
1193
+ name, klass, options.merge!(:override => false), &block)
1194
+ end
1195
+ end
1196
+
1197
+ def catchall(*args)
1198
+
1199
+ klass = args.empty? ? Ruote::StorageParticipant : args.first
1200
+ options = args[1] || {}
1201
+
1202
+ participant('.+', klass, options)
1203
+ end
1204
+
1205
+ alias catch_all catchall
1206
+
1207
+ # Maybe a bit audacious...
1208
+ #
1209
+ def method_missing(method_name, *args, &block)
1210
+
1211
+ participant(method_name, *args, &block)
1212
+ end
1213
+
1214
+ def _flush
1215
+
1216
+ @dashboard.participant_list = @list if @list
1217
+ end
1218
+ end
1219
+
1220
+ # Refines a schedule as found in the ruote storage into something a bit
1221
+ # easier to present.
1222
+ #
1223
+ def self.schedule_to_h(sched)
1224
+
1225
+ h = sched.dup
1226
+
1227
+ class << h; attr_accessor :h; end
1228
+ h.h = sched
1229
+ #
1230
+ # for the sake of ProcessStatus#to_h
1231
+
1232
+ h.delete('_rev')
1233
+ h.delete('type')
1234
+ msg = h.delete('msg')
1235
+ owner = h.delete('owner')
1236
+
1237
+ h['wfid'] = owner['wfid']
1238
+ h['action'] = msg['action']
1239
+ h['type'] = msg['flavour']
1240
+ h['owner'] = Ruote::FlowExpressionId.new(owner)
1241
+
1242
+ h['target'] = Ruote::FlowExpressionId.new(msg['fei']) if msg['fei']
1243
+
1244
+ h
1245
+ end
1246
+ end
1247
+