dynflow 0.6.2 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|