dynflow 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ require_relative 'example_helper'
5
+ require 'tmpdir'
6
+
7
+ class SampleAction < Dynflow::Action
8
+ def plan
9
+ number = rand(1e10)
10
+ puts "Plannin action: #{number}"
11
+ plan_self(number: number)
12
+ end
13
+
14
+ def run
15
+ puts "Running action: #{input[:number]}"
16
+ end
17
+ end
18
+
19
+ class RemoteExecutorExample
20
+ class << self
21
+
22
+ def run_server
23
+ world = ExampleHelper.create_world(persistence_adapter: persistence_adapter)
24
+ listener = Dynflow::Listeners::Socket.new world, socket
25
+
26
+ Thread.new { Dynflow::Daemon.new(listener, world).run }
27
+ ExampleHelper.run_web_console(world)
28
+ ensure
29
+ File.delete(db_path)
30
+ end
31
+
32
+ def run_client
33
+ executor = ->(world) { Dynflow::Executors::RemoteViaSocket.new(world, socket) }
34
+ world = ExampleHelper.create_world(persistence_adapter: persistence_adapter,
35
+ executor: executor)
36
+
37
+ loop do
38
+ world.trigger(SampleAction).finished.wait
39
+ sleep 0.5
40
+ end
41
+ end
42
+
43
+ def socket
44
+ File.join(Dir.tmpdir, 'dynflow_socket')
45
+ end
46
+
47
+ def persistence_adapter
48
+ Dynflow::PersistenceAdapters::Sequel.new "sqlite://#{db_path}"
49
+ end
50
+
51
+ def db_path
52
+ File.expand_path("../remote_executor_db.sqlite", __FILE__)
53
+ end
54
+
55
+ end
56
+ end
57
+
58
+ command = ARGV.first || 'server'
59
+
60
+ if $0 == __FILE__
61
+ case command
62
+ when 'server'
63
+ puts <<MSG
64
+ The server is starting…. You can send the work to it by running:
65
+
66
+ #{$0} client
67
+
68
+ MSG
69
+ RemoteExecutorExample.run_server
70
+ when 'client'
71
+ RemoteExecutorExample.run_client
72
+ else
73
+ puts "Unknown command #{comment}"
74
+ exit 1
75
+ end
76
+ end
data/lib/dynflow.rb CHANGED
@@ -14,6 +14,7 @@ module Dynflow
14
14
  class Error < StandardError
15
15
  end
16
16
 
17
+ require 'dynflow/errors'
17
18
  require 'dynflow/future'
18
19
  require 'dynflow/micro_actor'
19
20
  require 'dynflow/serializable'
@@ -9,16 +9,19 @@ module Dynflow
9
9
  include Algebrick::Matching
10
10
 
11
11
  require 'dynflow/action/format'
12
- extend Format
12
+ extend Action::Format
13
13
 
14
14
  require 'dynflow/action/progress'
15
- include Progress
15
+ include Action::Progress
16
+
17
+ require 'dynflow/action/rescue'
18
+ include Action::Rescue
16
19
 
17
20
  require 'dynflow/action/suspended'
18
21
  require 'dynflow/action/missing'
19
22
 
20
23
  require 'dynflow/action/polling'
21
- require 'dynflow/action/cancellable_polling'
24
+ require 'dynflow/action/cancellable'
22
25
 
23
26
  def self.all_children
24
27
  children.values.inject(children.values) do |children, child|
@@ -56,6 +59,7 @@ module Dynflow
56
59
  end
57
60
  variants Executable, Present = atom
58
61
  end
62
+ Skip = Algebrick.atom
59
63
 
60
64
  module Executable
61
65
  def execute_method_name
@@ -165,7 +169,7 @@ module Dynflow
165
169
  phase! Present
166
170
  plan_step.
167
171
  planned_steps(execution_plan).
168
- map { |s| s.action execution_plan }.
172
+ map { |s| s.action(execution_plan) }.
169
173
  select { |a| a.is_a?(filter) }
170
174
  end
171
175
 
@@ -290,6 +294,10 @@ module Dynflow
290
294
  end
291
295
  remove_method :finalize
292
296
 
297
+ def run_accepts_events?
298
+ method(:run).arity != 0
299
+ end
300
+
293
301
  def self.new_from_hash(hash, world)
294
302
  new(hash, world)
295
303
  end
@@ -347,7 +355,7 @@ module Dynflow
347
355
  end
348
356
 
349
357
  def with_error_handling(&block)
350
- raise "wrong state #{self.state}" unless self.state == :running
358
+ raise "wrong state #{self.state}" unless [:skipping, :running].include?(self.state)
351
359
 
352
360
  begin
353
361
  catch(ERROR) { block.call }
@@ -360,6 +368,8 @@ module Dynflow
360
368
  case self.state
361
369
  when :running
362
370
  self.state = :success
371
+ when :skipping
372
+ self.state = :skipped
363
373
  when :suspended, :error
364
374
  else
365
375
  raise "wrong state #{self.state}"
@@ -412,19 +422,25 @@ module Dynflow
412
422
  when state == :running
413
423
  raise NotImplementedError, 'recovery after restart is not implemented'
414
424
 
415
- when [:pending, :error, :suspended].include?(state)
416
- if [:pending, :error].include?(state) && event
425
+ when [:pending, :error, :skipping, :suspended].include?(state)
426
+ if event && state != :suspended
417
427
  raise 'event can be processed only when in suspended state'
418
428
  end
419
429
 
420
- self.state = :running
430
+ self.state = :running unless self.state == :skipping
421
431
  save_state
422
432
  with_error_handling do
423
- result = catch(SUSPEND) do
424
- world.middleware.execute(:run, self, *[event].compact) { |*args| run(*args) }
425
- end
426
- if result == SUSPEND
427
- self.state = :suspended
433
+ event = Skip if state == :skipping
434
+
435
+ # we run the Skip event only when the run accepts events
436
+ if event != Skip || run_accepts_events?
437
+ result = catch(SUSPEND) do
438
+ world.middleware.execute(:run, self, *[event].compact) do |*args|
439
+ run(*args)
440
+ end
441
+ end
442
+
443
+ self.state = :suspended if result == SUSPEND
428
444
  end
429
445
 
430
446
  check_serializable :output
@@ -1,17 +1,16 @@
1
1
  module Dynflow
2
- module Action::CancellablePolling
3
- include Action::Polling
2
+ module Action::Cancellable
4
3
  Cancel = Algebrick.atom
5
4
 
6
5
  def run(event = nil)
7
6
  if Cancel === event
8
- self.external_task = cancel_external_task
7
+ cancel!
9
8
  else
10
9
  super event
11
10
  end
12
11
  end
13
12
 
14
- def cancel_external_task
13
+ def cancel!
15
14
  NotImplementedError
16
15
  end
17
16
  end
@@ -0,0 +1,59 @@
1
+ module Dynflow
2
+ module Action::Rescue
3
+
4
+ Strategy = Algebrick.type do
5
+ variants Skip = atom, Pause = atom
6
+ end
7
+
8
+ SuggestedStrategy = Algebrick.type do
9
+ fields! action: Action,
10
+ strategy: Strategy
11
+ end
12
+
13
+ # What strategy should be used for rescuing from error in
14
+ # the action or its sub actions
15
+ #
16
+ # @return Strategy
17
+ #
18
+ # When determining the strategy, the algorithm starts from the
19
+ # entry action that by default takes the strategy from #rescue_strategy_for_self
20
+ # and #rescue_strategy_for_planned_actions and combines them together.
21
+ def rescue_strategy
22
+ suggested_strategies = []
23
+
24
+ if self.steps.compact.any? { |step| step.state == :error }
25
+ suggested_strategies << SuggestedStrategy[self, rescue_strategy_for_self]
26
+ end
27
+
28
+ self.planned_actions.each do |planned_action|
29
+ suggested_strategies << SuggestedStrategy[planned_action, rescue_strategy_for_planned_action(planned_action)]
30
+ end
31
+
32
+ combine_suggested_strategies(suggested_strategies)
33
+ end
34
+
35
+ # Override when another strategy should be used for rescuing from
36
+ # error on the action
37
+ def rescue_strategy_for_self
38
+ return Pause
39
+ end
40
+
41
+ # Override when the action should override the rescue
42
+ # strategy of an action it planned
43
+ def rescue_strategy_for_planned_action(action)
44
+ action.rescue_strategy
45
+ end
46
+
47
+ # Override when different appraoch should be taken for combining
48
+ # the suggested strategies
49
+ def combine_suggested_strategies(suggested_strategies)
50
+ if suggested_strategies.empty? ||
51
+ suggested_strategies.all? { |suggested_strategy| suggested_strategy.strategy == Skip }
52
+ return Skip
53
+ else
54
+ return Pause
55
+ end
56
+ end
57
+ end
58
+ end
59
+
@@ -0,0 +1,28 @@
1
+ module Dynflow
2
+ module Errors
3
+ class RescueError < StandardError; end
4
+
5
+ # placeholder in case the deserialized error is no longer available
6
+ class UnknownError < StandardError
7
+ def self.for_exception_class(class_name)
8
+ Class.new(self) do
9
+ define_singleton_method :name do
10
+ class_name
11
+ end
12
+ end
13
+ end
14
+
15
+ def self.inspect
16
+ "#{UnknownError.name}[#{name}]"
17
+ end
18
+
19
+ def self.to_s
20
+ inspect
21
+ end
22
+
23
+ def inspect
24
+ "#{self.class.inspect}: #{message}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -4,6 +4,7 @@ module Dynflow
4
4
 
5
5
  # TODO extract planning logic to an extra class ExecutionPlanner
6
6
  class ExecutionPlan < Serializable
7
+
7
8
  include Algebrick::TypeCheck
8
9
  include Stateful
9
10
 
@@ -83,7 +84,9 @@ module Dynflow
83
84
  all_steps = steps.values
84
85
  if all_steps.any? { |step| step.state == :error }
85
86
  return :error
86
- elsif all_steps.all? { |step| [:success, :skipped].include?(step.state) }
87
+ elsif all_steps.any? { |step| [:skipping, :skipped].include?(step.state) }
88
+ return :warning
89
+ elsif all_steps.all? { |step| step.state == :success }
87
90
  return :success
88
91
  else
89
92
  return :pending
@@ -98,6 +101,36 @@ module Dynflow
98
101
  steps.values.map(&:error).compact
99
102
  end
100
103
 
104
+ def rescue_strategy
105
+ Type! entry_action.rescue_strategy, Action::Rescue::Strategy
106
+ end
107
+
108
+ def rescue_plan_id
109
+ case rescue_strategy
110
+ when Action::Rescue::Pause
111
+ nil
112
+ when Action::Rescue::Skip
113
+ failed_steps.each { |step| self.skip(step) }
114
+ self.id
115
+ end
116
+ end
117
+
118
+ def failed_steps
119
+ steps_in_state(:error)
120
+ end
121
+
122
+ def steps_in_state(*states)
123
+ self.steps.values.find_all {|step| states.include?(step.state) }
124
+ end
125
+
126
+ def rescue_from_error
127
+ if rescue_plan_id = self.rescue_plan_id
128
+ @world.execute(rescue_plan_id)
129
+ else
130
+ raise Errors::RescueError, 'Unable to rescue from the error'
131
+ end
132
+ end
133
+
101
134
  def generate_action_id
102
135
  @last_action_id ||= 0
103
136
  @last_action_id += 1
@@ -137,11 +170,7 @@ module Dynflow
137
170
  end
138
171
 
139
172
  def skip(step)
140
- raise "plan step can't be skipped" if step.is_a? Steps::PlanStep
141
- steps_to_skip = steps_to_skip(step).each do |s|
142
- s.state = :skipped
143
- s.save
144
- end
173
+ steps_to_skip = steps_to_skip(step).each(&:mark_to_skip)
145
174
  self.save
146
175
  return steps_to_skip
147
176
  end
@@ -269,12 +298,14 @@ module Dynflow
269
298
  plan_total > 0 ? (plan_done / plan_total) : 1
270
299
  end
271
300
 
272
- # @return [Array<Action>] actions in Present phase, consider using
273
- # Steps::Abstract#action instead
301
+ def entry_action
302
+ @entry_action ||= root_plan_step.action(self)
303
+ end
304
+
305
+ # @return [Array<Action>] actions in Present phase
274
306
  def actions
275
307
  @actions ||= begin
276
- action_ids = steps.reduce({}) { |h, (_, s)| h.update s.action_id => s }
277
- action_ids.map { |_, step| step.action self }
308
+ [entry_action] + entry_action.all_planned_actions
278
309
  end
279
310
  end
280
311
 
@@ -50,6 +50,10 @@ module Dynflow
50
50
  raise NotImplementedError
51
51
  end
52
52
 
53
+ def mark_to_skip
54
+ raise NotImplementedError
55
+ end
56
+
53
57
  def persistence
54
58
  world.persistence
55
59
  end
@@ -59,7 +63,7 @@ module Dynflow
59
63
  end
60
64
 
61
65
  def self.states
62
- @states ||= [:pending, :running, :success, :suspended, :skipped, :error]
66
+ @states ||= [:pending, :running, :success, :suspended, :skipping, :skipped, :error]
63
67
  end
64
68
 
65
69
  def execute(*args)
@@ -110,8 +114,15 @@ module Dynflow
110
114
  # details, human outputs, etc.
111
115
  def action(execution_plan)
112
116
  attributes = world.persistence.adapter.load_action(execution_plan_id, action_id)
113
- Action.from_hash(attributes.update(phase: Action::Present, execution_plan: execution_plan),
114
- world)
117
+ Action.from_hash(attributes.update(phase: Action::Present, execution_plan: execution_plan), world)
118
+ end
119
+
120
+ def skippable?
121
+ self.state == :error
122
+ end
123
+
124
+ def cancellable?
125
+ false
115
126
  end
116
127
 
117
128
  protected
@@ -31,7 +31,12 @@ module Dynflow
31
31
  end
32
32
 
33
33
  def self.new_from_hash(hash)
34
- self.new(hash[:exception_class].constantize, hash[:message], hash[:backtrace], nil)
34
+ exception_class = begin
35
+ hash[:exception_class].constantize
36
+ rescue NameError
37
+ Errors::UnknownError.for_exception_class(hash[:exception_class])
38
+ end
39
+ self.new(exception_class, hash[:message], hash[:backtrace], nil)
35
40
  end
36
41
 
37
42
  def to_hash
@@ -18,6 +18,11 @@ module Dynflow
18
18
  Action::Finalize
19
19
  end
20
20
 
21
+ def mark_to_skip
22
+ self.state = :skipped
23
+ self.save
24
+ end
25
+
21
26
  end
22
27
  end
23
28
  end