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.
- 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
@@ -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
data/lib/dynflow/action.rb
CHANGED
@@ -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/
|
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
|
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
|
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
|
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
|
-
|
424
|
-
|
425
|
-
|
426
|
-
if
|
427
|
-
|
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::
|
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
|
-
|
7
|
+
cancel!
|
9
8
|
else
|
10
9
|
super event
|
11
10
|
end
|
12
11
|
end
|
13
12
|
|
14
|
-
def
|
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.
|
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
|
-
|
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
|
-
|
273
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|