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,97 @@
1
+ module Dynflow
2
+ module Executors
3
+ class Parallel < Abstract
4
+ class SequenceCursor
5
+
6
+ def initialize(flow_manager, sequence, parent_cursor = nil)
7
+ @flow_manager = flow_manager
8
+ @sequence = sequence
9
+ @parent_cursor = parent_cursor
10
+ @todo = []
11
+ @index = -1 # starts before first element
12
+ @no_error_so_far = true
13
+ end
14
+
15
+ # @param [ExecutionPlan::Steps::Abstract, SequenceCursor] work
16
+ # step or sequence cursor that was done
17
+ # @param [true, false] success was the work finished successfully
18
+ # @return [Array<Integer>] new step_ids that can be done next
19
+ def what_is_next(work = nil, success = true)
20
+ unless work.nil? || @todo.delete(work)
21
+ raise "marking as done work that was not expected: #{work.inspect}"
22
+ end
23
+
24
+ @no_error_so_far &&= success
25
+
26
+ if done_here?
27
+ return next_steps
28
+ else
29
+ return []
30
+ end
31
+ end
32
+
33
+
34
+ # return true if we can't move the cursor further, either when
35
+ # everyting is done in the sequence or there was some failure
36
+ # that prevents us from moving
37
+ def done?
38
+ (!@no_error_so_far && done_here?) || @index == @sequence.size
39
+ end
40
+
41
+ protected
42
+
43
+ # steps we can do right now without waiting for anything
44
+ def steps_todo
45
+ @todo.map do |item|
46
+ case item
47
+ when SequenceCursor
48
+ item.steps_todo
49
+ else
50
+ item
51
+ end
52
+ end.flatten
53
+ end
54
+
55
+ def move
56
+ @index += 1
57
+ next_flow = @sequence.sub_flows[@index]
58
+ add_todo(next_flow)
59
+ end
60
+
61
+ private
62
+
63
+ def done_here?
64
+ @todo.empty?
65
+ end
66
+
67
+ def next_steps
68
+ move if @no_error_so_far
69
+ if done?
70
+ if @parent_cursor
71
+ return @parent_cursor.what_is_next(self, @no_error_so_far)
72
+ else
73
+ return []
74
+ end
75
+ else
76
+ return steps_todo
77
+ end
78
+ end
79
+
80
+ def add_todo(flow)
81
+ case flow
82
+ when Flows::Sequence
83
+ @todo << SequenceCursor.new(@flow_manager, flow, self).tap do |cursor|
84
+ cursor.move
85
+ end
86
+ when Flows::Concurrence
87
+ flow.sub_flows.each { |sub_flow| add_todo(sub_flow) }
88
+ when Flows::Atom
89
+ @flow_manager.cursor_index[flow.step_id] = self
90
+ @todo << @flow_manager.execution_plan.steps[flow.step_id]
91
+ end
92
+ end
93
+
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,81 @@
1
+ module Dynflow
2
+ module Executors
3
+ class Parallel::SequentialManager
4
+ attr_reader :execution_plan, :world
5
+
6
+ def initialize(world, execution_plan)
7
+ @world = world
8
+ @execution_plan = execution_plan
9
+ @done = false
10
+ end
11
+
12
+ def run
13
+ with_state_updates do
14
+ dispatch(execution_plan.run_flow)
15
+ finalize
16
+ end
17
+
18
+ return execution_plan
19
+ end
20
+
21
+ def finalize
22
+ reset_finalize_steps
23
+ unless execution_plan.error?
24
+ world.transaction_adapter.transaction do
25
+ unless dispatch(execution_plan.finalize_flow)
26
+ world.transaction_adapter.rollback
27
+ end
28
+ end
29
+ end
30
+ @done = true
31
+ end
32
+
33
+ def reset_finalize_steps
34
+ execution_plan.finalize_flow.all_step_ids.each do |step_id|
35
+ step = execution_plan.steps[step_id]
36
+ step.state = :pending if [:success, :error].include? step.state
37
+ end
38
+ end
39
+
40
+ def done?
41
+ @done
42
+ end
43
+
44
+ private
45
+
46
+ def dispatch(flow)
47
+ case flow
48
+ when Flows::Sequence
49
+ run_in_sequence(flow.flows)
50
+ when Flows::Concurrence
51
+ run_in_concurrence(flow.flows)
52
+ when Flows::Atom
53
+ run_step(execution_plan.steps[flow.step_id])
54
+ else
55
+ raise ArgumentError, "Don't know how to run #{flow}"
56
+ end
57
+ end
58
+
59
+ def run_in_sequence(steps)
60
+ steps.all? { |s| dispatch(s) }
61
+ end
62
+
63
+ def run_in_concurrence(steps)
64
+ run_in_sequence(steps)
65
+ end
66
+
67
+ def run_step(step)
68
+ step.execute
69
+ execution_plan.update_execution_time step.execution_time
70
+ execution_plan.save
71
+ return step.state != :error
72
+ end
73
+
74
+ def with_state_updates(&block)
75
+ execution_plan.update_state(:running)
76
+ block.call
77
+ execution_plan.update_state(execution_plan.error? ? :paused : :stopped)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,44 @@
1
+ module Dynflow
2
+ module Executors
3
+ class Parallel < Abstract
4
+ class WorkQueue
5
+ include Algebrick::TypeCheck
6
+
7
+ def initialize(key_type = Object, work_type = Object)
8
+ @key_type = key_type
9
+ @work_type = work_type
10
+ @stash = Hash.new { |hash, key| hash[key] = [] }
11
+ end
12
+
13
+ def push(key, work)
14
+ Type! key, @key_type
15
+ Type! work, @work_type
16
+ @stash[key].push work
17
+ end
18
+
19
+ def shift(key)
20
+ return nil unless present? key
21
+ @stash[key].shift.tap { |work| @stash.delete(key) if @stash[key].empty? }
22
+ end
23
+
24
+ def present?(key)
25
+ @stash.key?(key)
26
+ end
27
+
28
+ def empty?(key)
29
+ !present?(key)
30
+ end
31
+
32
+ def size(key)
33
+ return 0 if empty?(key)
34
+ @stash[key].size
35
+ end
36
+
37
+ def first(key)
38
+ return nil if empty?(key)
39
+ @stash[key].first
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ module Dynflow
2
+ module Executors
3
+ class Parallel < Abstract
4
+ class Worker < MicroActor
5
+ def initialize(pool, transaction_adapter)
6
+ super(pool.logger, pool, transaction_adapter)
7
+ end
8
+
9
+ private
10
+
11
+ def delayed_initialize(pool, transaction_adapter)
12
+ @pool = pool
13
+ @transaction_adapter = Type! transaction_adapter, TransactionAdapters::Abstract
14
+ end
15
+
16
+ def on_message(message)
17
+ match message,
18
+ Work::Step.(step: ~any) | Work::Event.(step: ~any, event: Event.(event: ~any)) >-> step, event do
19
+ step.execute event
20
+ end,
21
+ Work::Finalize.(~any, any) >-> sequential_manager do
22
+ sequential_manager.finalize
23
+ end
24
+ @pool << WorkerDone[work: message, worker: self]
25
+ @transaction_adapter.cleanup
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,38 @@
1
+ require 'multi_json'
2
+ require 'socket'
3
+
4
+ module Dynflow
5
+ module Executors
6
+ class RemoteViaSocket < Abstract
7
+ require 'dynflow/executors/remote_via_socket/core'
8
+
9
+ include Listeners::Serialization
10
+ include Algebrick::Matching
11
+
12
+ def initialize(world, socket_path)
13
+ super world
14
+ @core = Core.new world, socket_path
15
+ end
16
+
17
+ def execute(execution_plan_id, finished = Future.new)
18
+ @core.ask(Core::Execute[execution_plan_id, finished]).value!.value!
19
+ finished
20
+ rescue => e
21
+ finished.fail e unless finished.ready?
22
+ raise e
23
+ end
24
+
25
+ def event(execution_plan_id, step_id, event, future = Future)
26
+ raise 'events are handled in a process with real executor'
27
+ end
28
+
29
+ def terminate(future = Future.new)
30
+ @core.ask(MicroActor::Terminate, future)
31
+ end
32
+
33
+ def initialized
34
+ @core.initialized
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,150 @@
1
+ module Dynflow
2
+ module Executors
3
+ class RemoteViaSocket < Abstract
4
+ class Core < MicroActor
5
+ include Listeners::Serialization
6
+
7
+ Message = Algebrick.type do
8
+ variants Closed = atom,
9
+ Received = type { fields message: SocketMessage },
10
+ Execute = type { fields execution_plan_uuid: String, future: Future }
11
+ end
12
+
13
+ Execution = Algebrick.type do
14
+ fields! id: Integer, accepted: Future, finished: Future
15
+ end
16
+
17
+ module Execution
18
+ def accept!
19
+ accepted.resolve true
20
+ self
21
+ end
22
+
23
+ def reject!(error)
24
+ accepted.fail error
25
+ finished.fail error
26
+ self
27
+ end
28
+
29
+ def success!(value)
30
+ raise unless accepted.ready?
31
+ finished.resolve value
32
+ self
33
+ end
34
+
35
+ def fail!(error)
36
+ if accepted.ready?
37
+ finished.fail error
38
+ else
39
+ reject! error
40
+ end
41
+ self
42
+ end
43
+ end
44
+
45
+ def initialize(world, socket_path)
46
+ super(world.logger, world, socket_path)
47
+ end
48
+
49
+ private
50
+
51
+ def delayed_initialize(world, socket_path)
52
+ @socket_path = Type! socket_path, String
53
+ @world = Type! world, World
54
+ @socket = nil
55
+ @last_id = 0
56
+ @executions = {}
57
+ connect
58
+ end
59
+
60
+ def termination
61
+ disconnect
62
+ end
63
+
64
+ def on_message(message)
65
+ match message,
66
+
67
+ (on Core::Execute.(~any, ~any) do |execution_plan_uuid, future|
68
+ raise 'terminating' if terminating?
69
+ id, accepted = add_execution future
70
+ success = connect && begin
71
+ send_message @socket, RemoteViaSocket::Execute[id, execution_plan_uuid]
72
+ true
73
+ rescue IOError => error
74
+ logger.warn error
75
+ false
76
+ end
77
+
78
+ unless success
79
+ @executions[id].reject! Dynflow::Error.new(
80
+ 'No connection to RemoteViaSocket::Listener')
81
+ end
82
+
83
+ return accepted
84
+ end),
85
+
86
+ (on Received.(Accepted.(~any)) do |id|
87
+ @executions[id].accept!
88
+ end),
89
+
90
+ (on Received.(Failed.(~any, ~any)) do |id, error|
91
+ @executions.delete(id).reject! Dynflow::Error.new(error)
92
+ end),
93
+
94
+ (on Received.(Done.(~any, ~any)) do |id, uuid|
95
+ @executions.delete(id).success! @world.persistence.load_execution_plan(uuid)
96
+ end),
97
+
98
+ (on Closed do
99
+ @socket = nil
100
+ logger.info 'Disconnected from server.'
101
+ @executions.each { |_, c| c.fail! 'No connection to RemoteViaSocket::Listener' }
102
+ @executions.clear
103
+ terminate! if terminating?
104
+ end)
105
+ end
106
+
107
+ def add_execution(finished)
108
+ @executions[id = (@last_id += 1)] = Execution[id, accepted = Future.new, finished]
109
+ return id, accepted
110
+ end
111
+
112
+ def connect
113
+ return true if @socket
114
+ @socket = UNIXSocket.new @socket_path
115
+ logger.info 'Connected to server.'
116
+ read_socket_until_closed
117
+ true
118
+ rescue IOError => error
119
+ logger.warn error
120
+ false
121
+ end
122
+
123
+ def disconnect
124
+ return true unless @socket
125
+ @socket.shutdown :RDWR
126
+ true
127
+ end
128
+
129
+ def read_socket_until_closed
130
+ Thread.new do
131
+ catch(:stop_reading) do
132
+ loop { read_socket }
133
+ end
134
+ end
135
+ end
136
+
137
+ def read_socket
138
+ match message = receive_message(@socket),
139
+ SocketMessage >-> { self << Received[message] },
140
+ NilClass.to_m >-> do
141
+ self << Closed
142
+ throw :stop_reading
143
+ end
144
+ rescue => error
145
+ logger.fatal error
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end