dynflow 0.1.0 → 0.2.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.
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