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,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