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