dynflow 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,9 +1,17 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'multi_json'
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
- # [Publish, {"title"=>"Short", "body"=>"Short"}],
122
- # [Review, {"title"=>"Short", "body"=>"Short"}],
123
- # [Print, {"title"=>"Short", "body"=>"Short", "color"=>false}]
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/**/*_test.rb']
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/eventum"
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
- # [Click, {"x"=>5, "y"=>4}],
68
- # [SayHello, {"x"=>5, "y"=>4}],
69
- # [SayPosition, {"x"=>5, "y"=>4}],
70
- # [SayGoodbye, {"x"=>5, "y"=>4}]
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
- # [Publish, {"title"=>"Short", "body"=>"Short"}],
111
- # [Review, {"title"=>"Short", "body"=>"Short"}],
112
- # [Print, {"title"=>"Short", "body"=>"Short", "color"=>false}]
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/message'
3
+ require 'dynflow/execution_plan'
3
4
  require 'dynflow/dispatcher'
4
5
  require 'dynflow/bus'
5
- require 'dynflow/orch_request'
6
- require 'dynflow/orch_response'
6
+ require 'dynflow/step'
7
7
  require 'dynflow/action'
8
8
 
9
9
  module Dynflow
@@ -1,11 +1,11 @@
1
1
  module Dynflow
2
- class Action < Message
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
- output ||= {}
31
- super('input' => input, 'output' => output)
30
+ @input = input
31
+ @output = output || {}
32
32
  end
33
33
 
34
- def input
35
- @data['input']
36
- end
37
34
 
38
- def input=(input)
39
- @data['input'] = input
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 output
43
- @data['output']
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.plan(*args))
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
- action.plan(*args)
78
- action.add_subscriptions(*args)
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 << [self.class, input]
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!(@data['output'])
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, :wait_for, :process, :trigger, :finalize
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::MemoryBus.new
13
+ @impl ||= Bus.new
13
14
  end
14
15
  attr_writer :impl
15
16
  end
16
17
 
17
- def finalize(outputs)
18
- outputs.each do |action|
19
- if action.respond_to?(:finalize)
20
- action.finalize(outputs)
21
- end
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 process(action_class, input, output = nil)
26
- # TODO: here goes the message validation
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
- def wait_for(*args)
33
- raise NotImplementedError, 'Abstract method'
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
- def logger
37
- @logger ||= Dynflow::Logger.new(self.class)
48
+ alias_method :resume, :execute
49
+
50
+ def skip(step)
51
+ step.status = 'skipped'
52
+ step.persist
38
53
  end
39
54
 
40
- class MemoryBus < Bus
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
- def initialize
43
- super
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
- def trigger(execution_plan)
47
- outputs = []
48
- execution_plan.each do |(action_class, input)|
49
- outputs << self.process(action_class, input)
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
- self.finalize(outputs)
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
@@ -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