dynflow 0.0.1 → 0.1.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/Gemfile +12 -4
- data/README.md +4 -4
- data/Rakefile +9 -1
- data/dynflow.gemspec +1 -1
- data/examples/events.rb +5 -5
- data/examples/workflow.rb +4 -4
- data/lib/dynflow.rb +3 -3
- data/lib/dynflow/action.rb +27 -18
- data/lib/dynflow/bus.rb +136 -24
- data/lib/dynflow/dispatcher.rb +1 -1
- data/lib/dynflow/execution_plan.rb +56 -0
- data/lib/dynflow/step.rb +234 -0
- data/lib/dynflow/version.rb +1 -1
- data/test/action_test.rb +6 -5
- data/test/bus_test.rb +127 -32
- data/test/execution_plan_test.rb +121 -0
- data/test/test_helper.rb +1 -80
- metadata +7 -8
- data/lib/dynflow/message.rb +0 -38
- data/lib/dynflow/orch_request.rb +0 -14
- data/lib/dynflow/orch_response.rb +0 -5
- data/test/dispatcher_test.rb +0 -108
data/Gemfile
CHANGED
@@ -1,9 +1,17 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
|
4
|
-
gem 'activesupport'
|
5
|
-
gem 'apipie-params', :git => '/home/inecas/Projects/apipie-params'
|
3
|
+
gemspec
|
6
4
|
|
7
|
-
group :development do
|
5
|
+
group :development, :test do
|
8
6
|
gem 'pry'
|
9
7
|
end
|
8
|
+
|
9
|
+
group :test do
|
10
|
+
gem 'database_cleaner'
|
11
|
+
end
|
12
|
+
|
13
|
+
group :engine do
|
14
|
+
gem "rails", "~> 3.2.8"
|
15
|
+
gem "jquery-rails"
|
16
|
+
gem "sqlite3"
|
17
|
+
end
|
data/README.md
CHANGED
@@ -115,12 +115,12 @@ One can generate the execution plan for an action without actually
|
|
115
115
|
running it:
|
116
116
|
|
117
117
|
```ruby
|
118
|
-
pp Publish.plan(short_article)
|
118
|
+
pp Publish.plan(short_article).actions
|
119
119
|
# the expanded workflow is:
|
120
120
|
# [
|
121
|
-
#
|
122
|
-
#
|
123
|
-
#
|
121
|
+
# Publish: {"title"=>"Short", "body"=>"Short"} ~> {},
|
122
|
+
# Review: {"title"=>"Short", "body"=>"Short"} ~> {},
|
123
|
+
# Print: {"title"=>"Short", "body"=>"Short", "color"=>false} ~> {}
|
124
124
|
# ]
|
125
125
|
```
|
126
126
|
|
data/Rakefile
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
require 'rake/testtask'
|
2
|
+
require 'fileutils'
|
2
3
|
|
4
|
+
desc "Generic tests"
|
3
5
|
Rake::TestTask.new do |t|
|
4
6
|
t.libs << 'lib' << 'test'
|
5
|
-
t.test_files = FileList['test
|
7
|
+
t.test_files = FileList['test/*_test.rb']
|
6
8
|
t.verbose = true
|
7
9
|
end
|
10
|
+
|
11
|
+
namespace :test do
|
12
|
+
desc "All tests"
|
13
|
+
task :all => [:test] do
|
14
|
+
end
|
15
|
+
end
|
data/dynflow.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.version = Dynflow::VERSION
|
8
8
|
s.authors = ["Ivan Necas"]
|
9
9
|
s.email = ["inecas@redhat.com"]
|
10
|
-
s.homepage = "http://github.com/iNecas/
|
10
|
+
s.homepage = "http://github.com/iNecas/dynflow"
|
11
11
|
s.summary = "DYNamic workFLOW engine"
|
12
12
|
s.description = "Generate and executed workflows dynamically based "+
|
13
13
|
"on input data and leave it open for others to jump into it as well"
|
data/examples/events.rb
CHANGED
@@ -60,12 +60,12 @@ Click.trigger('x' => 5, 'y' => 4)
|
|
60
60
|
# your position is [5 - 4]
|
61
61
|
# Good Bye
|
62
62
|
|
63
|
-
pp Click.plan('x' => 5, 'y' => 4)
|
63
|
+
pp Click.plan('x' => 5, 'y' => 4).actions
|
64
64
|
# returns the execution plan for the event (nothing is triggered):
|
65
65
|
# [
|
66
66
|
# since the event is action as well, it could have a run method
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
67
|
+
# [Click: {"x"=>5, "y"=>4} ~> {},
|
68
|
+
# SayHello: {"x"=>5, "y"=>4} ~> {},
|
69
|
+
# SayPosition: {"x"=>5, "y"=>4} ~> {},
|
70
|
+
# SayGoodbye: {"x"=>5, "y"=>4} ~> {}]
|
71
71
|
# ]
|
data/examples/workflow.rb
CHANGED
@@ -104,12 +104,12 @@ short_article = Article.new('Short', 'Short', false)
|
|
104
104
|
long_article = Article.new('Long', 'This is long', false)
|
105
105
|
colorful_article = Article.new('Long Color', 'This is long in color', true)
|
106
106
|
|
107
|
-
pp Publish.plan(short_article)
|
107
|
+
pp Publish.plan(short_article).actions
|
108
108
|
# the expanded workflow is:
|
109
109
|
# [
|
110
|
-
#
|
111
|
-
#
|
112
|
-
#
|
110
|
+
# Publish: {"title"=>"Short", "body"=>"Short"} ~> {},
|
111
|
+
# Review: {"title"=>"Short", "body"=>"Short"} ~> {},
|
112
|
+
# Print: {"title"=>"Short", "body"=>"Short", "color"=>false} ~> {}
|
113
113
|
# ]
|
114
114
|
|
115
115
|
begin
|
data/lib/dynflow.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
1
2
|
require 'dynflow/logger'
|
2
|
-
require 'dynflow/
|
3
|
+
require 'dynflow/execution_plan'
|
3
4
|
require 'dynflow/dispatcher'
|
4
5
|
require 'dynflow/bus'
|
5
|
-
require 'dynflow/
|
6
|
-
require 'dynflow/orch_response'
|
6
|
+
require 'dynflow/step'
|
7
7
|
require 'dynflow/action'
|
8
8
|
|
9
9
|
module Dynflow
|
data/lib/dynflow/action.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
module Dynflow
|
2
|
-
class Action
|
2
|
+
class Action
|
3
3
|
|
4
4
|
# only for the planning phase: flag indicating that the action
|
5
5
|
# was triggered from subscription. If so, the implicit plan
|
6
6
|
# method uses the input of the parent action. Otherwise, the
|
7
7
|
# argument the plan_action is used as default.
|
8
|
-
attr_accessor :from_subscription
|
8
|
+
attr_accessor :execution_plan, :from_subscription, :input, :output
|
9
9
|
|
10
10
|
def self.inherited(child)
|
11
11
|
self.actions << child
|
@@ -23,24 +23,22 @@ module Dynflow
|
|
23
23
|
nil
|
24
24
|
end
|
25
25
|
|
26
|
-
def initialize(input, output =
|
26
|
+
def initialize(input, output = nil)
|
27
27
|
# for preparation phase
|
28
|
-
@execution_plan =
|
28
|
+
@execution_plan = ExecutionPlan.new
|
29
29
|
|
30
|
-
|
31
|
-
|
30
|
+
@input = input
|
31
|
+
@output = output || {}
|
32
32
|
end
|
33
33
|
|
34
|
-
def input
|
35
|
-
@data['input']
|
36
|
-
end
|
37
34
|
|
38
|
-
def
|
39
|
-
|
35
|
+
def ==(other)
|
36
|
+
[self.class.name, self.input, self.output] ==
|
37
|
+
[other.class.name, other.input, other.output]
|
40
38
|
end
|
41
39
|
|
42
|
-
def
|
43
|
-
|
40
|
+
def inspect
|
41
|
+
"#{self.class.name}: #{input.inspect} ~> #{output.inspect}"
|
44
42
|
end
|
45
43
|
|
46
44
|
# the block contains the expression in Apipie::Params::DSL
|
@@ -68,14 +66,25 @@ module Dynflow
|
|
68
66
|
end
|
69
67
|
|
70
68
|
def self.trigger(*args)
|
71
|
-
Dynflow::Bus.trigger(self
|
69
|
+
Dynflow::Bus.trigger(self, *args)
|
72
70
|
end
|
73
71
|
|
74
72
|
def self.plan(*args)
|
75
73
|
action = self.new({})
|
76
74
|
yield action if block_given?
|
77
|
-
|
78
|
-
|
75
|
+
|
76
|
+
plan_step = Step::Plan.new(action)
|
77
|
+
action.execution_plan.plan_steps << plan_step
|
78
|
+
plan_step.catch_errors do
|
79
|
+
action.plan(*args)
|
80
|
+
end
|
81
|
+
|
82
|
+
if action.execution_plan.failed_steps.any?
|
83
|
+
action.execution_plan.status = 'error'
|
84
|
+
else
|
85
|
+
action.add_subscriptions(*args)
|
86
|
+
end
|
87
|
+
|
79
88
|
action.execution_plan
|
80
89
|
end
|
81
90
|
|
@@ -95,7 +104,7 @@ module Dynflow
|
|
95
104
|
|
96
105
|
def plan_self(input)
|
97
106
|
self.input = input
|
98
|
-
@execution_plan <<
|
107
|
+
@execution_plan << self
|
99
108
|
end
|
100
109
|
|
101
110
|
def plan_action(action_class, *args)
|
@@ -114,7 +123,7 @@ module Dynflow
|
|
114
123
|
end
|
115
124
|
|
116
125
|
def validate!
|
117
|
-
self.clss.output_format.validate!(
|
126
|
+
self.clss.output_format.validate!(output)
|
118
127
|
end
|
119
128
|
|
120
129
|
end
|
data/lib/dynflow/bus.rb
CHANGED
@@ -6,51 +6,163 @@ module Dynflow
|
|
6
6
|
class << self
|
7
7
|
extend Forwardable
|
8
8
|
|
9
|
-
def_delegators :impl, :
|
9
|
+
def_delegators :impl, :trigger, :resume, :skip, :preview_execution_plan,
|
10
|
+
:persisted_plans, :persisted_plan, :persisted_step
|
10
11
|
|
11
12
|
def impl
|
12
|
-
@impl ||= Bus
|
13
|
+
@impl ||= Bus.new
|
13
14
|
end
|
14
15
|
attr_writer :impl
|
15
16
|
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
# Entry point for running an action
|
19
|
+
def trigger(action_class, *args)
|
20
|
+
execution_plan = nil
|
21
|
+
in_transaction_if_possible do
|
22
|
+
execution_plan = prepare_execution_plan(action_class, *args)
|
23
|
+
rollback_transaction if execution_plan.status == 'error'
|
24
|
+
end
|
25
|
+
persist_plan_if_possible(execution_plan)
|
26
|
+
unless execution_plan.status == 'error'
|
27
|
+
execute(execution_plan)
|
22
28
|
end
|
29
|
+
return execution_plan
|
23
30
|
end
|
24
31
|
|
25
|
-
def
|
26
|
-
|
27
|
-
action = action_class.new(input, output)
|
28
|
-
action.run if action.respond_to?(:run)
|
29
|
-
return action
|
32
|
+
def prepare_execution_plan(action_class, *args)
|
33
|
+
action_class.plan(*args)
|
30
34
|
end
|
31
35
|
|
32
|
-
|
33
|
-
|
36
|
+
# execution and finalizaition. Usable for resuming paused plan
|
37
|
+
# as well as starting from scratch
|
38
|
+
def execute(execution_plan)
|
39
|
+
run_execution_plan(execution_plan)
|
40
|
+
in_transaction_if_possible do
|
41
|
+
unless self.finalize(execution_plan)
|
42
|
+
rollback_transaction
|
43
|
+
end
|
44
|
+
end
|
45
|
+
execution_plan.persist(true)
|
34
46
|
end
|
35
47
|
|
36
|
-
|
37
|
-
|
48
|
+
alias_method :resume, :execute
|
49
|
+
|
50
|
+
def skip(step)
|
51
|
+
step.status = 'skipped'
|
52
|
+
step.persist
|
38
53
|
end
|
39
54
|
|
40
|
-
|
55
|
+
# return true if everyting worked fine
|
56
|
+
def finalize(execution_plan)
|
57
|
+
success = true
|
58
|
+
if execution_plan.run_steps.any? { |action| ['pending', 'error'].include?(action.status) }
|
59
|
+
success = false
|
60
|
+
else
|
61
|
+
execution_plan.finalize_steps.each(&:replace_references!)
|
62
|
+
execution_plan.finalize_steps.each do |step|
|
63
|
+
break unless success
|
64
|
+
next if %w[skipped].include?(step.status)
|
65
|
+
|
66
|
+
success = step.catch_errors do
|
67
|
+
step.action.finalize(execution_plan.run_steps)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if success
|
73
|
+
execution_plan.status = 'finished'
|
74
|
+
else
|
75
|
+
execution_plan.status = 'paused'
|
76
|
+
end
|
77
|
+
return success
|
78
|
+
end
|
41
79
|
|
42
|
-
|
43
|
-
|
80
|
+
# return true if the run phase finished successfully
|
81
|
+
def run_execution_plan(execution_plan)
|
82
|
+
success = true
|
83
|
+
execution_plan.run_steps.map do |step|
|
84
|
+
next step if !success || %w[skipped success].include?(step.status)
|
85
|
+
step.persist_before_run
|
86
|
+
success = step.catch_errors do
|
87
|
+
step.output = {}
|
88
|
+
step.action.run
|
89
|
+
end
|
90
|
+
step.persist_after_run
|
91
|
+
step
|
44
92
|
end
|
93
|
+
return success
|
94
|
+
end
|
95
|
+
|
96
|
+
def transaction_driver
|
97
|
+
nil
|
98
|
+
end
|
45
99
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
100
|
+
def in_transaction_if_possible
|
101
|
+
if transaction_driver
|
102
|
+
ret = nil
|
103
|
+
transaction_driver.transaction do
|
104
|
+
ret = yield
|
50
105
|
end
|
51
|
-
|
106
|
+
return ret
|
107
|
+
else
|
108
|
+
return yield
|
52
109
|
end
|
110
|
+
end
|
53
111
|
|
112
|
+
def rollback_transaction
|
113
|
+
transaction_driver.rollback if transaction_driver
|
54
114
|
end
|
115
|
+
|
116
|
+
|
117
|
+
def persistence_driver
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
|
121
|
+
def persist_plan_if_possible(execution_plan)
|
122
|
+
if persistence_driver
|
123
|
+
persistence_driver.persist(execution_plan)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def persisted_plans(status = nil)
|
128
|
+
if persistence_driver
|
129
|
+
persistence_driver.persisted_plans(status)
|
130
|
+
else
|
131
|
+
[]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def persisted_plan(persistence_id)
|
136
|
+
if persistence_driver
|
137
|
+
persistence_driver.persisted_plan(persistence_id)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def persisted_step(persistence_id)
|
142
|
+
if persistence_driver
|
143
|
+
persistence_driver.persisted_step(persistence_id)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# performs the planning phase of an action, but rollbacks any db
|
148
|
+
# changes done in this phase. Returns the resulting execution
|
149
|
+
# plan. Suitable for debugging.
|
150
|
+
def preview_execution_plan(action_class, *args)
|
151
|
+
unless transaction_driver
|
152
|
+
raise "Bus doesn't know how to run in transaction"
|
153
|
+
end
|
154
|
+
|
155
|
+
execution_plan = nil
|
156
|
+
transaction_driver.transaction do
|
157
|
+
execution_plan = prepare_execution_plan(action_class, *args)
|
158
|
+
transaction_driver.rollback
|
159
|
+
end
|
160
|
+
return execution_plan
|
161
|
+
end
|
162
|
+
|
163
|
+
def logger
|
164
|
+
@logger ||= Dynflow::Logger.new(self.class)
|
165
|
+
end
|
166
|
+
|
55
167
|
end
|
56
168
|
end
|
data/lib/dynflow/dispatcher.rb
CHANGED
@@ -21,7 +21,7 @@ module Dynflow
|
|
21
21
|
def execution_plan_for(action, *plan_args)
|
22
22
|
ordered_actions = subscribed_actions(action).sort_by(&:name)
|
23
23
|
|
24
|
-
execution_plan =
|
24
|
+
execution_plan = ExecutionPlan.new
|
25
25
|
ordered_actions.each do |action_class|
|
26
26
|
sub_action_plan = action_class.plan(*plan_args) do |sub_action|
|
27
27
|
sub_action.input = action.input
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Dynflow
|
4
|
+
class ExecutionPlan
|
5
|
+
|
6
|
+
attr_reader :plan_steps, :run_steps, :finalize_steps
|
7
|
+
|
8
|
+
# allows storing and reloading the execution plan to something
|
9
|
+
# more persistent than memory
|
10
|
+
attr_accessor :persistence
|
11
|
+
# one of [new, running, paused, aborted, finished]
|
12
|
+
attr_accessor :status
|
13
|
+
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
def initialize(plan_steps = [], run_steps = [], finalize_steps = [])
|
17
|
+
@plan_steps = plan_steps
|
18
|
+
@run_steps = run_steps
|
19
|
+
@finalize_steps = finalize_steps
|
20
|
+
@status = 'new'
|
21
|
+
end
|
22
|
+
|
23
|
+
def steps
|
24
|
+
self.plan_steps + self.run_steps + self.finalize_steps
|
25
|
+
end
|
26
|
+
|
27
|
+
def failed_steps
|
28
|
+
self.steps.find_all { |step| step.status == 'error' }
|
29
|
+
end
|
30
|
+
|
31
|
+
def <<(action)
|
32
|
+
run_step = Step::Run.new(action)
|
33
|
+
@run_steps << run_step if action.respond_to? :run
|
34
|
+
@finalize_steps << Step::Finalize.new(run_step) if action.respond_to? :finalize
|
35
|
+
end
|
36
|
+
|
37
|
+
def concat(other)
|
38
|
+
self.plan_steps.concat(other.plan_steps)
|
39
|
+
self.run_steps.concat(other.run_steps)
|
40
|
+
self.finalize_steps.concat(other.finalize_steps)
|
41
|
+
self.status = other.status
|
42
|
+
end
|
43
|
+
|
44
|
+
# update the persistence based on the current status
|
45
|
+
def persist(include_steps = false)
|
46
|
+
if @persistence
|
47
|
+
@persistence.persist(self)
|
48
|
+
|
49
|
+
if include_steps
|
50
|
+
steps.each { |step| step.persist }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|