dynflow 0.6.2 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|