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