ruote 2.2.0 → 2.3.0

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