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.
- data/CHANGELOG.txt +166 -1
- data/CREDITS.txt +36 -17
- data/LICENSE.txt +1 -1
- data/README.rdoc +1 -7
- data/Rakefile +38 -29
- data/TODO.txt +93 -52
- data/lib/ruote-fs.rb +3 -0
- data/lib/ruote.rb +5 -1
- data/lib/ruote/context.rb +140 -35
- data/lib/ruote/dashboard.rb +1247 -0
- data/lib/ruote/{engine → dboard}/process_error.rb +22 -2
- data/lib/ruote/dboard/process_status.rb +587 -0
- data/lib/ruote/engine.rb +6 -871
- data/lib/ruote/exp/command.rb +7 -2
- data/lib/ruote/exp/commanded.rb +2 -2
- data/lib/ruote/exp/condition.rb +38 -13
- data/lib/ruote/exp/fe_add_branches.rb +1 -1
- data/lib/ruote/exp/fe_apply.rb +1 -1
- data/lib/ruote/exp/fe_await.rb +357 -0
- data/lib/ruote/exp/fe_cancel_process.rb +17 -3
- data/lib/ruote/exp/fe_command.rb +8 -4
- data/lib/ruote/exp/fe_concurrence.rb +218 -18
- data/lib/ruote/exp/fe_concurrent_iterator.rb +71 -10
- data/lib/ruote/exp/fe_cron.rb +3 -10
- data/lib/ruote/exp/fe_cursor.rb +14 -4
- data/lib/ruote/exp/fe_define.rb +3 -1
- data/lib/ruote/exp/fe_echo.rb +1 -1
- data/lib/ruote/exp/fe_equals.rb +1 -1
- data/lib/ruote/exp/fe_error.rb +1 -1
- data/lib/ruote/exp/fe_filter.rb +163 -4
- data/lib/ruote/exp/fe_forget.rb +21 -4
- data/lib/ruote/exp/fe_given.rb +1 -1
- data/lib/ruote/exp/fe_if.rb +1 -1
- data/lib/ruote/exp/fe_inc.rb +102 -35
- data/lib/ruote/exp/fe_iterator.rb +47 -12
- data/lib/ruote/exp/fe_listen.rb +96 -11
- data/lib/ruote/exp/fe_lose.rb +31 -4
- data/lib/ruote/exp/fe_noop.rb +1 -1
- data/lib/ruote/exp/fe_on_error.rb +109 -0
- data/lib/ruote/exp/fe_once.rb +10 -19
- data/lib/ruote/exp/fe_participant.rb +90 -28
- data/lib/ruote/exp/fe_read.rb +69 -0
- data/lib/ruote/exp/fe_redo.rb +3 -2
- data/lib/ruote/exp/fe_ref.rb +57 -27
- data/lib/ruote/exp/fe_registerp.rb +1 -3
- data/lib/ruote/exp/fe_reserve.rb +1 -1
- data/lib/ruote/exp/fe_restore.rb +6 -6
- data/lib/ruote/exp/fe_save.rb +12 -19
- data/lib/ruote/exp/fe_sequence.rb +38 -2
- data/lib/ruote/exp/fe_set.rb +143 -40
- data/lib/ruote/exp/{fe_let.rb → fe_stall.rb} +7 -38
- data/lib/ruote/exp/fe_subprocess.rb +8 -2
- data/lib/ruote/exp/fe_that.rb +1 -1
- data/lib/ruote/exp/fe_undo.rb +40 -4
- data/lib/ruote/exp/fe_unregisterp.rb +1 -3
- data/lib/ruote/exp/fe_wait.rb +12 -25
- data/lib/ruote/exp/{flowexpression.rb → flow_expression.rb} +375 -229
- data/lib/ruote/exp/iterator.rb +2 -2
- data/lib/ruote/exp/merge.rb +78 -17
- data/lib/ruote/exp/ro_attributes.rb +46 -36
- data/lib/ruote/exp/ro_filters.rb +34 -8
- data/lib/ruote/exp/ro_on_x.rb +431 -0
- data/lib/ruote/exp/ro_persist.rb +19 -7
- data/lib/ruote/exp/ro_timers.rb +123 -0
- data/lib/ruote/exp/ro_variables.rb +90 -29
- data/lib/ruote/fei.rb +57 -3
- data/lib/ruote/fs.rb +3 -0
- data/lib/ruote/id/mnemo_wfid_generator.rb +30 -7
- data/lib/ruote/id/wfid_generator.rb +17 -38
- data/lib/ruote/log/default_history.rb +23 -9
- data/lib/ruote/log/fancy_printing.rb +265 -0
- data/lib/ruote/log/storage_history.rb +23 -13
- data/lib/ruote/log/wait_logger.rb +224 -17
- data/lib/ruote/observer.rb +82 -0
- data/lib/ruote/part/block_participant.rb +65 -28
- data/lib/ruote/part/code_participant.rb +81 -0
- data/lib/ruote/part/engine_participant.rb +7 -2
- data/lib/ruote/part/local_participant.rb +221 -21
- data/lib/ruote/part/no_op_participant.rb +1 -1
- data/lib/ruote/part/null_participant.rb +1 -1
- data/lib/ruote/part/participant.rb +50 -0
- data/lib/ruote/part/rev_participant.rb +178 -0
- data/lib/ruote/part/smtp_participant.rb +2 -2
- data/lib/ruote/part/storage_participant.rb +228 -60
- data/lib/ruote/part/template.rb +1 -1
- data/lib/ruote/participant.rb +2 -0
- data/lib/ruote/reader.rb +205 -68
- data/lib/ruote/reader/json.rb +49 -0
- data/lib/ruote/reader/radial.rb +303 -0
- data/lib/ruote/reader/ruby_dsl.rb +44 -9
- data/lib/ruote/reader/xml.rb +11 -8
- data/lib/ruote/receiver/base.rb +98 -45
- data/lib/ruote/storage/base.rb +104 -35
- data/lib/ruote/storage/composite_storage.rb +50 -60
- data/lib/ruote/storage/fs_storage.rb +25 -34
- data/lib/ruote/storage/hash_storage.rb +38 -36
- data/lib/ruote/svc/dispatch_pool.rb +104 -35
- data/lib/ruote/svc/dollar_sub.rb +10 -8
- data/lib/ruote/svc/error_handler.rb +108 -52
- data/lib/ruote/svc/expression_map.rb +3 -3
- data/lib/ruote/svc/participant_list.rb +160 -55
- data/lib/ruote/svc/tracker.rb +31 -31
- data/lib/ruote/svc/treechecker.rb +28 -16
- data/lib/ruote/tree_dot.rb +1 -1
- data/lib/ruote/util/deep.rb +143 -0
- data/lib/ruote/util/filter.rb +125 -18
- data/lib/ruote/util/hashdot.rb +15 -13
- data/lib/ruote/util/look.rb +1 -1
- data/lib/ruote/util/lookup.rb +60 -22
- data/lib/ruote/util/misc.rb +63 -18
- data/lib/ruote/util/mpatch.rb +53 -0
- data/lib/ruote/util/ometa.rb +1 -2
- data/lib/ruote/util/process_observer.rb +177 -0
- data/lib/ruote/util/subprocess.rb +1 -1
- data/lib/ruote/util/time.rb +2 -2
- data/lib/ruote/util/tree.rb +64 -2
- data/lib/ruote/version.rb +3 -2
- data/lib/ruote/worker.rb +421 -92
- data/lib/ruote/workitem.rb +157 -22
- data/ruote.gemspec +15 -9
- data/test/bm/ci.rb +0 -2
- data/test/bm/ici.rb +0 -2
- data/test/bm/load_26c.rb +0 -3
- data/test/bm/mega.rb +0 -2
- data/test/functional/base.rb +57 -43
- data/test/functional/concurrent_base.rb +16 -13
- data/test/functional/ct_0_concurrence.rb +7 -11
- data/test/functional/ct_1_iterator.rb +9 -11
- data/test/functional/ct_2_cancel.rb +28 -17
- data/test/functional/eft_0_flow_expression.rb +35 -0
- data/test/functional/eft_10_cancel_process.rb +1 -1
- data/test/functional/eft_11_wait.rb +13 -13
- data/test/functional/eft_12_listen.rb +199 -66
- data/test/functional/eft_13_iterator.rb +95 -29
- data/test/functional/eft_14_cursor.rb +74 -24
- data/test/functional/eft_15_loop.rb +7 -7
- data/test/functional/eft_16_if.rb +1 -1
- data/test/functional/eft_17_equals.rb +1 -1
- data/test/functional/eft_18_concurrent_iterator.rb +156 -68
- data/test/functional/eft_19_reserve.rb +15 -15
- data/test/functional/eft_1_echo.rb +1 -1
- data/test/functional/eft_20_save.rb +51 -9
- data/test/functional/eft_21_restore.rb +1 -1
- data/test/functional/eft_22_noop.rb +1 -1
- data/test/functional/eft_23_apply.rb +1 -1
- data/test/functional/eft_24_add_branches.rb +7 -8
- data/test/functional/eft_25_command.rb +1 -1
- data/test/functional/eft_26_error.rb +11 -11
- data/test/functional/eft_27_inc.rb +111 -67
- data/test/functional/eft_28_once.rb +16 -16
- data/test/functional/eft_29_cron.rb +9 -9
- data/test/functional/eft_2_sequence.rb +23 -4
- data/test/functional/eft_30_ref.rb +36 -24
- data/test/functional/eft_31_registerp.rb +24 -24
- data/test/functional/eft_32_lose.rb +46 -20
- data/test/functional/eft_34_given.rb +1 -1
- data/test/functional/eft_35_filter.rb +161 -7
- data/test/functional/eft_36_read.rb +97 -0
- data/test/functional/{eft_0_process_definition.rb → eft_37_process_definition.rb} +4 -4
- data/test/functional/eft_38_on_error.rb +195 -0
- data/test/functional/eft_39_stall.rb +35 -0
- data/test/functional/eft_3_participant.rb +77 -22
- data/test/functional/eft_40_await.rb +297 -0
- data/test/functional/eft_4_set.rb +110 -11
- data/test/functional/eft_5_subprocess.rb +27 -5
- data/test/functional/eft_6_concurrence.rb +299 -60
- data/test/functional/eft_7_forget.rb +24 -22
- data/test/functional/eft_8_undo.rb +52 -15
- data/test/functional/eft_9_redo.rb +18 -20
- data/test/functional/ft_0_worker.rb +122 -13
- data/test/functional/ft_10_dollar.rb +77 -16
- data/test/functional/ft_11_recursion.rb +9 -9
- data/test/functional/ft_12_launchitem.rb +7 -9
- data/test/functional/ft_13_variables.rb +125 -22
- data/test/functional/ft_14_re_apply.rb +112 -56
- data/test/functional/ft_15_timeout.rb +64 -33
- data/test/functional/ft_16_participant_params.rb +59 -6
- data/test/functional/ft_17_conditional.rb +68 -2
- data/test/functional/ft_18_kill.rb +48 -30
- data/test/functional/ft_19_participant_code.rb +67 -0
- data/test/functional/ft_1_process_status.rb +222 -150
- data/test/functional/ft_20_storage_participant.rb +445 -44
- data/test/functional/ft_21_forget.rb +21 -26
- data/test/functional/ft_22_process_definitions.rb +8 -6
- data/test/functional/ft_23_load_defs.rb +29 -5
- data/test/functional/ft_24_block_participant.rb +199 -20
- data/test/functional/ft_25_receiver.rb +98 -46
- data/test/functional/ft_26_participant_rtimeout.rb +34 -26
- data/test/functional/ft_27_var_indirection.rb +40 -5
- data/test/functional/ft_28_null_noop_participants.rb +5 -5
- data/test/functional/ft_29_part_template.rb +2 -2
- data/test/functional/ft_2_errors.rb +106 -74
- data/test/functional/ft_30_smtp_participant.rb +7 -7
- data/test/functional/ft_31_part_blocking.rb +11 -11
- data/test/functional/ft_32_scope.rb +50 -0
- data/test/functional/ft_33_participant_subprocess_priority.rb +3 -3
- data/test/functional/ft_34_cursor_rewind.rb +14 -14
- data/test/functional/ft_35_add_service.rb +67 -9
- data/test/functional/ft_36_storage_history.rb +92 -24
- data/test/functional/ft_37_default_history.rb +35 -23
- data/test/functional/ft_38_participant_more.rb +189 -32
- data/test/functional/ft_39_wait_for.rb +25 -25
- data/test/functional/ft_3_participant_registration.rb +235 -107
- data/test/functional/ft_40_wait_logger.rb +105 -18
- data/test/functional/ft_41_participants.rb +13 -12
- data/test/functional/ft_42_storage_copy.rb +12 -12
- data/test/functional/ft_43_participant_on_reply.rb +85 -11
- data/test/functional/ft_44_var_participant.rb +5 -5
- data/test/functional/ft_45_participant_accept.rb +3 -3
- data/test/functional/ft_46_launch_single.rb +17 -17
- data/test/functional/ft_47_wfids.rb +41 -0
- data/test/functional/ft_48_lose.rb +19 -25
- data/test/functional/ft_49_engine_on_error.rb +54 -70
- data/test/functional/ft_4_cancel.rb +84 -26
- data/test/functional/ft_50_engine_config.rb +4 -4
- data/test/functional/ft_51_misc.rb +12 -12
- data/test/functional/ft_52_case.rb +17 -17
- data/test/functional/ft_53_engine_on_terminate.rb +18 -21
- data/test/functional/ft_54_patterns.rb +18 -16
- data/test/functional/ft_55_engine_participant.rb +55 -55
- data/test/functional/ft_56_filter_attribute.rb +90 -52
- data/test/functional/ft_57_rev_participant.rb +252 -0
- data/test/functional/ft_58_workitem.rb +150 -0
- data/test/functional/ft_59_pause.rb +329 -0
- data/test/functional/ft_5_on_error.rb +430 -77
- data/test/functional/ft_60_code_participant.rb +65 -0
- data/test/functional/ft_61_trailing_fields.rb +34 -0
- data/test/functional/ft_62_exp_name_and_dollar_substitution.rb +35 -0
- data/test/functional/ft_63_participants_221.rb +458 -0
- data/test/functional/ft_64_stash.rb +41 -0
- data/test/functional/ft_65_timers.rb +313 -0
- data/test/functional/ft_66_flank.rb +133 -0
- data/test/functional/ft_67_radial_misc.rb +34 -0
- data/test/functional/ft_68_reput.rb +72 -0
- data/test/functional/ft_69_worker_info.rb +56 -0
- data/test/functional/ft_6_on_cancel.rb +189 -36
- data/test/functional/ft_70_take_and_discard_attributes.rb +94 -0
- data/test/functional/ft_71_retries.rb +144 -0
- data/test/functional/ft_72_on_terminate.rb +60 -0
- data/test/functional/ft_73_raise_msg.rb +107 -0
- data/test/functional/ft_74_respark.rb +106 -0
- data/test/functional/ft_75_context.rb +66 -0
- data/test/functional/ft_76_observer.rb +53 -0
- data/test/functional/ft_77_process_observer.rb +157 -0
- data/test/functional/ft_78_part_participant.rb +37 -0
- data/test/functional/ft_7_tags.rb +238 -50
- data/test/functional/ft_8_participant_consumption.rb +27 -21
- data/test/functional/ft_9_subprocesses.rb +48 -18
- data/test/functional/restart_base.rb +4 -6
- data/test/functional/rt_0_wait.rb +10 -10
- data/test/functional/rt_1_listen.rb +6 -6
- data/test/functional/rt_2_errors.rb +12 -12
- data/test/functional/rt_3_once.rb +17 -12
- data/test/functional/rt_4_cron.rb +17 -17
- data/test/functional/rt_5_timeout.rb +13 -13
- data/test/functional/signals.rb +103 -0
- data/test/functional/storage.rb +730 -0
- data/test/functional/storage_helper.rb +48 -35
- data/test/functional/test.rb +6 -2
- data/test/misc/idle.rb +21 -0
- data/test/misc/light.rb +29 -0
- data/test/path_helper.rb +1 -1
- data/test/test.rb +2 -5
- data/test/test_helper.rb +13 -0
- data/test/unit/test.rb +1 -4
- data/test/unit/ut_0_ruby_reader.rb +25 -9
- data/test/unit/ut_10_participants.rb +47 -0
- data/test/unit/ut_11_lookup.rb +59 -2
- data/test/unit/ut_12_wait_logger.rb +123 -0
- data/test/unit/ut_14_is_uri.rb +1 -1
- data/test/unit/ut_15_util.rb +1 -1
- data/test/unit/ut_16_reader.rb +136 -14
- data/test/unit/ut_17_merge.rb +155 -0
- data/test/unit/ut_19_part_template.rb +1 -1
- data/test/unit/ut_1_fei.rb +11 -2
- data/test/unit/ut_20_composite_storage.rb +27 -1
- data/test/unit/{ut_21_participant_list.rb → ut_21_svc_participant_list.rb} +2 -3
- data/test/unit/ut_22_filter.rb +231 -10
- data/test/unit/ut_23_svc_tracker.rb +48 -0
- data/test/unit/ut_24_radial_reader.rb +458 -0
- data/test/unit/ut_25_process_status.rb +143 -0
- data/test/unit/ut_26_deep.rb +131 -0
- data/test/unit/ut_2_dashboard.rb +114 -0
- data/test/unit/ut_3_worker.rb +54 -0
- data/test/unit/ut_4_expmap.rb +1 -1
- data/test/unit/ut_5_tree.rb +23 -23
- data/test/unit/ut_6_condition.rb +71 -29
- data/test/unit/ut_7_workitem.rb +18 -4
- data/test/unit/ut_8_tree_to_dot.rb +1 -1
- data/test/unit/ut_9_xml_reader.rb +1 -1
- metadata +142 -63
- data/jruby_issue.txt +0 -32
- data/lib/ruote/engine/process_status.rb +0 -403
- data/lib/ruote/log/pretty.rb +0 -165
- data/lib/ruote/log/test_logger.rb +0 -204
- data/lib/ruote/util/serializer.rb +0 -103
- data/phil.txt +0 -14
- data/test/functional/eft_33_let.rb +0 -31
- data/test/functional/ft_19_alias.rb +0 -33
- data/test/functional/ft_47_wfid_generator.rb +0 -54
- data/test/unit/storage.rb +0 -403
- data/test/unit/storages.rb +0 -37
- data/test/unit/ut_13_serializer.rb +0 -65
- data/test/unit/ut_18_engine.rb +0 -47
- data/test/unit/ut_3_wait_logger.rb +0 -39
data/lib/ruote-fs.rb
ADDED
data/lib/ruote.rb
CHANGED
|
@@ -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
|
|
data/lib/ruote/context.rb
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#--
|
|
2
|
-
# Copyright (c) 2005-
|
|
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 :
|
|
41
|
-
attr_accessor :engine
|
|
40
|
+
attr_accessor :dashboard
|
|
42
41
|
|
|
43
|
-
def initialize(storage
|
|
42
|
+
def initialize(storage)
|
|
44
43
|
|
|
45
44
|
@storage = storage
|
|
46
45
|
@storage.context = self
|
|
47
46
|
|
|
48
|
-
@
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
aa = [ self, opts ].compact
|
|
115
137
|
|
|
116
|
-
|
|
138
|
+
service = if klass
|
|
117
139
|
|
|
118
|
-
|
|
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
|
-
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
@
|
|
145
|
-
|
|
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.
|
|
229
|
+
@services.keys.sort.collect { |k| @services[k] }
|
|
148
230
|
end
|
|
149
231
|
|
|
150
232
|
protected
|
|
151
233
|
|
|
152
|
-
def
|
|
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(
|
|
241
|
+
default_conf.merge(conf).each do |key, value|
|
|
160
242
|
|
|
161
|
-
|
|
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
|
+
|