dynflow 0.7.9 → 0.8.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 (118) hide show
  1. data/.gitignore +2 -0
  2. data/.travis.yml +16 -1
  3. data/Gemfile +13 -1
  4. data/doc/pages/source/_drafts/2015-03-01-new-documentation.markdown +10 -0
  5. data/doc/pages/source/_includes/menu.html +1 -0
  6. data/doc/pages/source/_includes/menu_right.html +1 -1
  7. data/doc/pages/source/_sass/_bootstrap-variables.sass +1 -0
  8. data/doc/pages/source/_sass/_style.scss +4 -0
  9. data/doc/pages/source/blog/index.html +12 -0
  10. data/doc/pages/source/documentation/index.md +330 -5
  11. data/dynflow.gemspec +3 -1
  12. data/examples/example_helper.rb +18 -11
  13. data/examples/orchestrate_evented.rb +2 -1
  14. data/examples/remote_executor.rb +53 -20
  15. data/lib/dynflow.rb +16 -6
  16. data/lib/dynflow/action/suspended.rb +1 -1
  17. data/lib/dynflow/action/with_sub_plans.rb +3 -6
  18. data/lib/dynflow/actor.rb +56 -0
  19. data/lib/dynflow/clock.rb +43 -38
  20. data/lib/dynflow/config.rb +107 -0
  21. data/lib/dynflow/connectors.rb +7 -0
  22. data/lib/dynflow/connectors/abstract.rb +41 -0
  23. data/lib/dynflow/connectors/database.rb +175 -0
  24. data/lib/dynflow/connectors/direct.rb +71 -0
  25. data/lib/dynflow/coordinator.rb +280 -0
  26. data/lib/dynflow/coordinator_adapters.rb +8 -0
  27. data/lib/dynflow/coordinator_adapters/abstract.rb +28 -0
  28. data/lib/dynflow/coordinator_adapters/sequel.rb +29 -0
  29. data/lib/dynflow/dispatcher.rb +58 -0
  30. data/lib/dynflow/dispatcher/abstract.rb +14 -0
  31. data/lib/dynflow/dispatcher/client_dispatcher.rb +139 -0
  32. data/lib/dynflow/dispatcher/executor_dispatcher.rb +86 -0
  33. data/lib/dynflow/errors.rb +7 -1
  34. data/lib/dynflow/execution_history.rb +46 -0
  35. data/lib/dynflow/execution_plan.rb +19 -15
  36. data/lib/dynflow/executors.rb +0 -1
  37. data/lib/dynflow/executors/abstract.rb +5 -10
  38. data/lib/dynflow/executors/parallel.rb +16 -13
  39. data/lib/dynflow/executors/parallel/core.rb +76 -78
  40. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +4 -5
  41. data/lib/dynflow/executors/parallel/pool.rb +22 -52
  42. data/lib/dynflow/executors/parallel/running_steps_manager.rb +9 -2
  43. data/lib/dynflow/executors/parallel/worker.rb +5 -10
  44. data/lib/dynflow/persistence.rb +14 -0
  45. data/lib/dynflow/persistence_adapters/abstract.rb +14 -3
  46. data/lib/dynflow/persistence_adapters/sequel.rb +142 -38
  47. data/lib/dynflow/persistence_adapters/sequel_migrations/004_coordinator_records.rb +14 -0
  48. data/lib/dynflow/persistence_adapters/sequel_migrations/005_envelopes.rb +14 -0
  49. data/lib/dynflow/round_robin.rb +37 -0
  50. data/lib/dynflow/serializable.rb +1 -2
  51. data/lib/dynflow/serializer.rb +46 -0
  52. data/lib/dynflow/testing/dummy_executor.rb +2 -2
  53. data/lib/dynflow/testing/dummy_world.rb +1 -1
  54. data/lib/dynflow/transaction_adapters/abstract.rb +0 -5
  55. data/lib/dynflow/transaction_adapters/active_record.rb +0 -10
  56. data/lib/dynflow/version.rb +1 -1
  57. data/lib/dynflow/web.rb +26 -0
  58. data/lib/dynflow/web/console.rb +108 -0
  59. data/lib/dynflow/web/console_helpers.rb +158 -0
  60. data/lib/dynflow/web/filtering_helpers.rb +85 -0
  61. data/lib/dynflow/web/world_helpers.rb +9 -0
  62. data/lib/dynflow/web_console.rb +3 -310
  63. data/lib/dynflow/world.rb +188 -119
  64. data/test/abnormal_states_recovery_test.rb +152 -0
  65. data/test/action_test.rb +2 -3
  66. data/test/clock_test.rb +1 -5
  67. data/test/coordinator_test.rb +152 -0
  68. data/test/dispatcher_test.rb +146 -0
  69. data/test/execution_plan_test.rb +2 -1
  70. data/test/executor_test.rb +534 -612
  71. data/test/middleware_test.rb +4 -4
  72. data/test/persistence_test.rb +17 -0
  73. data/test/prepare_travis_env.sh +35 -0
  74. data/test/rescue_test.rb +5 -3
  75. data/test/round_robin_test.rb +28 -0
  76. data/test/support/code_workflow_example.rb +0 -73
  77. data/test/support/dummy_example.rb +130 -0
  78. data/test/support/test_execution_log.rb +41 -0
  79. data/test/test_helper.rb +222 -116
  80. data/test/testing_test.rb +10 -10
  81. data/test/web_console_test.rb +3 -3
  82. data/test/world_test.rb +23 -0
  83. data/web/assets/images/logo-square.png +0 -0
  84. data/web/assets/stylesheets/application.css +9 -0
  85. data/web/assets/vendor/bootstrap/config.json +429 -0
  86. data/web/assets/vendor/bootstrap/css/bootstrap-theme.css +479 -0
  87. data/web/assets/vendor/bootstrap/css/bootstrap-theme.min.css +10 -0
  88. data/web/assets/vendor/bootstrap/css/bootstrap.css +5377 -4980
  89. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -8
  90. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  91. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
  92. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  93. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  94. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
  95. data/web/assets/vendor/bootstrap/js/bootstrap.js +1674 -1645
  96. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +11 -5
  97. data/web/views/execution_history.erb +17 -0
  98. data/web/views/index.erb +4 -6
  99. data/web/views/layout.erb +44 -8
  100. data/web/views/show.erb +4 -5
  101. data/web/views/worlds.erb +26 -0
  102. metadata +116 -23
  103. checksums.yaml +0 -15
  104. data/lib/dynflow/daemon.rb +0 -30
  105. data/lib/dynflow/executors/remote_via_socket.rb +0 -43
  106. data/lib/dynflow/executors/remote_via_socket/core.rb +0 -184
  107. data/lib/dynflow/future.rb +0 -173
  108. data/lib/dynflow/listeners.rb +0 -7
  109. data/lib/dynflow/listeners/abstract.rb +0 -17
  110. data/lib/dynflow/listeners/serialization.rb +0 -77
  111. data/lib/dynflow/listeners/socket.rb +0 -117
  112. data/lib/dynflow/micro_actor.rb +0 -102
  113. data/lib/dynflow/simple_world.rb +0 -19
  114. data/test/remote_via_socket_test.rb +0 -170
  115. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +0 -1109
  116. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +0 -9
  117. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  118. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
@@ -0,0 +1,9 @@
1
+ module Dynflow
2
+ module Web
3
+ module WorldHelpers
4
+ def world
5
+ settings.world
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,312 +1,5 @@
1
- require 'dynflow'
2
- require 'pp'
3
- require 'sinatra/base'
4
- require 'yaml'
1
+ require 'dynflow/web'
5
2
 
6
- module Dynflow
7
- class WebConsole < Sinatra::Base
3
+ warn %{"require 'dynflow/web_console'" is deprecated, use "require 'dynflow/web'" instead}
8
4
 
9
- def self.setup(&block)
10
- Sinatra.new(self) do
11
- instance_exec(&block)
12
- end
13
- end
14
-
15
- web_dir = File.join(File.expand_path('../../../web', __FILE__))
16
-
17
- set :public_folder, File.join(web_dir, 'assets')
18
- set :views, File.join(web_dir, 'views')
19
- set :per_page, 10
20
-
21
- helpers ERB::Util
22
-
23
- helpers do
24
- def world
25
- settings.world
26
- end
27
-
28
- def prettify_value(value)
29
- YAML.dump(value)
30
- end
31
-
32
- def prettyprint(value)
33
- value = prettyprint_references(value)
34
- if value
35
- pretty_value = prettify_value(value)
36
- <<-HTML
37
- <pre class="prettyprint lang-yaml">#{h(pretty_value)}</pre>
38
- HTML
39
- else
40
- ""
41
- end
42
- end
43
-
44
- def prettyprint_references(value)
45
- case value
46
- when Hash
47
- value.reduce({}) do |h, (key, val)|
48
- h.update(key => prettyprint_references(val))
49
- end
50
- when Array
51
- value.map { |val| prettyprint_references(val) }
52
- when ExecutionPlan::OutputReference
53
- value.inspect
54
- else
55
- value
56
- end
57
- end
58
-
59
- def duration_to_s(duration)
60
- h("%0.2fs" % duration)
61
- end
62
-
63
- def load_action(step)
64
- world.persistence.load_action(step)
65
- end
66
-
67
- def step_error(step)
68
- if step.error
69
- ['<pre>',
70
- "#{h(step.error.message)} (#{h(step.error.exception_class)})\n",
71
- h(step.error.backtrace.join("\n")),
72
- '</pre>'].join
73
- end
74
- end
75
-
76
- def show_action_data(label, value)
77
- value_html = prettyprint(value)
78
- if !value_html.empty?
79
- <<-HTML
80
- <p>
81
- <b>#{h(label)}</b>
82
- #{value_html}
83
- </p>
84
- HTML
85
- else
86
- ""
87
- end
88
- end
89
-
90
- def atom_css_classes(atom)
91
- classes = ["atom"]
92
- step = @plan.steps[atom.step_id]
93
- case step.state
94
- when :success
95
- classes << "success"
96
- when :error
97
- classes << "error"
98
- when :skipped, :skipping
99
- classes << "skipped"
100
- end
101
- return classes.join(" ")
102
- end
103
-
104
- def flow_css_classes(flow, sub_flow = nil)
105
- classes = []
106
- case flow
107
- when Flows::Sequence
108
- classes << "sequence"
109
- when Flows::Concurrence
110
- classes << "concurrence"
111
- when Flows::Atom
112
- classes << atom_css_classes(flow)
113
- else
114
- raise "Unknown run plan #{run_plan.inspect}"
115
- end
116
- classes << atom_css_classes(sub_flow) if sub_flow.is_a? Flows::Atom
117
- return classes.join(" ")
118
- end
119
-
120
- def step_css_class(step)
121
- case step.state
122
- when :success
123
- "success"
124
- when :error
125
- "important"
126
- end
127
- end
128
-
129
- def progress_width(step)
130
- if step.state == :error
131
- 100 # we want to show the red bar in full width
132
- else
133
- step.progress_done * 100
134
- end
135
- end
136
-
137
- def step(step_id)
138
- @plan.steps[step_id]
139
- end
140
-
141
- def paginate?
142
- world.persistence.adapter.pagination?
143
- end
144
-
145
- def updated_url(new_params)
146
- url(request.path_info + "?" + Rack::Utils.build_nested_query(params.merge(new_params.stringify_keys)))
147
- end
148
-
149
- def paginated_url(delta)
150
- h(updated_url(page: [0, page + delta].max.to_s))
151
- end
152
-
153
- def pagination_options
154
- if paginate?
155
- { page: page, per_page: per_page }
156
- else
157
- if params[:page] || params[:per_page]
158
- halt 400, "The persistence doesn't support pagination"
159
- end
160
- return {}
161
- end
162
- end
163
-
164
- def page
165
- (params[:page] || 0).to_i
166
- end
167
-
168
- def per_page
169
- (params[:per_page] || 10).to_i
170
- end
171
-
172
- def supported_ordering?(ord_attr)
173
- world.persistence.adapter.ordering_by.any? do |attr|
174
- attr.to_s == ord_attr.to_s
175
- end
176
- end
177
-
178
- def ordering_options
179
- return @ordering_options if @ordering_options
180
-
181
- if params[:order_by]
182
- unless supported_ordering?(params[:order_by])
183
- halt 400, "Unsupported ordering"
184
- end
185
- @ordering_options = { order_by: params[:order_by],
186
- desc: (params[:desc] == 'true') }
187
- elsif supported_ordering?('started_at')
188
- @ordering_options = { order_by: 'started_at', desc: true }
189
- else
190
- @ordering_options = {}
191
- end
192
- return @ordering_options
193
- end
194
-
195
- def order_link(attr, label)
196
- return h(label) unless supported_ordering?(attr)
197
- new_ordering_options = { order_by: attr.to_s,
198
- desc: false }
199
- arrow = ""
200
- if ordering_options[:order_by].to_s == attr.to_s
201
- arrow = ordering_options[:desc] ? "&#9660;" : "&#9650;"
202
- new_ordering_options[:desc] = !ordering_options[:desc]
203
- end
204
- url = updated_url(new_ordering_options)
205
- return %{<a href="#{url}"> #{arrow} #{h(label)}</a>}
206
- end
207
-
208
- def supported_filter?(filter_attr)
209
- world.persistence.adapter.filtering_by.any? do |attr|
210
- attr.to_s == filter_attr.to_s
211
- end
212
- end
213
-
214
- def filtering_options(show_all = false)
215
- return @filtering_options if @filtering_options
216
-
217
- if params[:filters]
218
- params[:filters].map do |key, value|
219
- unless supported_filter?(key)
220
- halt 400, "Unsupported ordering"
221
- end
222
- end
223
-
224
- filters = params[:filters]
225
- elsif supported_filter?('state')
226
- excluded_states = show_all ? [] : ['stopped']
227
- filters = { 'state' => ExecutionPlan.states.map(&:to_s) - excluded_states }
228
- else
229
- filters = {}
230
- end
231
- @filtering_options = { filters: filters }.with_indifferent_access
232
- return @filtering_options
233
- end
234
-
235
- def find_execution_plans_options(show_all = false)
236
- options = HashWithIndifferentAccess.new
237
- options.merge!(filtering_options(show_all))
238
- options.merge!(pagination_options)
239
- options.merge!(ordering_options)
240
- end
241
-
242
- def filter_checkbox(field, values)
243
- out = "<p>#{field}: %s</p>"
244
- checkboxes = values.map do |value|
245
- field_filter = filtering_options[:filters][field]
246
- checked = field_filter && field_filter.include?(value)
247
- %{<input type="checkbox" name="filters[#{field}][]" value="#{value}" #{ "checked" if checked }/>#{value}}
248
- end.join(' ')
249
- out %= checkboxes
250
- return out
251
- end
252
-
253
- end
254
-
255
- get('/') do
256
- options = find_execution_plans_options
257
-
258
- @plans = world.persistence.find_execution_plans(options)
259
- erb :index
260
- end
261
-
262
- get('/:execution_plan_id/actions/:action_id/sub_plans') do |execution_plan_id, action_id|
263
- options = find_execution_plans_options(true)
264
- options[:filters].update('caller_execution_plan_id' => execution_plan_id,
265
- 'caller_action_id' => action_id)
266
- @plans = world.persistence.find_execution_plans(options)
267
- erb :index
268
- end
269
-
270
-
271
- get('/:id') do |id|
272
- @plan = world.persistence.load_execution_plan(id)
273
- @notice = params[:notice]
274
- erb :show
275
- end
276
-
277
- post('/:id/resume') do |id|
278
- plan = world.persistence.load_execution_plan(id)
279
- if plan.state != :paused
280
- redirect(url "/#{plan.id}?notice=#{url_encode('The exeuction has to be paused to be able to resume')}")
281
- else
282
- world.execute(plan.id)
283
- redirect(url "/#{plan.id}?notice=#{url_encode('The execution was resumed')}")
284
- end
285
- end
286
-
287
- post('/:id/skip/:step_id') do |id, step_id|
288
- plan = world.persistence.load_execution_plan(id)
289
- step = plan.steps[step_id.to_i]
290
- if plan.state != :paused
291
- redirect(url "/#{plan.id}?notice=#{url_encode('The exeuction has to be paused to be able to skip')}")
292
- elsif step.state != :error
293
- redirect(url "/#{plan.id}?notice=#{url_encode('The step has to be failed to be able to skip')}")
294
- else
295
- plan.skip(step)
296
- redirect(url "/#{plan.id}")
297
- end
298
- end
299
-
300
- post('/:id/cancel/:step_id') do |id, step_id|
301
- plan = world.persistence.load_execution_plan(id)
302
- step = plan.steps[step_id.to_i]
303
- if step.cancellable?
304
- world.event(plan.id, step.id, Dynflow::Action::Cancellable::Cancel)
305
- redirect(url "/#{plan.id}?notice=#{url_encode('The step was asked to cancel')}")
306
- else
307
- redirect(url "/#{plan.id}?notice=#{url_encode('The step does not support cancelling')}")
308
- end
309
- end
310
-
311
- end
312
- end
5
+ Dynflow::WebConsole = Dynflow::Web
data/lib/dynflow/world.rb CHANGED
@@ -1,41 +1,60 @@
1
+ # -*- coding: utf-8 -*-
1
2
  module Dynflow
2
3
  class World
3
4
  include Algebrick::TypeCheck
5
+ include Algebrick::Matching
4
6
 
5
- attr_reader :executor, :persistence, :transaction_adapter, :action_classes, :subscription_index,
6
- :logger_adapter, :options, :middleware, :auto_rescue
7
-
8
- def initialize(options_hash = {})
9
- @options = default_options.merge options_hash
10
- @logger_adapter = Type! option_val(:logger_adapter), LoggerAdapters::Abstract
11
- @transaction_adapter = Type! option_val(:transaction_adapter), TransactionAdapters::Abstract
12
- persistence_adapter = Type! option_val(:persistence_adapter), PersistenceAdapters::Abstract
13
- @persistence = Persistence.new(self, persistence_adapter)
14
- @executor = Type! option_val(:executor), Executors::Abstract
15
- @action_classes = option_val(:action_classes)
16
- @auto_rescue = option_val(:auto_rescue)
17
- @exit_on_terminate = option_val(:exit_on_terminate)
18
- @middleware = Middleware::World.new
7
+ attr_reader :id, :client_dispatcher, :executor_dispatcher, :executor, :connector,
8
+ :transaction_adapter, :logger_adapter, :coordinator,
9
+ :persistence, :action_classes, :subscription_index,
10
+ :middleware, :auto_rescue, :clock, :meta
11
+
12
+ def initialize(config)
13
+ @id = SecureRandom.uuid
14
+ @clock = spawn_and_wait(Clock, 'clock')
15
+ config_for_world = Config::ForWorld.new(config, self)
16
+ config_for_world.validate
17
+ @logger_adapter = config_for_world.logger_adapter
18
+ @transaction_adapter = config_for_world.transaction_adapter
19
+ @persistence = Persistence.new(self, config_for_world.persistence_adapter)
20
+ @coordinator = Coordinator.new(config_for_world.coordinator_adapter)
21
+ @executor = config_for_world.executor
22
+ @action_classes = config_for_world.action_classes
23
+ @auto_rescue = config_for_world.auto_rescue
24
+ @exit_on_terminate = config_for_world.exit_on_terminate
25
+ @connector = config_for_world.connector
26
+ @middleware = Middleware::World.new
27
+ @client_dispatcher = spawn_and_wait(Dispatcher::ClientDispatcher, "client-dispatcher", self)
28
+ @meta = config_for_world.meta
19
29
  calculate_subscription_index
20
30
 
21
- executor.initialized.wait
31
+ if executor
32
+ @executor_dispatcher = spawn_and_wait(Dispatcher::ExecutorDispatcher, "executor-dispatcher", self)
33
+ executor.initialized.wait
34
+ end
35
+ coordinator.register_world(registered_world)
22
36
  @termination_barrier = Mutex.new
23
- @clock_barrier = Mutex.new
37
+ @before_termination_hooks = Queue.new
24
38
 
25
- transaction_adapter.check self
39
+ if config_for_world.auto_terminate
40
+ at_exit do
41
+ @exit_on_terminate = false # make sure we don't terminate twice
42
+ self.terminate.wait
43
+ end
44
+ end
45
+ self.auto_execute if config_for_world.auto_execute
26
46
  end
27
47
 
28
- def default_options
29
- @default_options ||=
30
- { action_classes: Action.all_children,
31
- logger_adapter: LoggerAdapters::Simple.new,
32
- executor: -> world { Executors::Parallel.new(world, options[:pool_size]) },
33
- exit_on_terminate: true,
34
- auto_rescue: true }
48
+ def before_termination(&block)
49
+ @before_termination_hooks << block
35
50
  end
36
51
 
37
- def clock
38
- @clock_barrier.synchronize { @clock ||= Clock.new(logger) }
52
+ def registered_world
53
+ if executor
54
+ Coordinator::ExecutorWorld.new(self)
55
+ else
56
+ Coordinator::ClientWorld.new(self)
57
+ end
39
58
  end
40
59
 
41
60
  def logger
@@ -52,7 +71,14 @@ module Dynflow
52
71
 
53
72
  # reload actions classes, intended only for devel
54
73
  def reload!
55
- @action_classes.map! { |klass| klass.to_s.constantize }
74
+ # TODO what happens with newly loaded classes
75
+ @action_classes = @action_classes.map do |klass|
76
+ begin
77
+ klass.to_s.constantize
78
+ rescue NameError
79
+ nil # ignore missing classes
80
+ end
81
+ end.compact
56
82
  middleware.clear_cache!
57
83
  calculate_subscription_index
58
84
  end
@@ -60,22 +86,20 @@ module Dynflow
60
86
  TriggerResult = Algebrick.type do
61
87
  # Returned by #trigger when planning fails.
62
88
  PlaningFailed = type { fields! execution_plan_id: String, error: Exception }
63
- # Returned by #trigger when planning is successful but execution fails to start.
64
- ExecutionFailed = type { fields! execution_plan_id: String, error: Exception }
65
89
  # Returned by #trigger when planning is successful, #future will resolve after
66
90
  # ExecutionPlan is executed.
67
- Triggered = type { fields! execution_plan_id: String, future: Future }
91
+ Triggered = type { fields! execution_plan_id: String, future: Concurrent::Edge::Future }
68
92
 
69
- variants PlaningFailed, ExecutionFailed, Triggered
93
+ variants PlaningFailed, Triggered
70
94
  end
71
95
 
72
96
  module TriggerResult
73
97
  def planned?
74
- match self, PlaningFailed => false, ExecutionFailed => true, Triggered => true
98
+ match self, PlaningFailed => false, Triggered => true
75
99
  end
76
100
 
77
101
  def triggered?
78
- match self, PlaningFailed => false, ExecutionFailed => false, Triggered => true
102
+ match self, PlaningFailed => false, Triggered => true
79
103
  end
80
104
 
81
105
  def id
@@ -102,27 +126,13 @@ module Dynflow
102
126
  planned = execution_plan.state == :planned
103
127
 
104
128
  if planned
105
- begin
106
- Triggered[execution_plan.id, execute(execution_plan.id)]
107
- rescue => exception
108
- ExecutionFailed[execution_plan.id, exception]
109
- end
129
+ done = execute(execution_plan.id, Concurrent.future)
130
+ Triggered[execution_plan.id, done]
110
131
  else
111
132
  PlaningFailed[execution_plan.id, execution_plan.errors.first.exception]
112
133
  end
113
134
  end
114
135
 
115
- def event(execution_plan_id, step_id, event, future = Future.new)
116
- # we do this to avoid unresolved future when getting into
117
- # the executor mailbox right at the termination.
118
- # TODO: concurrent-ruby dead letter routing should make this
119
- # more elegant
120
- raise Dynflow::Error, "terminating world is not accepting events" if terminating?
121
- executor.event execution_plan_id, step_id, event, future
122
- rescue => e
123
- future.fail e
124
- end
125
-
126
136
  def plan(action_class, *args)
127
137
  ExecutionPlan.new(self).tap do |execution_plan|
128
138
  execution_plan.prepare(action_class)
@@ -137,84 +147,134 @@ module Dynflow
137
147
  end
138
148
  end
139
149
 
140
- # @return [Future] containing execution_plan when finished
150
+ # @return [Concurrent::Edge::Future] containing execution_plan when finished
141
151
  # raises when ExecutionPlan is not accepted for execution
142
- def execute(execution_plan_id, finished = Future.new)
143
- executor.execute execution_plan_id, finished
152
+ def execute(execution_plan_id, done = Concurrent.future)
153
+ publish_request(Dispatcher::Execution[execution_plan_id], done, true)
144
154
  end
145
155
 
146
- def terminate(future = Future.new)
156
+ def event(execution_plan_id, step_id, event, done = Concurrent.future)
157
+ publish_request(Dispatcher::Event[execution_plan_id, step_id, event], done, false)
158
+ end
159
+
160
+ def ping(world_id, timeout, done = Concurrent.future)
161
+ publish_request(Dispatcher::Ping[world_id], done, false, timeout)
162
+ end
163
+
164
+ def publish_request(request, done, wait_for_accepted, timeout = nil)
165
+ accepted = Concurrent.future
166
+ accepted.rescue do |reason|
167
+ done.fail reason if reason
168
+ end
169
+ client_dispatcher.ask([:publish_request, done, request, timeout], accepted)
170
+ accepted.wait if wait_for_accepted
171
+ done
172
+ rescue => e
173
+ accepted.fail e
174
+ end
175
+
176
+ def terminate(future = Concurrent.future)
147
177
  @termination_barrier.synchronize do
148
- if @executor_terminated.nil?
149
- @executor_terminated = Future.new
150
- @clock_terminated = Future.new
151
- executor.terminate(@executor_terminated).
152
- do_then { clock.ask(MicroActor::Terminate, @clock_terminated) }
153
- if @exit_on_terminate
154
- future.do_then { Kernel.exit }
178
+ @terminated ||= Concurrent.future do
179
+ begin
180
+ run_before_termination_hooks
181
+
182
+
183
+ if executor
184
+ connector.stop_receiving_new_work(self)
185
+
186
+ logger.info "start terminating executor..."
187
+ executor.terminate.wait
188
+
189
+ logger.info "start terminating executor dispatcher..."
190
+ executor_dispatcher_terminated = Concurrent.future
191
+ executor_dispatcher.ask([:start_termination, executor_dispatcher_terminated])
192
+ executor_dispatcher_terminated.wait
193
+ end
194
+
195
+ logger.info "start terminating client dispatcher..."
196
+ client_dispatcher_terminated = Concurrent.future
197
+ client_dispatcher.ask([:start_termination, client_dispatcher_terminated])
198
+ client_dispatcher_terminated.wait
199
+
200
+ logger.info "stop listening for new events..."
201
+ connector.stop_listening(self)
202
+
203
+ if @clock
204
+ logger.info "start terminating clock..."
205
+ clock.ask(:terminate!).wait
206
+ end
207
+
208
+ coordinator.release_by_owner("world:#{registered_world.id}")
209
+ coordinator.delete_world(registered_world)
210
+ true
211
+ rescue => e
212
+ logger.fatal(e)
155
213
  end
214
+ end.on_completion do
215
+ Kernel.exit if @exit_on_terminate
156
216
  end
157
217
  end
158
- Future.join([@executor_terminated, @clock_terminated], future)
218
+
219
+ @terminated.tangle(future)
220
+ future
159
221
  end
160
222
 
161
223
  def terminating?
162
- !!@executor_terminated
163
- end
164
-
165
- # Detects execution plans that are marked as running but no executor
166
- # handles them (probably result of non-standard executor termination)
167
- #
168
- # The current implementation expects no execution_plan being actually run
169
- # by the executor.
170
- #
171
- # TODO: persist the running executors in the system, so that we can detect
172
- # the orphaned execution plans. The register should be managable by the
173
- # console, so that the administrator can unregister dead executors when needed.
174
- # After the executor is unregistered, the consistency check should be performed
175
- # to fix the orphaned plans as well.
176
- def consistency_check
177
- abnormal_execution_plans =
178
- self.persistence.find_execution_plans filters: { 'state' => %w(planning running) }
179
- if abnormal_execution_plans.empty?
180
- logger.info 'Clean start.'
181
- else
182
- format_str = '%36s %10s %10s'
183
- message = ['Abnormal execution plans, process was probably killed.',
184
- 'Following ExecutionPlans will be set to paused, ',
185
- 'it should be fixed manually by administrator.',
186
- (format format_str, 'ExecutionPlan', 'state', 'result'),
187
- *(abnormal_execution_plans.map do |ep|
188
- format format_str, ep.id, ep.state, ep.result
189
- end)]
190
-
191
- logger.error message.join("\n")
192
-
193
- abnormal_execution_plans.each do |ep|
194
- ep.update_state case ep.state
195
- when :planning
196
- :stopped
197
- when :running
198
- :paused
199
- else
200
- raise
201
- end
202
- ep.steps.values.each do |step|
203
- if [:suspended, :running].include?(step.state)
204
- step.error = ExecutionPlan::Steps::Error.new("Abnormal termination (previous state: #{step.state})")
205
- step.state = :error
206
- step.save
207
- end
208
- end
224
+ defined?(@terminated)
225
+ end
226
+
227
+ # Invalidate another world, that left some data in the runtime,
228
+ # but it's not really running
229
+ def invalidate(world)
230
+ Type! world, Coordinator::ClientWorld, Coordinator::ExecutorWorld
231
+ coordinator.acquire(Coordinator::WorldInvalidationLock.new(self, world)) do
232
+ old_execution_locks = coordinator.find_locks(class: Coordinator::ExecutionLock.name,
233
+ owner_id: "world:#{world.id}")
234
+
235
+ coordinator.deactivate_world(world)
236
+
237
+ old_execution_locks.each do |execution_lock|
238
+ invalidate_execution_lock(execution_lock)
209
239
  end
240
+
241
+ coordinator.delete_world(world)
210
242
  end
211
243
  end
212
244
 
213
- # should be called after World is initialized, SimpleWorld does it automatically
214
- def execute_planned_execution_plans
215
- planned_execution_plans =
216
- self.persistence.find_execution_plans filters: { 'state' => %w(planned) }
217
- planned_execution_plans.each { |ep| execute ep.id }
245
+ def invalidate_execution_lock(execution_lock)
246
+ plan = persistence.load_execution_plan(execution_lock.execution_plan_id)
247
+ plan.execution_history.add('terminate execution', execution_lock.world_id)
248
+
249
+ plan.steps.values.each do |step|
250
+ if step.state == :running
251
+ step.error = ExecutionPlan::Steps::Error.new("Abnormal termination (previous state: #{step.state})")
252
+ step.state = :error
253
+ step.save
254
+ end
255
+ end
256
+
257
+ plan.update_state(:paused) unless plan.state == :paused
258
+ plan.save
259
+ coordinator.release(execution_lock)
260
+ unless plan.error?
261
+ client_dispatcher.tell([:dispatch_request,
262
+ Dispatcher::Execution[execution_lock.execution_plan_id],
263
+ execution_lock.client_world_id,
264
+ execution_lock.request_id])
265
+ end
266
+ rescue Errors::PersistenceError
267
+ logger.error "failed to write data while invalidating execution lock #{execution_lock}"
268
+ end
269
+
270
+ # executes plans that are planned/paused and haven't reported any error yet (usually when no executor
271
+ # was available by the time of planning or terminating)
272
+ def auto_execute
273
+ coordinator.acquire(Coordinator::AutoExecuteLock.new(self)) do
274
+ planned_execution_plans =
275
+ self.persistence.find_execution_plans filters: { 'state' => %w(planned paused), 'result' => 'pending' }
276
+ planned_execution_plans.each { |ep| execute ep.id }
277
+ end
218
278
  end
219
279
 
220
280
  private
@@ -229,13 +289,22 @@ module Dynflow
229
289
  end.tap { |o| o.freeze }
230
290
  end
231
291
 
232
- def option_val(key)
233
- val = options.fetch(key)
234
- if val.is_a? Proc
235
- options[key] = val.call(self)
236
- else
237
- val
292
+ def run_before_termination_hooks
293
+ until @before_termination_hooks.empty?
294
+ begin
295
+ @before_termination_hooks.pop.call
296
+ rescue => e
297
+ logger.error e
298
+ end
238
299
  end
239
300
  end
301
+
302
+ def spawn_and_wait(klass, name, *args)
303
+ initialized = Concurrent.future
304
+ actor = klass.spawn(name: name, args: args, initialized: initialized)
305
+ initialized.wait
306
+ return actor
307
+ end
308
+
240
309
  end
241
310
  end