dynflow 0.6.2 → 0.7.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/README.md +34 -14
- data/doc/images/screenshot.png +0 -0
- data/examples/example_helper.rb +47 -0
- data/examples/orchestrate.rb +58 -22
- data/examples/orchestrate_evented.rb +174 -0
- data/examples/remote_executor.rb +76 -0
- data/lib/dynflow.rb +1 -0
- data/lib/dynflow/action.rb +29 -13
- data/lib/dynflow/action/{cancellable_polling.rb → cancellable.rb} +3 -4
- data/lib/dynflow/action/rescue.rb +59 -0
- data/lib/dynflow/errors.rb +28 -0
- data/lib/dynflow/execution_plan.rb +41 -10
- data/lib/dynflow/execution_plan/steps/abstract.rb +14 -3
- data/lib/dynflow/execution_plan/steps/error.rb +6 -1
- data/lib/dynflow/execution_plan/steps/finalize_step.rb +5 -0
- data/lib/dynflow/execution_plan/steps/run_step.rb +20 -2
- data/lib/dynflow/executors/parallel/core.rb +31 -3
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web_console.rb +13 -1
- data/lib/dynflow/world.rb +4 -2
- data/test/execution_plan_test.rb +15 -2
- data/test/executor_test.rb +1 -1
- data/test/persistance_adapters_test.rb +1 -1
- data/test/rescue_test.rb +164 -0
- data/test/support/code_workflow_example.rb +5 -4
- data/test/support/rescue_example.rb +73 -0
- data/test/test_helper.rb +6 -3
- data/web/assets/stylesheets/application.css +4 -0
- data/web/views/flow_step.erb +5 -1
- data/web/views/show.erb +3 -1
- metadata +13 -6
- data/examples/generate_work_for_daemon.rb +0 -24
- data/examples/run_daemon.rb +0 -17
- data/examples/web_console.rb +0 -29
@@ -4,18 +4,36 @@ module Dynflow
|
|
4
4
|
|
5
5
|
def self.state_transitions
|
6
6
|
@state_transitions ||= {
|
7
|
-
pending: [:running, :skipped], # :skipped when it cannot be run because it depends on
|
7
|
+
pending: [:running, :skipped], # :skipped when it cannot be run because it depends on skipping step
|
8
8
|
running: [:success, :error, :suspended],
|
9
9
|
success: [:suspended], # after not-done process_update
|
10
10
|
suspended: [:running, :error], # process_update, e.g. error in setup_progress_updates
|
11
|
+
skipping: [:error, :skipped], # waiting for the skip method to be called
|
11
12
|
skipped: [],
|
12
|
-
error: [:
|
13
|
+
error: [:skipping, :running]
|
13
14
|
}
|
14
15
|
end
|
15
16
|
|
16
17
|
def phase
|
17
18
|
Action::Run
|
18
19
|
end
|
20
|
+
|
21
|
+
def cancellable?
|
22
|
+
[:suspended, :running].include?(self.state) &&
|
23
|
+
self.action_class < Action::Cancellable
|
24
|
+
end
|
25
|
+
|
26
|
+
def mark_to_skip
|
27
|
+
case self.state
|
28
|
+
when :error
|
29
|
+
self.state = :skipping
|
30
|
+
when :pending
|
31
|
+
self.state = :skipped
|
32
|
+
else
|
33
|
+
raise "Skipping step in #{self.state} is not supported"
|
34
|
+
end
|
35
|
+
self.save
|
36
|
+
end
|
19
37
|
end
|
20
38
|
end
|
21
39
|
end
|
@@ -14,6 +14,7 @@ module Dynflow
|
|
14
14
|
@world = Type! world, World
|
15
15
|
@pool = Pool.new(self, pool_size, world.transaction_adapter)
|
16
16
|
@execution_plan_managers = {}
|
17
|
+
@plan_ids_in_rescue = Set.new
|
17
18
|
end
|
18
19
|
|
19
20
|
def on_message(message)
|
@@ -77,13 +78,29 @@ module Dynflow
|
|
77
78
|
|
78
79
|
def continue_manager(manager, next_work)
|
79
80
|
if manager.done?
|
80
|
-
|
81
|
-
try_to_terminate
|
81
|
+
finish_plan manager.execution_plan.id
|
82
82
|
else
|
83
83
|
feed_pool next_work
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
87
|
+
def rescue?(manager)
|
88
|
+
@world.auto_rescue && manager.execution_plan.state == :paused &&
|
89
|
+
!@plan_ids_in_rescue.include?(manager.execution_plan.id)
|
90
|
+
end
|
91
|
+
|
92
|
+
def rescue!(manager)
|
93
|
+
# TODO: after moving to concurrent-ruby actors, there should be better place
|
94
|
+
# to put this logic of making sure we don't run rescues in endless loop
|
95
|
+
@plan_ids_in_rescue << manager.execution_plan.id
|
96
|
+
rescue_plan_id = manager.execution_plan.rescue_plan_id
|
97
|
+
if rescue_plan_id
|
98
|
+
self << Parallel::Execution[rescue_plan_id, manager.future]
|
99
|
+
else
|
100
|
+
set_future(manager)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
87
104
|
def feed_pool(work_items)
|
88
105
|
Type! work_items, Array, Work, NilClass
|
89
106
|
return if work_items.nil?
|
@@ -92,11 +109,22 @@ module Dynflow
|
|
92
109
|
work_items.each { |new_work| @pool << new_work }
|
93
110
|
end
|
94
111
|
|
95
|
-
def
|
112
|
+
def finish_plan(execution_plan_id)
|
96
113
|
manager = @execution_plan_managers.delete(execution_plan_id)
|
114
|
+
if rescue?(manager)
|
115
|
+
rescue!(manager)
|
116
|
+
else
|
117
|
+
set_future(manager)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def set_future(manager)
|
122
|
+
@plan_ids_in_rescue.delete(manager.execution_plan.id)
|
97
123
|
manager.future.resolve manager.execution_plan
|
124
|
+
try_to_terminate
|
98
125
|
end
|
99
126
|
|
127
|
+
|
100
128
|
def event(event)
|
101
129
|
Type! event, Parallel::Event
|
102
130
|
execution_plan_manager = @execution_plan_managers[event.execution_plan_id]
|
data/lib/dynflow/version.rb
CHANGED
data/lib/dynflow/web_console.rb
CHANGED
@@ -95,7 +95,7 @@ module Dynflow
|
|
95
95
|
classes << "success"
|
96
96
|
when :error
|
97
97
|
classes << "error"
|
98
|
-
when :skipped
|
98
|
+
when :skipped, :skipping
|
99
99
|
classes << "skipped"
|
100
100
|
end
|
101
101
|
return classes.join(" ")
|
@@ -256,6 +256,7 @@ module Dynflow
|
|
256
256
|
|
257
257
|
get('/:id') do |id|
|
258
258
|
@plan = world.persistence.load_execution_plan(id)
|
259
|
+
@notice = params[:notice]
|
259
260
|
erb :show
|
260
261
|
end
|
261
262
|
|
@@ -282,5 +283,16 @@ module Dynflow
|
|
282
283
|
end
|
283
284
|
end
|
284
285
|
|
286
|
+
post('/:id/cancel/:step_id') do |id, step_id|
|
287
|
+
plan = world.persistence.load_execution_plan(id)
|
288
|
+
step = plan.steps[step_id.to_i]
|
289
|
+
if step.cancellable?
|
290
|
+
world.event(plan.id, step.id, Dynflow::Action::Cancellable::Cancel)
|
291
|
+
redirect(url "/#{plan.id}?notice=#{url_encode('The step was asked to cancel')}")
|
292
|
+
else
|
293
|
+
redirect(url "/#{plan.id}?notice=#{url_encode('The step does not support cancelling')}")
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
285
297
|
end
|
286
298
|
end
|
data/lib/dynflow/world.rb
CHANGED
@@ -3,7 +3,7 @@ module Dynflow
|
|
3
3
|
include Algebrick::TypeCheck
|
4
4
|
|
5
5
|
attr_reader :executor, :persistence, :transaction_adapter, :action_classes, :subscription_index,
|
6
|
-
:logger_adapter, :options, :middleware
|
6
|
+
:logger_adapter, :options, :middleware, :auto_rescue
|
7
7
|
|
8
8
|
def initialize(options_hash = {})
|
9
9
|
@options = default_options.merge options_hash
|
@@ -13,6 +13,7 @@ module Dynflow
|
|
13
13
|
@persistence = Persistence.new(self, persistence_adapter)
|
14
14
|
@executor = Type! option_val(:executor), Executors::Abstract
|
15
15
|
@action_classes = option_val(:action_classes)
|
16
|
+
@auto_rescue = option_val(:auto_rescue)
|
16
17
|
@middleware = Middleware::World.new
|
17
18
|
calculate_subscription_index
|
18
19
|
|
@@ -26,7 +27,8 @@ module Dynflow
|
|
26
27
|
@default_options ||=
|
27
28
|
{ action_classes: Action.all_children,
|
28
29
|
logger_adapter: LoggerAdapters::Simple.new,
|
29
|
-
executor: -> world { Executors::Parallel.new(world, options[:pool_size]) }
|
30
|
+
executor: -> world { Executors::Parallel.new(world, options[:pool_size]) },
|
31
|
+
auto_rescue: true }
|
30
32
|
end
|
31
33
|
|
32
34
|
def clock
|
data/test/execution_plan_test.rb
CHANGED
@@ -96,8 +96,8 @@ module Dynflow
|
|
96
96
|
end
|
97
97
|
end
|
98
98
|
|
99
|
-
it 'should be :
|
100
|
-
execution_plan.result.must_equal :
|
99
|
+
it 'should be :warning' do
|
100
|
+
execution_plan.result.must_equal :warning
|
101
101
|
end
|
102
102
|
|
103
103
|
end
|
@@ -246,6 +246,19 @@ module Dynflow
|
|
246
246
|
end
|
247
247
|
end
|
248
248
|
end
|
249
|
+
|
250
|
+
describe ExecutionPlan::Steps::Error do
|
251
|
+
|
252
|
+
it "doesn't fail when deserializing with missing class" do
|
253
|
+
error = ExecutionPlan::Steps::Error.new_from_hash(exception_class: "RenamedError",
|
254
|
+
message: "This errror is not longer here",
|
255
|
+
backtrace: [])
|
256
|
+
error.exception_class.name.must_equal "RenamedError"
|
257
|
+
error.exception_class.to_s.must_equal "Dynflow::Errors::UnknownError[RenamedError]"
|
258
|
+
error.exception.inspect.must_equal "Dynflow::Errors::UnknownError[RenamedError]: This errror is not longer here"
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|
249
262
|
end
|
250
263
|
end
|
251
264
|
end
|
data/test/executor_test.rb
CHANGED
@@ -556,7 +556,7 @@ module Dynflow
|
|
556
556
|
|
557
557
|
it "runs all pending steps except skipped" do
|
558
558
|
resumed_execution_plan.state.must_equal :stopped
|
559
|
-
resumed_execution_plan.result.must_equal :
|
559
|
+
resumed_execution_plan.result.must_equal :warning
|
560
560
|
|
561
561
|
run_triages = TestExecutionLog.run.find_all do |action_class, input|
|
562
562
|
action_class == Support::CodeWorkflowExample::Triage
|
data/test/rescue_test.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
module Dynflow
|
4
|
+
module RescueTest
|
5
|
+
describe 'on error' do
|
6
|
+
|
7
|
+
Example = Support::RescueExample
|
8
|
+
|
9
|
+
include WorldInstance
|
10
|
+
|
11
|
+
def execute(*args)
|
12
|
+
plan = world.plan(*args)
|
13
|
+
raise plan.errors.first if plan.error?
|
14
|
+
world.execute(plan.id).value
|
15
|
+
end
|
16
|
+
|
17
|
+
let :rescued_plan do
|
18
|
+
execution_plan.rescue_from_error.value
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'of simple skippable action in run phase' do
|
22
|
+
|
23
|
+
let :execution_plan do
|
24
|
+
execute(Example::ActionWithSkip, 1, :error_on_run)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'suggests skipping the action' do
|
28
|
+
execution_plan.rescue_strategy.must_equal Action::Rescue::Skip
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'skips the action and continues' do
|
32
|
+
rescued_plan.state.must_equal :stopped
|
33
|
+
rescued_plan.result.must_equal :warning
|
34
|
+
rescued_plan.entry_action.output[:message].
|
35
|
+
must_equal "skipped because some error as you wish"
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'of simple skippable action in finalize phase' do
|
41
|
+
|
42
|
+
let :execution_plan do
|
43
|
+
execute(Example::ActionWithSkip, 1, :error_on_finalize)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'suggests skipping the action' do
|
47
|
+
execution_plan.rescue_strategy.must_equal Action::Rescue::Skip
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'skips the action and continues' do
|
51
|
+
rescued_plan.state.must_equal :stopped
|
52
|
+
rescued_plan.result.must_equal :warning
|
53
|
+
rescued_plan.entry_action.output[:message].must_equal "Been here"
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
describe 'of complex action with skips in run phase' do
|
59
|
+
|
60
|
+
let :execution_plan do
|
61
|
+
execute(Example::ComplexActionWithSkip, :error_on_run)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'suggests skipping the action' do
|
65
|
+
execution_plan.rescue_strategy.must_equal Action::Rescue::Skip
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'skips the action and continues' do
|
69
|
+
rescued_plan.state.must_equal :stopped
|
70
|
+
rescued_plan.result.must_equal :warning
|
71
|
+
skipped_action = rescued_plan.actions.find do |action|
|
72
|
+
action.run_step && action.run_step.state == :skipped
|
73
|
+
end
|
74
|
+
skipped_action.output[:message].must_equal "skipped because some error as you wish"
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
describe 'of complex action with skips in finalize phase' do
|
80
|
+
|
81
|
+
let :execution_plan do
|
82
|
+
execute(Example::ComplexActionWithSkip, :error_on_finalize)
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'suggests skipping the action' do
|
86
|
+
execution_plan.rescue_strategy.must_equal Action::Rescue::Skip
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'skips the action and continues' do
|
90
|
+
rescued_plan.state.must_equal :stopped
|
91
|
+
rescued_plan.result.must_equal :warning
|
92
|
+
skipped_action = rescued_plan.actions.find do |action|
|
93
|
+
action.steps.find { |step| step && step.state == :skipped }
|
94
|
+
end
|
95
|
+
skipped_action.output[:message].must_equal "Been here"
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
describe 'of complex action without skips' do
|
101
|
+
|
102
|
+
let :execution_plan do
|
103
|
+
execute(Example::ComplexActionWithoutSkip, :error_on_run)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'suggests pausing the plan' do
|
107
|
+
execution_plan.rescue_strategy.must_equal Action::Rescue::Pause
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'fails rescuing' do
|
111
|
+
lambda { rescued_plan }.must_raise Errors::RescueError
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
describe 'auto rescue' do
|
117
|
+
|
118
|
+
def world
|
119
|
+
@world ||= WorldInstance.create_world(auto_rescue: true)
|
120
|
+
end
|
121
|
+
|
122
|
+
describe 'of plan with skips' do
|
123
|
+
|
124
|
+
let :execution_plan do
|
125
|
+
execute(Example::ComplexActionWithSkip, :error_on_run)
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'skips the action and continues automatically' do
|
129
|
+
execution_plan.state.must_equal :stopped
|
130
|
+
execution_plan.result.must_equal :warning
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
describe 'of plan faild on auto-rescue' do
|
136
|
+
|
137
|
+
let :execution_plan do
|
138
|
+
execute(Example::ActionWithSkip, 1, :error_on_skip)
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'tryied to rescue only once' do
|
142
|
+
execution_plan.state.must_equal :paused
|
143
|
+
execution_plan.result.must_equal :error
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
describe 'of plan without skips' do
|
149
|
+
|
150
|
+
let :execution_plan do
|
151
|
+
execute(Example::ComplexActionWithoutSkip, :error_on_run)
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'skips the action and continues automatically' do
|
155
|
+
execution_plan.state.must_equal :paused
|
156
|
+
execution_plan.result.must_equal :error
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -263,9 +263,10 @@ module Support
|
|
263
263
|
end
|
264
264
|
|
265
265
|
class CancelableSuspended < Dynflow::Action
|
266
|
-
include Dynflow::Action::
|
266
|
+
include Dynflow::Action::Polling
|
267
|
+
include Dynflow::Action::Cancellable
|
267
268
|
|
268
|
-
Cancel = Dynflow::Action::
|
269
|
+
Cancel = Dynflow::Action::Cancellable::Cancel
|
269
270
|
|
270
271
|
def invoke_external_task
|
271
272
|
{ progress: 0 }
|
@@ -288,9 +289,9 @@ module Support
|
|
288
289
|
{ progress: new_progress }
|
289
290
|
end
|
290
291
|
|
291
|
-
def
|
292
|
+
def cancel!
|
292
293
|
if input[:text] !~ /cancel-fail/
|
293
|
-
external_task.merge(cancelled: true)
|
294
|
+
self.external_task = external_task.merge(cancelled: true)
|
294
295
|
else
|
295
296
|
error! 'action cancelled'
|
296
297
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Support
|
4
|
+
module RescueExample
|
5
|
+
|
6
|
+
class ComplexActionWithSkip < Dynflow::Action
|
7
|
+
|
8
|
+
def plan(error_state)
|
9
|
+
sequence do
|
10
|
+
concurrence do
|
11
|
+
plan_action(ActionWithSkip, 3, :success)
|
12
|
+
plan_action(ActionWithSkip, 4, error_state)
|
13
|
+
end
|
14
|
+
plan_action(ActionWithSkip, 5, :success)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class ComplexActionWithoutSkip < ComplexActionWithSkip
|
20
|
+
|
21
|
+
def rescue_strategy_for_planned_action(action)
|
22
|
+
# enforce pause even when error on skipable action
|
23
|
+
Dynflow::Action::Rescue::Pause
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
class AbstractAction < Dynflow::Action
|
29
|
+
|
30
|
+
def plan(identifier, desired_state)
|
31
|
+
plan_self(identifier: identifier, desired_state: desired_state)
|
32
|
+
end
|
33
|
+
|
34
|
+
def run
|
35
|
+
case input[:desired_state].to_sym
|
36
|
+
when :success, :error_on_finalize
|
37
|
+
output[:message] = 'Been here'
|
38
|
+
when :error_on_run, :error_on_skip
|
39
|
+
raise 'some error as you wish'
|
40
|
+
when :pending
|
41
|
+
raise 'we were not supposed to get here'
|
42
|
+
else
|
43
|
+
raise "unkown desired state #{input[:desired_state]}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def finalize
|
48
|
+
case input[:desired_state].to_sym
|
49
|
+
when :error_on_finalize, :error_on_skip
|
50
|
+
raise 'some error as you wish'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
class ActionWithSkip < AbstractAction
|
57
|
+
|
58
|
+
def run(event = nil)
|
59
|
+
if event === Dynflow::Action::Skip
|
60
|
+
output[:message] = "skipped because #{self.error.message}"
|
61
|
+
raise 'we failed on skip as well' if input[:desired_state].to_sym == :error_on_skip
|
62
|
+
else
|
63
|
+
super()
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def rescue_strategy_for_self
|
68
|
+
Dynflow::Action::Rescue::Skip
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|