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,119 @@
1
+ module Dynflow
2
+ module Executors
3
+ class Parallel < Abstract
4
+
5
+ # TODO add dynflow error handling to avoid getting stuck and report errors to the future
6
+ class Core < MicroActor
7
+ def initialize(world, pool_size)
8
+ super(world.logger, world, pool_size)
9
+ end
10
+
11
+ private
12
+
13
+ def delayed_initialize(world, pool_size)
14
+ @world = Type! world, World
15
+ @pool = Pool.new(self, pool_size, world.transaction_adapter)
16
+ @execution_plan_managers = {}
17
+ end
18
+
19
+ def on_message(message)
20
+ match message,
21
+ (on ~Execution do |(execution_plan_id, finished)|
22
+ start_executing track_execution_plan(execution_plan_id, finished)
23
+ true
24
+ end),
25
+ (on ~Event do |event|
26
+ event(event)
27
+ end),
28
+ (on PoolDone.(~any) do |step|
29
+ update_manager(step)
30
+ end)
31
+ end
32
+
33
+ def termination
34
+ logger.info 'shutting down Core ...'
35
+ try_to_terminate
36
+ end
37
+
38
+ # @return false on problem
39
+ def track_execution_plan(execution_plan_id, finished)
40
+ execution_plan = @world.persistence.load_execution_plan(execution_plan_id)
41
+
42
+ if terminating?
43
+ raise Dynflow::Error, "cannot accept execution_plan_id:#{execution_plan_id} core is terminating"
44
+ end
45
+
46
+ if @execution_plan_managers[execution_plan_id]
47
+ raise Dynflow::Error, "cannot execute execution_plan_id:#{execution_plan_id} it's already running"
48
+ end
49
+
50
+ if execution_plan.state == :stopped
51
+ raise Dynflow::Error, "cannot execute execution_plan_id:#{execution_plan_id} it's stopped"
52
+ end
53
+
54
+ @execution_plan_managers[execution_plan_id] =
55
+ ExecutionPlanManager.new(@world, execution_plan, finished)
56
+
57
+ rescue Dynflow::Error => e
58
+ finished.fail e
59
+ raise e
60
+ end
61
+
62
+ def start_executing(manager)
63
+ Type! manager, ExecutionPlanManager
64
+
65
+ next_work = manager.start
66
+ continue_manager manager, next_work
67
+ end
68
+
69
+ def update_manager(finished_work)
70
+ manager = @execution_plan_managers[finished_work.execution_plan_id]
71
+ next_work = manager.what_is_next(finished_work)
72
+ continue_manager manager, next_work
73
+ end
74
+
75
+ def continue_manager(manager, next_work)
76
+ if manager.done?
77
+ loose_manager_and_set_future manager.execution_plan.id
78
+ try_to_terminate
79
+ else
80
+ feed_pool next_work
81
+ end
82
+ end
83
+
84
+ def feed_pool(work_items)
85
+ Type! work_items, Array, Work, NilClass
86
+ return if work_items.nil?
87
+ work_items = [work_items] if work_items.is_a? Work
88
+ work_items.all? { |i| Type! i, Work }
89
+ work_items.each { |new_work| @pool << new_work }
90
+ end
91
+
92
+ def loose_manager_and_set_future(execution_plan_id)
93
+ manager = @execution_plan_managers.delete(execution_plan_id)
94
+ manager.future.resolve manager.execution_plan
95
+ end
96
+
97
+ def event(event)
98
+ Type! event, Event
99
+ execution_plan_manager = @execution_plan_managers[event.execution_plan_id]
100
+ if execution_plan_manager
101
+ feed_pool execution_plan_manager.event(event)
102
+ true
103
+ else
104
+ logger.warn "dropping event #{event} - no manager for #{event.execution_plan_id}:#{event.step_id}"
105
+ event.result.fail UnprocessableEvent.new("no manager for #{event.execution_plan_id}:#{event.step_id}")
106
+ end
107
+ end
108
+
109
+ def try_to_terminate
110
+ if terminating? && @execution_plan_managers.empty?
111
+ @pool.ask(Terminate).wait
112
+ logger.info '... Core terminated.'
113
+ terminate!
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,120 @@
1
+ module Dynflow
2
+ module Executors
3
+ class Parallel < Abstract
4
+ class ExecutionPlanManager
5
+ include Algebrick::TypeCheck
6
+ include Algebrick::Matching
7
+
8
+ attr_reader :execution_plan, :future
9
+
10
+ def initialize(world, execution_plan, future)
11
+ @world = Type! world, World
12
+ @execution_plan = Type! execution_plan, ExecutionPlan
13
+ @future = Type! future, Future
14
+ @running_steps_manager = RunningStepsManager.new(world)
15
+
16
+ unless [:planned, :paused].include? execution_plan.state
17
+ raise "execution_plan is not in pending or paused state, it's #{execution_plan.state}"
18
+ end
19
+ execution_plan.update_state(:running)
20
+ end
21
+
22
+ def start
23
+ raise "The future was already set" if @future.ready?
24
+ start_run or start_finalize or finish
25
+ end
26
+
27
+ def prepare_next_step(step)
28
+ Work::Step[step, execution_plan.id].tap do |work|
29
+ @running_steps_manager.add(step, work)
30
+ end
31
+ end
32
+
33
+ # @return [Array<Work>] of Work items to continue with
34
+ def what_is_next(work)
35
+ Type! work, Work
36
+
37
+ compute_next_from_step =-> step do
38
+ raise unless @run_manager
39
+ raise if @run_manager.done?
40
+
41
+ next_steps = @run_manager.what_is_next(step)
42
+ if @run_manager.done?
43
+ start_finalize or finish
44
+ else
45
+ next_steps.map { |s| prepare_next_step(s) }
46
+ end
47
+ end
48
+
49
+ match work,
50
+
51
+ Work::Step.(step: ~any) >-> step do
52
+ suspended, work = @running_steps_manager.done(step)
53
+ if suspended
54
+ raise 'assert' unless compute_next_from_step.call(step).empty?
55
+ work
56
+ else
57
+ execution_plan.update_execution_time step.execution_time
58
+ compute_next_from_step.call step
59
+ end
60
+ end,
61
+
62
+ Work::Event.(step: ~any) >-> step do
63
+ suspended, work = @running_steps_manager.done(step)
64
+
65
+ if suspended
66
+ work
67
+ else
68
+ execution_plan.update_execution_time step.execution_time
69
+ compute_next_from_step.call step
70
+ end
71
+ end,
72
+
73
+ Work::Finalize >-> do
74
+ raise unless @finalize_manager
75
+ finish
76
+ end
77
+ end
78
+
79
+ def event(event)
80
+ Type! event, Event
81
+ raise unless event.execution_plan_id == @execution_plan.id
82
+ @running_steps_manager.event(event)
83
+ end
84
+
85
+ def done?
86
+ (!@run_manager || @run_manager.done?) && (!@finalize_manager || @finalize_manager.done?)
87
+ end
88
+
89
+ private
90
+
91
+ def no_work
92
+ raise "No work but not done" unless done?
93
+ []
94
+ end
95
+
96
+ def start_run
97
+ unless execution_plan.run_flow.empty?
98
+ raise 'run phase already started' if @run_manager
99
+ @run_manager = FlowManager.new(execution_plan, execution_plan.run_flow)
100
+ @run_manager.start.map { |s| prepare_next_step(s) }.tap { |a| raise if a.empty? }
101
+ end
102
+ end
103
+
104
+ def start_finalize
105
+ unless execution_plan.finalize_flow.empty?
106
+ raise 'finalize phase already started' if @finalize_manager
107
+ @finalize_manager = SequentialManager.new(@world, execution_plan)
108
+ Work::Finalize[@finalize_manager, execution_plan.id]
109
+ end
110
+ end
111
+
112
+ def finish
113
+ @execution_plan.update_state(execution_plan.error? ? :paused : :stopped)
114
+ return no_work
115
+ end
116
+
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,48 @@
1
+ module Dynflow
2
+ module Executors
3
+ class Parallel < Abstract
4
+ class FlowManager
5
+ include Algebrick::TypeCheck
6
+
7
+ attr_reader :execution_plan, :cursor_index
8
+
9
+ def initialize(execution_plan, flow)
10
+ @execution_plan = Type! execution_plan, ExecutionPlan
11
+ @flow = flow
12
+ @cursor_index = {}
13
+ @cursor = build_root_cursor
14
+ end
15
+
16
+ def done?
17
+ @cursor.done?
18
+ end
19
+
20
+ # @return [Set] of steps to continue with
21
+ def what_is_next(flow_step)
22
+ execution_plan.steps[flow_step.id] = flow_step
23
+ # TODO can be probably disabled to improve performance, execution time will not be updated, maybe more - check
24
+ execution_plan.save
25
+ return [] if flow_step.state == :suspended
26
+
27
+ success = flow_step.state != :error
28
+ return cursor_index[flow_step.id].what_is_next(flow_step, success)
29
+ end
30
+
31
+ # @return [Set] of steps to continue with
32
+ def start
33
+ return @cursor.what_is_next.tap do |steps|
34
+ raise 'invalid state' if steps.empty? && !done?
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def build_root_cursor
41
+ # the root cursor has to always run against sequence
42
+ sequence = @flow.is_a?(Flows::Sequence) ? @flow : Flows::Sequence.new([@flow])
43
+ return SequenceCursor.new(self, sequence, nil)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,102 @@
1
+ module Dynflow
2
+ module Executors
3
+ class Parallel < Abstract
4
+ class Pool < MicroActor
5
+ class RoundRobin
6
+ def initialize
7
+ @data = []
8
+ @cursor = 0
9
+ end
10
+
11
+ def add(item)
12
+ @data.push item
13
+ self
14
+ end
15
+
16
+ def delete(item)
17
+ @data.delete item
18
+ self
19
+ end
20
+
21
+ def next
22
+ @cursor = 0 if @cursor > @data.size-1
23
+ @data[@cursor].tap { @cursor += 1 }
24
+ end
25
+
26
+ def empty?
27
+ @data.empty?
28
+ end
29
+ end
30
+
31
+ class JobStorage
32
+ def initialize
33
+ @round_robin = RoundRobin.new
34
+ @jobs = Hash.new { |h, k| h[k] = [] }
35
+ end
36
+
37
+ def add(work)
38
+ @round_robin.add work.execution_plan_id unless tracked?(work)
39
+ @jobs[work.execution_plan_id] << work
40
+ end
41
+
42
+ def pop
43
+ return nil if empty?
44
+ execution_plan_id = @round_robin.next
45
+ @jobs[execution_plan_id].shift.tap { delete execution_plan_id if @jobs[execution_plan_id].empty? }
46
+ end
47
+
48
+ def empty?
49
+ @jobs.empty?
50
+ end
51
+
52
+ private
53
+
54
+ def tracked?(work)
55
+ @jobs.has_key? work.execution_plan_id
56
+ end
57
+
58
+ def delete(execution_plan_id)
59
+ @round_robin.delete execution_plan_id
60
+ @jobs.delete execution_plan_id
61
+ end
62
+ end
63
+
64
+ def initialize(core, pool_size, transaction_adapter)
65
+ super(core.logger, core, pool_size, transaction_adapter)
66
+ end
67
+
68
+ private
69
+
70
+ def delayed_initialize(core, pool_size, transaction_adapter)
71
+ @core = core
72
+ @pool_size = pool_size
73
+ @free_workers = Array.new(pool_size) { Worker.new(self, transaction_adapter) }
74
+ @jobs = JobStorage.new
75
+ end
76
+
77
+ def on_message(message)
78
+ match message,
79
+ ~Work >-> work do
80
+ @jobs.add work
81
+ distribute_jobs
82
+ end,
83
+ WorkerDone.(~any, ~any) >-> step, worker do
84
+ @core << PoolDone[step]
85
+ @free_workers << worker
86
+ distribute_jobs
87
+ end
88
+ end
89
+
90
+ def termination
91
+ raise unless @free_workers.size == @pool_size
92
+ @free_workers.map { |worker| worker.ask(Terminate) }.each(&:wait)
93
+ super
94
+ end
95
+
96
+ def distribute_jobs
97
+ @free_workers.pop << @jobs.pop until @free_workers.empty? || @jobs.empty?
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,63 @@
1
+ module Dynflow
2
+ module Executors
3
+ class Parallel < Abstract
4
+
5
+ # Handles the events generated while running actions, makes sure
6
+ # the events are sent to the action only when in suspended state
7
+ class RunningStepsManager
8
+ include Algebrick::TypeCheck
9
+
10
+ def initialize(world)
11
+ @world = Type! world, World
12
+ @running_steps = {}
13
+ @events = WorkQueue.new(Integer, Work)
14
+ end
15
+
16
+ def add(step, work)
17
+ Type! step, ExecutionPlan::Steps::RunStep
18
+ @running_steps[step.id] = step
19
+ # we make sure not to run any event when the step is still being executed
20
+ @events.push(step.id, work)
21
+ self
22
+ end
23
+
24
+ # @returns [Work, nil]
25
+ def done(step)
26
+ Type! step, ExecutionPlan::Steps::RunStep
27
+ @events.shift(step.id).tap do |work|
28
+ work.event.result.resolve true if Work::Event === work
29
+ end
30
+
31
+ if step.state == :suspended
32
+ return true, @events.first(step.id)
33
+ else
34
+ while (event = @events.shift(step.id))
35
+ message = "step #{step.execution_plan_id}:#{step.id} dropping event #{event.event}"
36
+ @world.logger.warn message
37
+ event.event.result.fail UnprocessableEvent.new(message).tap { |e| e.set_backtrace(caller) }
38
+ end
39
+ raise 'assert' unless @events.empty?(step.id)
40
+ @running_steps.delete(step.id)
41
+ return false, nil
42
+ end
43
+ end
44
+
45
+ # @returns [Work, nil]
46
+ def event(event)
47
+ Type! event, Event
48
+
49
+ step = @running_steps[event.step_id]
50
+ unless step
51
+ event.result.fail UnprocessableEvent.new('step is not suspended, it cannot process events')
52
+ return nil
53
+ end
54
+
55
+ can_run_event = @events.empty?(step.id)
56
+ work = Work::Event[step, event.execution_plan_id, event]
57
+ @events.push(step.id, work)
58
+ work if can_run_event
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end