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
+ module Dynflow
2
+ class Daemon
3
+ include Algebrick::TypeCheck
4
+
5
+ def initialize(listener, world, lock_file = nil)
6
+ @listener = Type! listener, Listeners::Abstract
7
+ @world = Type! world, World
8
+ @lock_file = Type! lock_file, String, NilClass
9
+ end
10
+
11
+ def run
12
+ with_lock_file do
13
+ terminated = Future.new
14
+ trap('SIGINT') { @world.terminate terminated }
15
+ terminated.wait
16
+ end
17
+ end
18
+
19
+ def with_lock_file(&block)
20
+ if @lock_file
21
+ raise "Lockfile #{@lock_file} is already present." if File.exist?(@lock_file)
22
+ File.write(@lock_file, "Locked at #{Time.now} by process #{$$}\n")
23
+ end
24
+ block.call
25
+ ensure
26
+ File.delete(@lock_file) if @lock_file
27
+ end
28
+ end
29
+ end
@@ -1,56 +1,308 @@
1
- require 'forwardable'
1
+ require 'uuidtools'
2
2
 
3
3
  module Dynflow
4
- class ExecutionPlan
5
4
 
6
- attr_reader :plan_steps, :run_steps, :finalize_steps
5
+ # TODO extract planning logic to an extra class ExecutionPlanner
6
+ class ExecutionPlan < Serializable
7
+ include Algebrick::TypeCheck
8
+ include Stateful
7
9
 
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
10
+ require 'dynflow/execution_plan/steps'
11
+ require 'dynflow/execution_plan/output_reference'
12
+ require 'dynflow/execution_plan/dependency_graph'
13
13
 
14
- extend Forwardable
14
+ attr_reader :id, :world, :root_plan_step, :steps, :run_flow, :finalize_flow,
15
+ :started_at, :ended_at, :execution_time, :real_time
15
16
 
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'
17
+ def self.states
18
+ @states ||= [:pending, :planning, :planned, :running, :paused, :stopped]
21
19
  end
22
20
 
23
- def steps
24
- self.plan_steps + self.run_steps + self.finalize_steps
21
+ def self.state_transitions
22
+ @state_transitions ||= { pending: [:planning],
23
+ planning: [:planned, :stopped],
24
+ planned: [:running],
25
+ running: [:paused, :stopped],
26
+ paused: [:running],
27
+ stopped: [] }
25
28
  end
26
29
 
27
- def failed_steps
28
- self.steps.find_all { |step| step.status == 'error' }
30
+ # all params with default values are part of *private* api
31
+ def initialize(world,
32
+ id = UUIDTools::UUID.random_create.to_s,
33
+ state = :pending,
34
+ root_plan_step = nil,
35
+ run_flow = Flows::Concurrence.new([]),
36
+ finalize_flow = Flows::Sequence.new([]),
37
+ steps = {},
38
+ started_at = nil,
39
+ ended_at = nil,
40
+ execution_time = 0.0,
41
+ real_time = 0.0)
42
+
43
+ @id = Type! id, String
44
+ @world = Type! world, World
45
+ self.state = state
46
+ @run_flow = Type! run_flow, Flows::Abstract
47
+ @finalize_flow = Type! finalize_flow, Flows::Abstract
48
+ @root_plan_step = root_plan_step
49
+ @started_at = Type! started_at, Time, NilClass
50
+ @ended_at = Type! ended_at, Time, NilClass
51
+ @execution_time = Type! execution_time, Float
52
+ @real_time = Type! real_time, Float
53
+
54
+ steps.all? do |k, v|
55
+ Type! k, Integer
56
+ Type! v, Steps::Abstract
57
+ end
58
+ @steps = steps
59
+ end
60
+
61
+ def logger
62
+ @world.logger
63
+ end
64
+
65
+ def update_state(state)
66
+ original = self.state
67
+ case self.state = state
68
+ when :planning
69
+ @started_at = Time.now
70
+ when :stopped
71
+ @ended_at = Time.now
72
+ @real_time = @ended_at - @started_at
73
+ else
74
+ # ignore
75
+ end
76
+ logger.debug "execution plan #{id} #{original} >> #{state}"
77
+ self.save
78
+ end
79
+
80
+ def update_execution_time(execution_time)
81
+ @execution_time += execution_time
82
+ end
83
+
84
+ def result
85
+ all_steps = steps.values
86
+ if all_steps.any? { |step| step.state == :error }
87
+ return :error
88
+ elsif all_steps.all? { |step| [:success, :skipped].include?(step.state) }
89
+ return :success
90
+ else
91
+ return :pending
92
+ end
93
+ end
94
+
95
+ def error?
96
+ result == :error
97
+ end
98
+
99
+ def generate_action_id
100
+ @last_action_id ||= 0
101
+ @last_action_id += 1
29
102
  end
30
103
 
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
104
+ def generate_step_id
105
+ @last_step_id ||= 0
106
+ @last_step_id += 1
35
107
  end
36
108
 
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
109
+ def prepare(action_class)
110
+ save
111
+ @root_plan_step = add_step(Steps::PlanStep, action_class, generate_action_id)
42
112
  end
43
113
 
44
- # update the persistence based on the current status
45
- def persist(include_steps = false)
46
- if @persistence
47
- @persistence.persist(self)
114
+ def plan(*args)
115
+ update_state(:planning)
116
+ world.transaction_adapter.transaction do
117
+ with_planning_scope do
118
+ root_plan_step.execute(self, nil, *args)
48
119
 
49
- if include_steps
50
- steps.each { |step| step.persist }
120
+ if @dependency_graph.unresolved?
121
+ raise "Some dependencies were not resolved: #{@dependency_graph.inspect}"
122
+ end
51
123
  end
124
+
125
+ if @run_flow.size == 1
126
+ @run_flow = @run_flow.sub_flows.first
127
+ end
128
+
129
+ world.transaction_adapter.rollback if error?
130
+ end
131
+ steps.values.each(&:save)
132
+ update_state(error? ? :stopped : :planned)
133
+ end
134
+
135
+ def skip(step)
136
+ raise "plan step can't be skipped" if step.is_a? Steps::PlanStep
137
+ steps_to_skip = steps_to_skip(step).each do |s|
138
+ s.state = :skipped
139
+ s.save
140
+ end
141
+ self.save
142
+ return steps_to_skip
143
+ end
144
+
145
+ # All the steps that need to get skipped when wanting to skip the step
146
+ # includes the step itself, all steps dependent on it (even transitively)
147
+ # FIND maybe move to persistence to let adapter to do it effectively?
148
+ # @return [Array<Steps::Abstract>]
149
+ def steps_to_skip(step)
150
+ dependent_steps = @steps.values.find_all do |s|
151
+ next if s.is_a? Steps::PlanStep
152
+ action = persistence.load_action(s)
153
+ action.required_step_ids.include?(step.id)
154
+ end
155
+
156
+ steps_to_skip = dependent_steps.map do |dependent_step|
157
+ steps_to_skip(dependent_step)
158
+ end.flatten
159
+
160
+ steps_to_skip << step
161
+
162
+ if step.is_a? Steps::RunStep
163
+ finalize_step_id = persistence.load_action(step).finalize_step_id
164
+ steps_to_skip << steps[finalize_step_id] if finalize_step_id
165
+ end
166
+
167
+ return steps_to_skip.uniq
168
+ end
169
+
170
+ # @api private
171
+ def current_run_flow
172
+ @run_flow_stack.last
173
+ end
174
+
175
+ # @api private
176
+ def with_planning_scope(&block)
177
+ @run_flow_stack = []
178
+ @dependency_graph = DependencyGraph.new
179
+ switch_flow(run_flow, &block)
180
+ ensure
181
+ @run_flow_stack = nil
182
+ @dependency_graph = nil
183
+ end
184
+
185
+ # @api private
186
+ # Switches the flow type (Sequence, Concurrence) to be used within the block.
187
+ def switch_flow(new_flow, &block)
188
+ @run_flow_stack << new_flow
189
+ return block.call
190
+ ensure
191
+ @run_flow_stack.pop
192
+ current_run_flow.add_and_resolve(@dependency_graph, new_flow) if current_run_flow
193
+ end
194
+
195
+ def add_plan_step(action_class, planned_by)
196
+ add_step(Steps::PlanStep, action_class, generate_action_id, planned_by.plan_step_id)
197
+ end
198
+
199
+ def add_run_step(action)
200
+ add_step(Steps::RunStep, action.action_class, action.id).tap do |step|
201
+ @dependency_graph.add_dependencies(step, action)
202
+ current_run_flow.add_and_resolve(@dependency_graph, Flows::Atom.new(step.id))
203
+ end
204
+ end
205
+
206
+ def add_finalize_step(action)
207
+ add_step(Steps::FinalizeStep, action.action_class, action.id).tap do |step|
208
+ finalize_flow << Flows::Atom.new(step.id)
209
+ end
210
+ end
211
+
212
+ def to_hash
213
+ recursive_to_hash id: self.id,
214
+ class: self.class.to_s,
215
+ state: self.state,
216
+ result: result,
217
+ root_plan_step_id: root_plan_step && root_plan_step.id,
218
+ run_flow: run_flow,
219
+ finalize_flow: finalize_flow,
220
+ step_ids: steps.map { |id, _| id },
221
+ started_at: time_to_str(started_at),
222
+ ended_at: time_to_str(ended_at),
223
+ execution_time: execution_time,
224
+ real_time: real_time
225
+ end
226
+
227
+ def save
228
+ persistence.save_execution_plan(self)
229
+ end
230
+
231
+ def self.new_from_hash(hash, world)
232
+ check_class_matching hash
233
+ execution_plan_id = hash[:id]
234
+ steps = steps_from_hash(hash[:step_ids], execution_plan_id, world)
235
+ self.new(world,
236
+ execution_plan_id,
237
+ hash[:state],
238
+ steps[hash[:root_plan_step_id]],
239
+ Flows::Abstract.from_hash(hash[:run_flow]),
240
+ Flows::Abstract.from_hash(hash[:finalize_flow]),
241
+ steps,
242
+ string_to_time(hash[:started_at]),
243
+ string_to_time(hash[:ended_at]),
244
+ hash[:execution_time],
245
+ hash[:real_time])
246
+ end
247
+
248
+ # @return [0..1] the percentage of the progress. See Action::Progress for more
249
+ # info
250
+ def progress
251
+ flow_step_ids = run_flow.all_step_ids + finalize_flow.all_step_ids
252
+ plan_done, plan_total = flow_step_ids.reduce([0.0, 0]) do |(done, total), step_id|
253
+ step_progress_done, step_progress_weight = self.steps[step_id].progress
254
+ [done + (step_progress_done * step_progress_weight),
255
+ total + step_progress_weight]
256
+ end
257
+ plan_total > 0 ? (plan_done / plan_total) : 1
258
+ end
259
+
260
+ # This method can be used to access result of the whole execution plan and detailed
261
+ # progress.
262
+ # @return [Array<Action::Presenter>] presenter of the actions
263
+ # involved in the plan
264
+ def actions
265
+ action_steps = Hash.new { |h, k| h[k] = [] }
266
+ all_actions = []
267
+ steps.values.each do |step|
268
+ action_steps[step.action_id] << step
269
+ end
270
+ action_steps.each do |action_id, involved_steps|
271
+ action = Action::Presenter.load(self,
272
+ action_id,
273
+ involved_steps,
274
+ all_actions)
275
+ all_actions << action
276
+ end
277
+ return all_actions
278
+ end
279
+
280
+ private
281
+
282
+ def persistence
283
+ world.persistence
284
+ end
285
+
286
+ def add_step(step_class, action_class, action_id, planned_by_step_id = nil)
287
+ step_class.new(self.id,
288
+ self.generate_step_id,
289
+ :pending,
290
+ action_class,
291
+ action_id,
292
+ nil,
293
+ world).tap do |new_step|
294
+ @steps[new_step.id] = new_step
295
+ @steps[planned_by_step_id].children << new_step.id if planned_by_step_id
296
+ end
297
+ end
298
+
299
+ def self.steps_from_hash(step_ids, execution_plan_id, world)
300
+ step_ids.inject({}) do |hash, step_id|
301
+ step = world.persistence.load_step(execution_plan_id, step_id, world)
302
+ hash.update(step_id.to_i => step)
52
303
  end
53
304
  end
54
305
 
306
+ private_class_method :steps_from_hash
55
307
  end
56
308
  end
@@ -0,0 +1,29 @@
1
+ module Dynflow
2
+ class ExecutionPlan::DependencyGraph
3
+
4
+ def initialize
5
+ @graph = Hash.new { |h, k| h[k] = Set.new }
6
+ end
7
+
8
+ # adds dependencies to graph that +step+ has based
9
+ # on the steps referenced in its +input+
10
+ def add_dependencies(step, action)
11
+ action.required_step_ids.each do |required_step_id|
12
+ @graph[step.id] << required_step_id
13
+ end
14
+ end
15
+
16
+ def required_step_ids(step_id)
17
+ @graph[step_id]
18
+ end
19
+
20
+ def mark_satisfied(step_id, required_step_id)
21
+ @graph[step_id].delete(required_step_id)
22
+ end
23
+
24
+ def unresolved?
25
+ @graph.any? { |step_id, required_step_ids| required_step_ids.any? }
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,52 @@
1
+ module Dynflow
2
+ class ExecutionPlan::OutputReference < Serializable
3
+
4
+ attr_reader :step_id, :action_id, :subkeys
5
+
6
+ def initialize(step_id, action_id, subkeys = [])
7
+ @step_id = step_id
8
+ @action_id = action_id
9
+ @subkeys = subkeys
10
+ end
11
+
12
+ def [](subkey)
13
+ return self.class.new(step_id, action_id, subkeys.dup << subkey)
14
+ end
15
+
16
+ def to_hash
17
+ recursive_to_hash class: self.class.to_s,
18
+ step_id: step_id,
19
+ action_id: action_id,
20
+ subkeys: subkeys
21
+ end
22
+
23
+ def to_s
24
+ "Step(#{@step_id}).output".tap do |ret|
25
+ ret << @subkeys.map { |k| "[:#{k}]" }.join('') if @subkeys.any?
26
+ end
27
+ end
28
+
29
+ alias_method :inspect, :to_s
30
+
31
+ def dereference(persistence, execution_plan_id)
32
+ action_data = persistence.adapter.load_action(execution_plan_id, action_id)
33
+ deref = action_data[:output]
34
+ @subkeys.each do |subkey|
35
+ if deref.respond_to?(:[])
36
+ deref = deref[subkey]
37
+ else
38
+ raise "We were not able to dereference subkey #{@subkeys} from #{self.inspect}"
39
+ end
40
+ end
41
+ return deref
42
+ end
43
+
44
+ protected
45
+
46
+ def self.new_from_hash(hash)
47
+ check_class_matching hash
48
+ new(hash[:step_id], hash[:action_id], hash[:subkeys])
49
+ end
50
+
51
+ end
52
+ end