dynflow 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +0 -10
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +99 -37
  6. data/Rakefile +2 -6
  7. data/doc/images/logo.png +0 -0
  8. data/dynflow.gemspec +10 -1
  9. data/examples/generate_work_for_daemon.rb +24 -0
  10. data/examples/orchestrate.rb +121 -0
  11. data/examples/run_daemon.rb +17 -0
  12. data/examples/web_console.rb +29 -0
  13. data/lib/dynflow.rb +27 -6
  14. data/lib/dynflow/action.rb +185 -77
  15. data/lib/dynflow/action/cancellable_polling.rb +18 -0
  16. data/lib/dynflow/action/finalize_phase.rb +18 -0
  17. data/lib/dynflow/action/flow_phase.rb +44 -0
  18. data/lib/dynflow/action/format.rb +46 -0
  19. data/lib/dynflow/action/missing.rb +26 -0
  20. data/lib/dynflow/action/plan_phase.rb +85 -0
  21. data/lib/dynflow/action/polling.rb +49 -0
  22. data/lib/dynflow/action/presenter.rb +51 -0
  23. data/lib/dynflow/action/progress.rb +62 -0
  24. data/lib/dynflow/action/run_phase.rb +43 -0
  25. data/lib/dynflow/action/suspended.rb +21 -0
  26. data/lib/dynflow/clock.rb +133 -0
  27. data/lib/dynflow/daemon.rb +29 -0
  28. data/lib/dynflow/execution_plan.rb +285 -33
  29. data/lib/dynflow/execution_plan/dependency_graph.rb +29 -0
  30. data/lib/dynflow/execution_plan/output_reference.rb +52 -0
  31. data/lib/dynflow/execution_plan/steps.rb +12 -0
  32. data/lib/dynflow/execution_plan/steps/abstract.rb +121 -0
  33. data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +52 -0
  34. data/lib/dynflow/execution_plan/steps/error.rb +33 -0
  35. data/lib/dynflow/execution_plan/steps/finalize_step.rb +23 -0
  36. data/lib/dynflow/execution_plan/steps/plan_step.rb +81 -0
  37. data/lib/dynflow/execution_plan/steps/run_step.rb +21 -0
  38. data/lib/dynflow/executors.rb +9 -0
  39. data/lib/dynflow/executors/abstract.rb +32 -0
  40. data/lib/dynflow/executors/parallel.rb +88 -0
  41. data/lib/dynflow/executors/parallel/core.rb +119 -0
  42. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +120 -0
  43. data/lib/dynflow/executors/parallel/flow_manager.rb +48 -0
  44. data/lib/dynflow/executors/parallel/pool.rb +102 -0
  45. data/lib/dynflow/executors/parallel/running_steps_manager.rb +63 -0
  46. data/lib/dynflow/executors/parallel/sequence_cursor.rb +97 -0
  47. data/lib/dynflow/executors/parallel/sequential_manager.rb +81 -0
  48. data/lib/dynflow/executors/parallel/work_queue.rb +44 -0
  49. data/lib/dynflow/executors/parallel/worker.rb +30 -0
  50. data/lib/dynflow/executors/remote_via_socket.rb +38 -0
  51. data/lib/dynflow/executors/remote_via_socket/core.rb +150 -0
  52. data/lib/dynflow/flows.rb +13 -0
  53. data/lib/dynflow/flows/abstract.rb +36 -0
  54. data/lib/dynflow/flows/abstract_composed.rb +104 -0
  55. data/lib/dynflow/flows/atom.rb +36 -0
  56. data/lib/dynflow/flows/concurrence.rb +28 -0
  57. data/lib/dynflow/flows/sequence.rb +13 -0
  58. data/lib/dynflow/future.rb +173 -0
  59. data/lib/dynflow/listeners.rb +7 -0
  60. data/lib/dynflow/listeners/abstract.rb +13 -0
  61. data/lib/dynflow/listeners/serialization.rb +41 -0
  62. data/lib/dynflow/listeners/socket.rb +88 -0
  63. data/lib/dynflow/logger_adapters.rb +8 -0
  64. data/lib/dynflow/logger_adapters/abstract.rb +30 -0
  65. data/lib/dynflow/logger_adapters/delegator.rb +13 -0
  66. data/lib/dynflow/logger_adapters/formatters.rb +8 -0
  67. data/lib/dynflow/logger_adapters/formatters/abstract.rb +33 -0
  68. data/lib/dynflow/logger_adapters/formatters/exception.rb +15 -0
  69. data/lib/dynflow/logger_adapters/simple.rb +59 -0
  70. data/lib/dynflow/micro_actor.rb +102 -0
  71. data/lib/dynflow/persistence.rb +53 -0
  72. data/lib/dynflow/persistence_adapters.rb +6 -0
  73. data/lib/dynflow/persistence_adapters/abstract.rb +56 -0
  74. data/lib/dynflow/persistence_adapters/sequel.rb +160 -0
  75. data/lib/dynflow/persistence_adapters/sequel_migrations/001_initial.rb +52 -0
  76. data/lib/dynflow/serializable.rb +66 -0
  77. data/lib/dynflow/simple_world.rb +18 -0
  78. data/lib/dynflow/stateful.rb +40 -0
  79. data/lib/dynflow/testing.rb +32 -0
  80. data/lib/dynflow/testing/assertions.rb +64 -0
  81. data/lib/dynflow/testing/dummy_execution_plan.rb +40 -0
  82. data/lib/dynflow/testing/dummy_executor.rb +29 -0
  83. data/lib/dynflow/testing/dummy_planned_action.rb +18 -0
  84. data/lib/dynflow/testing/dummy_step.rb +19 -0
  85. data/lib/dynflow/testing/dummy_world.rb +33 -0
  86. data/lib/dynflow/testing/factories.rb +83 -0
  87. data/lib/dynflow/testing/managed_clock.rb +23 -0
  88. data/lib/dynflow/testing/mimic.rb +38 -0
  89. data/lib/dynflow/transaction_adapters.rb +9 -0
  90. data/lib/dynflow/transaction_adapters/abstract.rb +26 -0
  91. data/lib/dynflow/transaction_adapters/active_record.rb +27 -0
  92. data/lib/dynflow/transaction_adapters/none.rb +12 -0
  93. data/lib/dynflow/version.rb +1 -1
  94. data/lib/dynflow/web_console.rb +277 -0
  95. data/lib/dynflow/world.rb +168 -0
  96. data/test/action_test.rb +89 -11
  97. data/test/clock_test.rb +59 -0
  98. data/test/code_workflow_example.rb +382 -0
  99. data/test/execution_plan_test.rb +195 -64
  100. data/test/executor_test.rb +692 -0
  101. data/test/persistance_adapters_test.rb +173 -0
  102. data/test/test_helper.rb +316 -1
  103. data/test/testing_test.rb +148 -0
  104. data/test/web_console_test.rb +38 -0
  105. data/web/assets/javascripts/application.js +25 -0
  106. data/web/assets/stylesheets/application.css +101 -0
  107. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +1109 -0
  108. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +9 -0
  109. data/web/assets/vendor/bootstrap/css/bootstrap.css +6167 -0
  110. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -0
  111. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  112. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
  113. data/web/assets/vendor/bootstrap/js/bootstrap.js +2280 -0
  114. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +6 -0
  115. data/web/assets/vendor/google-code-prettify/lang-basic.js +3 -0
  116. data/web/assets/vendor/google-code-prettify/prettify.css +1 -0
  117. data/web/assets/vendor/google-code-prettify/prettify.js +30 -0
  118. data/web/assets/vendor/google-code-prettify/run_prettify.js +34 -0
  119. data/web/assets/vendor/jquery/jquery.js +9807 -0
  120. data/web/views/flow.erb +19 -0
  121. data/web/views/flow_step.erb +31 -0
  122. data/web/views/index.erb +39 -0
  123. data/web/views/layout.erb +20 -0
  124. data/web/views/plan_step.erb +11 -0
  125. data/web/views/show.erb +54 -0
  126. metadata +250 -11
  127. data/examples/events.rb +0 -71
  128. data/examples/workflow.rb +0 -140
  129. data/lib/dynflow/bus.rb +0 -168
  130. data/lib/dynflow/dispatcher.rb +0 -36
  131. data/lib/dynflow/logger.rb +0 -34
  132. data/lib/dynflow/step.rb +0 -234
  133. data/test/bus_test.rb +0 -150
@@ -0,0 +1,29 @@
1
+ # Demo for Dynflow web console
2
+ # usage: ruby web_console.rb
3
+
4
+ $:.unshift(File.expand_path('../../lib', __FILE__))
5
+
6
+ require 'dynflow'
7
+ require_relative 'orchestrate'
8
+
9
+ world = Dynflow::SimpleWorld.new
10
+
11
+ require 'dynflow/web_console'
12
+ dynflow_console = Dynflow::WebConsole.setup do
13
+ set :world, world
14
+ end
15
+
16
+ 3.times do
17
+ Thread.new do
18
+ 3.times do
19
+ world.trigger(Orchestrate::CreateInfrastructure)
20
+ end
21
+ end
22
+ end
23
+
24
+ puts <<MESSAGE
25
+ =============================================
26
+ See the console at http://localhost:4567/
27
+ =============================================
28
+ MESSAGE
29
+ dynflow_console.run!
data/lib/dynflow.rb CHANGED
@@ -1,11 +1,32 @@
1
+ require 'apipie-params'
2
+ require 'algebrick'
3
+ require 'thread'
4
+ require 'set'
1
5
  require 'active_support/core_ext/hash/indifferent_access'
2
- require 'dynflow/logger'
3
- require 'dynflow/execution_plan'
4
- require 'dynflow/dispatcher'
5
- require 'dynflow/bus'
6
- require 'dynflow/step'
7
- require 'dynflow/action'
8
6
 
7
+ # TODO validate in/output, also validate unknown keys
8
+ # TODO performance testing, how many actions will it handle?
9
+ # TODO profiling, find bottlenecks
9
10
  module Dynflow
10
11
 
12
+ class Error < StandardError
13
+ end
14
+
15
+ require 'dynflow/future'
16
+ require 'dynflow/micro_actor'
17
+ require 'dynflow/serializable'
18
+ require 'dynflow/clock'
19
+ require 'dynflow/stateful'
20
+ require 'dynflow/transaction_adapters'
21
+ require 'dynflow/persistence'
22
+ require 'dynflow/action'
23
+ require 'dynflow/flows'
24
+ require 'dynflow/execution_plan'
25
+ require 'dynflow/listeners'
26
+ require 'dynflow/executors'
27
+ require 'dynflow/logger_adapters'
28
+ require 'dynflow/world'
29
+ require 'dynflow/simple_world'
30
+ require 'dynflow/daemon'
31
+
11
32
  end
@@ -1,130 +1,238 @@
1
+ require 'active_support/inflector'
2
+
1
3
  module Dynflow
2
- class Action
3
4
 
4
- # only for the planning phase: flag indicating that the action
5
- # was triggered from subscription. If so, the implicit plan
6
- # method uses the input of the parent action. Otherwise, the
7
- # argument the plan_action is used as default.
8
- attr_accessor :execution_plan, :from_subscription, :input, :output
5
+ # TODO unify phases into one class, check what can be called in what phase at runtime
6
+ class Action < Serializable
7
+ include Algebrick::TypeCheck
8
+ include Algebrick::Matching
9
9
 
10
- def self.inherited(child)
11
- self.actions << child
10
+ require 'dynflow/action/format'
11
+ extend Format
12
+
13
+ require 'dynflow/action/progress'
14
+ include Progress
15
+
16
+ require 'dynflow/action/suspended'
17
+ require 'dynflow/action/missing'
18
+
19
+ require 'dynflow/action/plan_phase'
20
+ require 'dynflow/action/flow_phase'
21
+ require 'dynflow/action/run_phase'
22
+ require 'dynflow/action/finalize_phase'
23
+
24
+ require 'dynflow/action/presenter'
25
+ require 'dynflow/action/polling'
26
+ require 'dynflow/action/cancellable_polling'
27
+
28
+ # Override this to extend the phase classes
29
+ def self.phase_modules
30
+ { plan_phase: [PlanPhase],
31
+ run_phase: [RunPhase],
32
+ finalize_phase: [FinalizePhase],
33
+ presenter: [Presenter] }.freeze
34
+ end
35
+
36
+ phase_modules.each do |phase_name, _|
37
+ define_singleton_method phase_name do
38
+ instance_variable_get :"@#{phase_name}" or
39
+ instance_variable_set :"@#{phase_name}", __send__("create_#{phase_name}")
40
+ end
41
+
42
+ define_singleton_method "create_#{phase_name}" do
43
+ generate_phase(*phase_modules[phase_name])
44
+ end
12
45
  end
13
46
 
14
- def self.actions
15
- @actions ||= []
47
+ def self.generate_phase(*modules)
48
+ Class.new(self) { modules.each { |m| include m } }
16
49
  end
17
50
 
51
+ def self.phase?
52
+ [PlanPhase, RunPhase, FinalizePhase, Presenter].any? { |phase| self < phase }
53
+ end
54
+
55
+ def self.all_children
56
+ #noinspection RubyArgCount
57
+ children.
58
+ inject(children) { |children, child| children + child.all_children }.
59
+ select { |ch| !ch.phase? }
60
+ end
61
+
62
+ # FIND define subscriptions in world independent on action's classes,
63
+ # limited only by in/output formats
64
+ # @return [nil, Class] a child of Action
18
65
  def self.subscribe
19
66
  nil
20
67
  end
21
68
 
22
- def self.require
23
- nil
69
+ def self.attr_indifferent_access_hash(*names)
70
+ attr_reader(*names)
71
+ names.each do |name|
72
+ define_method("#{name}=") { |v| indifferent_access_hash_variable_set name, v }
73
+ end
24
74
  end
25
75
 
26
- def initialize(input, output = nil)
27
- # for preparation phase
28
- @execution_plan = ExecutionPlan.new
76
+ def indifferent_access_hash_variable_set(name, value)
77
+ Type! value, Hash
78
+ instance_variable_set :"@#{name}", value.with_indifferent_access
79
+ end
29
80
 
30
- @input = input
31
- @output = output || {}
81
+ def self.from_hash(hash, phase, *args)
82
+ check_class_key_present hash
83
+ raise ArgumentError, "unknown phase '#{phase}'" unless [:plan_phase, :run_phase, :finalize_phase].include? phase
84
+ Action.constantize(hash[:class]).send(phase).new_from_hash(hash, *args)
32
85
  end
33
86
 
87
+ attr_reader :world, :execution_plan_id, :id, :plan_step_id, :run_step_id, :finalize_step_id
34
88
 
35
- def ==(other)
36
- [self.class.name, self.input, self.output] ==
37
- [other.class.name, other.input, other.output]
38
- end
89
+ def initialize(attributes, world)
90
+ raise "It's not expected to initialize this class directly, use phases." unless self.class.phase?
91
+
92
+ Type! attributes, Hash
39
93
 
40
- def inspect
41
- "#{self.class.name}: #{input.inspect} ~> #{output.inspect}"
94
+ @world = Type! world, World
95
+ @step = Type! attributes[:step], ExecutionPlan::Steps::Abstract
96
+ @execution_plan_id = attributes[:execution_plan_id] || raise(ArgumentError, 'missing execution_plan_id')
97
+ @id = attributes[:id] || raise(ArgumentError, 'missing id')
98
+ @plan_step_id = attributes[:plan_step_id]
99
+ @run_step_id = attributes[:run_step_id]
100
+ @finalize_step_id = attributes[:finalize_step_id]
42
101
  end
43
102
 
44
- # the block contains the expression in Apipie::Params::DSL
45
- # describing the format of message
46
- def self.input_format(&block)
47
- if block
48
- @input_format_block = block
49
- elsif @input_format_block
50
- @input_format ||= Apipie::Params::Description.define(&@input_format_block)
103
+ def self.action_class
104
+ # superclass because we run this from the phases of action class
105
+ if phase?
106
+ superclass
51
107
  else
52
- nil
108
+ self
53
109
  end
54
110
  end
55
111
 
56
- # the block contains the expression in Apipie::Params::DSL
57
- # describing the format of message
58
- def self.output_format(&block)
59
- if block
60
- @output_format_block = block
61
- elsif @output_format_block
62
- @output_format ||= Apipie::Params::Description.define(&@output_format_block)
63
- else
64
- nil
65
- end
112
+ def self.constantize(action_name)
113
+ action_name.constantize
114
+ rescue NameError
115
+ Action::Missing.generate(action_name)
66
116
  end
67
117
 
68
- def self.trigger(*args)
69
- Dynflow::Bus.trigger(self, *args)
118
+ def action_logger
119
+ world.action_logger
70
120
  end
71
121
 
72
- def self.plan(*args)
73
- action = self.new({})
74
- yield action if block_given?
122
+ def action_class
123
+ self.class.action_class
124
+ end
75
125
 
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
126
+ def to_hash
127
+ recursive_to_hash class: action_class.name,
128
+ execution_plan_id: execution_plan_id,
129
+ id: id,
130
+ plan_step_id: plan_step_id,
131
+ run_step_id: run_step_id,
132
+ finalize_step_id: finalize_step_id
133
+ end
81
134
 
82
- if action.execution_plan.failed_steps.any?
83
- action.execution_plan.status = 'error'
84
- else
85
- action.add_subscriptions(*args)
86
- end
135
+ # @api private
136
+ # @return [Array<Fixnum>] - ids of steps referenced from action
137
+ def required_step_ids(value = self.input)
138
+ ret = case value
139
+ when Hash
140
+ value.values.map { |val| required_step_ids(val) }
141
+ when Array
142
+ value.map { |val| required_step_ids(val) }
143
+ when ExecutionPlan::OutputReference
144
+ value.step_id
145
+ else
146
+ # no reference hidden in this arg
147
+ end
148
+ return Array(ret).flatten.compact
149
+ end
87
150
 
88
- action.execution_plan
151
+ def state
152
+ @step.state
89
153
  end
90
154
 
91
- # for subscribed actions: by default take the input of the
92
- # subscribed action
155
+ def error
156
+ @step.error
157
+ end
158
+
159
+ protected
160
+
161
+ def state=(state)
162
+ @world.logger.debug "step #{execution_plan_id}:#{@step.id} #{self.state} >> #{state}"
163
+ @step.state = state
164
+ end
165
+
166
+ def save_state
167
+ @step.save
168
+ end
169
+
170
+ # @override
93
171
  def plan(*args)
94
- if from_subscription
172
+ if trigger
95
173
  # if the action is triggered by subscription, by default use the
96
- # input of parent action
97
- plan_self(self.input)
174
+ # input of parent action.
175
+ # should be replaced by referencing the input from input format
176
+ plan_self(input.merge(trigger.input))
98
177
  else
99
178
  # in this case, the action was triggered by plan_action. Use
100
179
  # the argument specified there.
101
- plan_self(args.first)
180
+ plan_self(*args)
102
181
  end
182
+ self
103
183
  end
104
184
 
105
- def plan_self(input)
106
- self.input = input
107
- @execution_plan << self
185
+ def self.new_from_hash(hash, world)
186
+ new(hash, world)
108
187
  end
109
188
 
110
- def plan_action(action_class, *args)
111
- sub_action_plan = action_class.plan(*args) do |action|
112
- action.input = self.input
113
- end
114
- @execution_plan.concat(sub_action_plan)
189
+ private
190
+
191
+ ERRORING = Object.new
192
+
193
+ # DSL to terminate action execution and set it to error
194
+ def error!(error)
195
+ set_error(error)
196
+ throw ERRORING
115
197
  end
116
198
 
117
- def add_subscriptions(*plan_args)
118
- @execution_plan.concat(Dispatcher.execution_plan_for(self, *plan_args))
199
+ def with_error_handling(&block)
200
+ raise "wrong state #{self.state}" unless self.state == :running
201
+
202
+ begin
203
+ catch(ERRORING) { block.call }
204
+ rescue Exception => error
205
+ set_error(error)
206
+ # reraise low-level exceptions
207
+ raise error unless Type? error, StandardError, ScriptError
208
+ end
209
+
210
+ case self.state
211
+ when :running
212
+ self.state = :success
213
+ when :suspended, :error
214
+ else
215
+ raise "wrong state #{self.state}"
216
+ end
119
217
  end
120
218
 
121
- def execution_plan
122
- @execution_plan
219
+ def set_error(error)
220
+ Type! error, Exception, String
221
+ action_logger.error error
222
+ self.state = :error
223
+ @step.error = if error.is_a?(String)
224
+ ExecutionPlan::Steps::Error.new(nil, error, nil)
225
+ else
226
+ ExecutionPlan::Steps::Error.new(error.class.name, error.message, error.backtrace)
227
+ end
123
228
  end
124
229
 
125
- def validate!
126
- self.clss.output_format.validate!(output)
230
+ def self.inherited(child)
231
+ children << child
127
232
  end
128
233
 
234
+ def self.children
235
+ @children ||= []
236
+ end
129
237
  end
130
238
  end
@@ -0,0 +1,18 @@
1
+ module Dynflow
2
+ module Action::CancellablePolling
3
+ include Action::Polling
4
+ Cancel = Algebrick.atom
5
+
6
+ def run(event = nil)
7
+ if Cancel === event
8
+ self.external_task = cancel_external_task
9
+ else
10
+ super event
11
+ end
12
+ end
13
+
14
+ def cancel_external_task
15
+ NotImplementedError
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module Dynflow
2
+ module Action::FinalizePhase
3
+
4
+ def self.included(base)
5
+ base.send(:include, Action::FlowPhase)
6
+ base.send(:attr_reader, :output)
7
+ end
8
+
9
+ def execute
10
+ self.state = :running
11
+ save_state
12
+ with_error_handling do
13
+ finalize
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ module Dynflow
2
+ module Action::FlowPhase
3
+
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ base.send(:attr_reader, :input)
7
+ end
8
+
9
+ def initialize(attributes, world)
10
+ super attributes, world
11
+
12
+ indifferent_access_hash_variable_set :input, deserialize_references(attributes[:input])
13
+ indifferent_access_hash_variable_set :output, attributes[:output] || {}
14
+ end
15
+
16
+ def to_hash
17
+ super.merge recursive_to_hash(input: input,
18
+ output: output)
19
+ end
20
+
21
+ def deserialize_references(value)
22
+ case value
23
+ when Hash
24
+ if value[:class] == "Dynflow::ExecutionPlan::OutputReference"
25
+ ExecutionPlan::OutputReference.new_from_hash(value)
26
+ else
27
+ value.reduce(HashWithIndifferentAccess.new) do |h, (key, val)|
28
+ h.update(key => deserialize_references(val))
29
+ end
30
+ end
31
+ when Array
32
+ value.map { |val| deserialize_references(val) }
33
+ else
34
+ value
35
+ end
36
+ end
37
+
38
+ module ClassMethods
39
+ def new_from_hash(hash, step, world)
40
+ new(hash.merge(step: step), world)
41
+ end
42
+ end
43
+ end
44
+ end