dynflow 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 skipped step
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: [:skipped, :running]
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
- loose_manager_and_set_future manager.execution_plan.id
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 loose_manager_and_set_future(execution_plan_id)
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]
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = '0.6.2'
2
+ VERSION = '0.7.0'
3
3
  end
@@ -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
@@ -96,8 +96,8 @@ module Dynflow
96
96
  end
97
97
  end
98
98
 
99
- it 'should be :success' do
100
- execution_plan.result.must_equal :success
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
@@ -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 :success
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
@@ -103,7 +103,7 @@ module PersistenceAdapterTest
103
103
 
104
104
  end
105
105
 
106
- class SequelTest < MiniTest::Test
106
+ class SequelTest < MiniTest::Spec
107
107
  include PersistenceAdapterTest
108
108
 
109
109
  def storage
@@ -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::CancellablePolling
266
+ include Dynflow::Action::Polling
267
+ include Dynflow::Action::Cancellable
267
268
 
268
- Cancel = Dynflow::Action::CancellablePolling::Cancel
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 cancel_external_task
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