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.
@@ -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