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,13 @@
1
+ require 'forwardable'
2
+
3
+ module Dynflow
4
+ module Flows
5
+
6
+ require 'dynflow/flows/abstract'
7
+ require 'dynflow/flows/atom'
8
+ require 'dynflow/flows/abstract_composed'
9
+ require 'dynflow/flows/concurrence'
10
+ require 'dynflow/flows/sequence'
11
+
12
+ end
13
+ end
@@ -0,0 +1,36 @@
1
+ module Dynflow
2
+ module Flows
3
+
4
+ class Abstract < Serializable
5
+ include Algebrick::TypeCheck
6
+
7
+ def initialize
8
+ raise 'cannot instantiate Flows::Abstract'
9
+ end
10
+
11
+ def to_hash
12
+ { :class => self.class.name }
13
+ end
14
+
15
+ def empty?
16
+ self.size == 0
17
+ end
18
+
19
+ def size
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def includes_step?(step_id)
24
+ self.all_step_ids.any? { |s| s == step_id }
25
+ end
26
+
27
+ def all_step_ids
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def flatten!
32
+ raise NotImplementedError
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,104 @@
1
+ module Dynflow
2
+ module Flows
3
+ class AbstractComposed < Abstract
4
+
5
+ attr_reader :flows
6
+
7
+ def initialize(flows)
8
+ Type! flows, Array
9
+ flows.all? { |f| Type! f, Abstract }
10
+ @flows = flows
11
+ end
12
+
13
+ def to_hash
14
+ super.merge recursive_to_hash(:flows => flows)
15
+ end
16
+
17
+ def <<(v)
18
+ @flows << v
19
+ self
20
+ end
21
+
22
+ def [](*args)
23
+ @flows[*args]
24
+ end
25
+
26
+ def []=(*args)
27
+ @flows.[]=(*args)
28
+ end
29
+
30
+ def size
31
+ @flows.size
32
+ end
33
+
34
+ alias_method :sub_flows, :flows
35
+
36
+ # @return [Array<Integer>] all step_ids recursively in the flow
37
+ def all_step_ids
38
+ flows.map(&:all_step_ids).flatten
39
+ end
40
+
41
+ def add_and_resolve(dependency_graph, new_flow)
42
+ return if new_flow.empty?
43
+
44
+ satisfying_flows = find_satisfying_sub_flows(dependency_graph, new_flow)
45
+ add_to_sequence(satisfying_flows, new_flow)
46
+ flatten!
47
+ end
48
+
49
+ def flatten!
50
+ self.sub_flows.to_enum.with_index.reverse_each do |flow, i|
51
+ if flow.class == self.class
52
+ expand_steps(i)
53
+ elsif flow.is_a?(AbstractComposed) && flow.sub_flows.size == 1
54
+ self.sub_flows[i] = flow.sub_flows.first
55
+ end
56
+ end
57
+
58
+ self.sub_flows.map(&:flatten!)
59
+ end
60
+
61
+ protected
62
+
63
+ def self.new_from_hash(hash)
64
+ check_class_matching hash
65
+ new(hash[:flows].map { |flow_hash| from_hash(flow_hash) })
66
+ end
67
+
68
+ # adds the +new_flow+ in a way that it's in sequence with
69
+ # the +satisfying_flows+
70
+ def add_to_sequence(satisfying_flows, new_flow)
71
+ raise NotImplementedError
72
+ end
73
+
74
+ private
75
+
76
+ def find_satisfying_sub_flows(dependency_graph, new_flow)
77
+ satisfying_flows = Set.new
78
+ new_flow.all_step_ids.each do |step_id|
79
+ dependency_graph.required_step_ids(step_id).each do |required_step_id|
80
+ satisfying_flow = sub_flows.find do |flow|
81
+ flow.includes_step?(required_step_id)
82
+ end
83
+ if satisfying_flow
84
+ satisfying_flows << satisfying_flow
85
+ dependency_graph.mark_satisfied(step_id, required_step_id)
86
+ end
87
+ end
88
+ end
89
+
90
+ return satisfying_flows.to_a
91
+ end
92
+
93
+ def expand_steps(index)
94
+ expanded_step = self.sub_flows[index]
95
+ self.sub_flows.delete_at(index)
96
+ expanded_step.sub_flows.each do |flow|
97
+ self.sub_flows.insert(index, flow)
98
+ index += 1
99
+ end
100
+ end
101
+
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,36 @@
1
+ module Dynflow
2
+ module Flows
3
+ class Atom < Abstract
4
+
5
+ attr_reader :step_id
6
+
7
+ def to_hash
8
+ super.merge(:step_id => step_id)
9
+ end
10
+
11
+ def initialize(step_id)
12
+ @step_id = Type! step_id, Integer
13
+ end
14
+
15
+ def size
16
+ 1
17
+ end
18
+
19
+ def all_step_ids
20
+ [step_id]
21
+ end
22
+
23
+ def flatten!
24
+ # nothing to do
25
+ end
26
+
27
+ protected
28
+
29
+ def self.new_from_hash(hash)
30
+ check_class_matching hash
31
+ new(hash[:step_id])
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ module Dynflow
2
+ module Flows
3
+ class Concurrence < AbstractComposed
4
+
5
+ protected
6
+
7
+ def add_to_sequence(satisfying_flows, dependent_flow)
8
+ if satisfying_flows.empty?
9
+ self.sub_flows << dependent_flow
10
+ return
11
+ end
12
+ extracted_flow = extract_flows(satisfying_flows)
13
+ sequence = Sequence.new([extracted_flow])
14
+
15
+ self.sub_flows << sequence
16
+ sequence << dependent_flow
17
+ end
18
+
19
+ def extract_flows(extracted_sub_flows)
20
+ extracted_sub_flows.each do |sub_flow|
21
+ self.sub_flows.delete(sub_flow)
22
+ end
23
+
24
+ return Concurrence.new(extracted_sub_flows)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ module Dynflow
2
+ module Flows
3
+ class Sequence < AbstractComposed
4
+
5
+ protected
6
+
7
+ def add_to_sequence(satisfying_flows, dependent_flow)
8
+ # the flows are already in sequence, we don't need to do anything extra
9
+ self << dependent_flow
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,173 @@
1
+ module Dynflow
2
+ class Future
3
+ Error = Class.new StandardError
4
+ FutureAlreadySet = Class.new Error
5
+ FutureFailed = Class.new Error
6
+ TimeOut = Class.new Error
7
+
8
+ # `#future` will become resolved to `true` when ``#countdown!`` is called `count` times
9
+ class CountDownLatch
10
+ attr_reader :future
11
+
12
+ def initialize(count, future = Future.new)
13
+ raise ArgumentError if count < 0
14
+ @count = count
15
+ @lock = Mutex.new
16
+ @future = future
17
+ end
18
+
19
+ def countdown!
20
+ @lock.synchronize do
21
+ @count -= 1 if @count > 0
22
+ @future.resolve true if @count == 0 && !@future.ready?
23
+ end
24
+ end
25
+
26
+ def count
27
+ @lock.synchronize { @count }
28
+ end
29
+ end
30
+
31
+ include Algebrick::TypeCheck
32
+ extend Algebrick::TypeCheck
33
+
34
+ def self.join(futures, result = Future.new)
35
+ countdown = CountDownLatch.new(futures.size, result)
36
+ futures.each do |future|
37
+ Type! future, Future
38
+ future.do_then { |_| countdown.countdown! }
39
+ end
40
+ result
41
+ end
42
+
43
+ def initialize(&task)
44
+ @lock = Mutex.new
45
+ @value = nil
46
+ @resolved = false
47
+ @failed = false
48
+ @waiting = []
49
+ @tasks = []
50
+ do_then &task if task
51
+ end
52
+
53
+ def value(timeout = nil)
54
+ wait timeout
55
+ @lock.synchronize { @value }
56
+ end
57
+
58
+ def value!
59
+ value.tap { raise value if failed? }
60
+ end
61
+
62
+ def resolve(result)
63
+ set result, false
64
+ end
65
+
66
+ def fail(exception)
67
+ Type! exception, Exception, String
68
+ if exception.is_a? String
69
+ exception = FutureFailed.new(exception).tap { |e| e.set_backtrace caller }
70
+ end
71
+ set exception, true
72
+ end
73
+
74
+ def evaluate_to(&block)
75
+ resolve block.call
76
+ rescue => error
77
+ self.fail error
78
+ end
79
+
80
+ def evaluate_to!(&block)
81
+ evaluate_to &block
82
+ raise value if self.failed?
83
+ end
84
+
85
+ def do_then(&task)
86
+ call_task = @lock.synchronize do
87
+ @tasks << task unless _ready?
88
+ @resolved
89
+ end
90
+ task.call value if call_task
91
+ self
92
+ end
93
+
94
+ def set(value, failed)
95
+ @lock.synchronize do
96
+ raise FutureAlreadySet, "future already set to #{@value} cannot use #{value}" if _ready?
97
+ if failed
98
+ @failed = true
99
+ else
100
+ @resolved = true
101
+ end
102
+ @value = value
103
+ while (thread = @waiting.pop)
104
+ begin
105
+ thread.wakeup
106
+ rescue ThreadError
107
+ retry
108
+ end
109
+ end
110
+ !failed
111
+ end
112
+ @tasks.each { |t| t.call value }
113
+ self
114
+ end
115
+
116
+ def wait(timeout = nil)
117
+ @lock.synchronize do
118
+ unless _ready?
119
+ @waiting << Thread.current
120
+ clock.ping self, timeout, Thread.current, :expired if timeout
121
+ @lock.sleep
122
+ raise TimeOut unless _ready?
123
+ end
124
+ end
125
+ self
126
+ end
127
+
128
+ def ready?
129
+ @lock.synchronize { _ready? }
130
+ end
131
+
132
+ def resolved?
133
+ @lock.synchronize { @resolved }
134
+ end
135
+
136
+ def failed?
137
+ @lock.synchronize { @failed }
138
+ end
139
+
140
+ def tangle(future)
141
+ do_then { |v| future.set v, failed? }
142
+ end
143
+
144
+ # @api private
145
+ def expired(thread)
146
+ @lock.synchronize do
147
+ thread.wakeup if @waiting.delete(thread)
148
+ end
149
+ end
150
+
151
+ private
152
+
153
+ def _ready?
154
+ @resolved || @failed
155
+ end
156
+
157
+ @clock_barrier = Mutex.new
158
+
159
+ # @api private
160
+ def self.clock
161
+ @clock_barrier.synchronize do
162
+ # TODO remove global state and use world.clock, needs to be terminated in right order
163
+ @clock ||= Clock.new(::Logger.new($stderr)).tap do |clock|
164
+ at_exit { clock.ask(Clock::Terminate).wait }
165
+ end
166
+ end
167
+ end
168
+
169
+ def clock
170
+ self.class.clock
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,7 @@
1
+ module Dynflow
2
+ module Listeners
3
+ require 'dynflow/listeners/serialization'
4
+ require 'dynflow/listeners/abstract'
5
+ require 'dynflow/listeners/socket'
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module Dynflow
2
+ module Listeners
3
+ class Abstract
4
+ include Algebrick::TypeCheck
5
+ attr_reader :world, :logger
6
+
7
+ def initialize(world)
8
+ @world = Type! world, World
9
+ @logger = world.logger
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ module Dynflow
2
+ module Listeners
3
+ module Serialization
4
+ SocketMessage = Algebrick.type do
5
+ Execute = type { fields request_id: Integer, execution_plan_uuid: String }
6
+ Confirmation = type do
7
+ variants Accepted = type { fields request_id: Integer },
8
+ Failed = type { fields request_id: Integer, error: String }
9
+ end
10
+ Done = type { fields request_id: Integer, execution_plan_uuid: String }
11
+
12
+ variants Execute, Confirmation, Done
13
+ end
14
+
15
+ def dump(obj)
16
+ MultiJson.dump(obj.to_hash)
17
+ end
18
+
19
+ def load(str)
20
+ SocketMessage.from_hash MultiJson.load(str)
21
+ end
22
+
23
+ def send_message(io, message, barrier = nil)
24
+ barrier.lock if barrier
25
+ io.puts dump(message)
26
+ ensure
27
+ barrier.unlock if barrier
28
+ end
29
+
30
+ def receive_message(io)
31
+ if (message = io.gets)
32
+ load(message)
33
+ else
34
+ nil
35
+ end
36
+ rescue IOError
37
+ nil
38
+ end
39
+ end
40
+ end
41
+ end