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.
- data/.gitignore +2 -0
- data/.travis.yml +16 -1
- data/Gemfile +13 -1
- data/doc/pages/source/_drafts/2015-03-01-new-documentation.markdown +10 -0
- data/doc/pages/source/_includes/menu.html +1 -0
- data/doc/pages/source/_includes/menu_right.html +1 -1
- data/doc/pages/source/_sass/_bootstrap-variables.sass +1 -0
- data/doc/pages/source/_sass/_style.scss +4 -0
- data/doc/pages/source/blog/index.html +12 -0
- data/doc/pages/source/documentation/index.md +330 -5
- data/dynflow.gemspec +3 -1
- data/examples/example_helper.rb +18 -11
- data/examples/orchestrate_evented.rb +2 -1
- data/examples/remote_executor.rb +53 -20
- data/lib/dynflow.rb +16 -6
- data/lib/dynflow/action/suspended.rb +1 -1
- data/lib/dynflow/action/with_sub_plans.rb +3 -6
- data/lib/dynflow/actor.rb +56 -0
- data/lib/dynflow/clock.rb +43 -38
- data/lib/dynflow/config.rb +107 -0
- data/lib/dynflow/connectors.rb +7 -0
- data/lib/dynflow/connectors/abstract.rb +41 -0
- data/lib/dynflow/connectors/database.rb +175 -0
- data/lib/dynflow/connectors/direct.rb +71 -0
- data/lib/dynflow/coordinator.rb +280 -0
- data/lib/dynflow/coordinator_adapters.rb +8 -0
- data/lib/dynflow/coordinator_adapters/abstract.rb +28 -0
- data/lib/dynflow/coordinator_adapters/sequel.rb +29 -0
- data/lib/dynflow/dispatcher.rb +58 -0
- data/lib/dynflow/dispatcher/abstract.rb +14 -0
- data/lib/dynflow/dispatcher/client_dispatcher.rb +139 -0
- data/lib/dynflow/dispatcher/executor_dispatcher.rb +86 -0
- data/lib/dynflow/errors.rb +7 -1
- data/lib/dynflow/execution_history.rb +46 -0
- data/lib/dynflow/execution_plan.rb +19 -15
- data/lib/dynflow/executors.rb +0 -1
- data/lib/dynflow/executors/abstract.rb +5 -10
- data/lib/dynflow/executors/parallel.rb +16 -13
- data/lib/dynflow/executors/parallel/core.rb +76 -78
- data/lib/dynflow/executors/parallel/execution_plan_manager.rb +4 -5
- data/lib/dynflow/executors/parallel/pool.rb +22 -52
- data/lib/dynflow/executors/parallel/running_steps_manager.rb +9 -2
- data/lib/dynflow/executors/parallel/worker.rb +5 -10
- data/lib/dynflow/persistence.rb +14 -0
- data/lib/dynflow/persistence_adapters/abstract.rb +14 -3
- data/lib/dynflow/persistence_adapters/sequel.rb +142 -38
- data/lib/dynflow/persistence_adapters/sequel_migrations/004_coordinator_records.rb +14 -0
- data/lib/dynflow/persistence_adapters/sequel_migrations/005_envelopes.rb +14 -0
- data/lib/dynflow/round_robin.rb +37 -0
- data/lib/dynflow/serializable.rb +1 -2
- data/lib/dynflow/serializer.rb +46 -0
- data/lib/dynflow/testing/dummy_executor.rb +2 -2
- data/lib/dynflow/testing/dummy_world.rb +1 -1
- data/lib/dynflow/transaction_adapters/abstract.rb +0 -5
- data/lib/dynflow/transaction_adapters/active_record.rb +0 -10
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web.rb +26 -0
- data/lib/dynflow/web/console.rb +108 -0
- data/lib/dynflow/web/console_helpers.rb +158 -0
- data/lib/dynflow/web/filtering_helpers.rb +85 -0
- data/lib/dynflow/web/world_helpers.rb +9 -0
- data/lib/dynflow/web_console.rb +3 -310
- data/lib/dynflow/world.rb +188 -119
- data/test/abnormal_states_recovery_test.rb +152 -0
- data/test/action_test.rb +2 -3
- data/test/clock_test.rb +1 -5
- data/test/coordinator_test.rb +152 -0
- data/test/dispatcher_test.rb +146 -0
- data/test/execution_plan_test.rb +2 -1
- data/test/executor_test.rb +534 -612
- data/test/middleware_test.rb +4 -4
- data/test/persistence_test.rb +17 -0
- data/test/prepare_travis_env.sh +35 -0
- data/test/rescue_test.rb +5 -3
- data/test/round_robin_test.rb +28 -0
- data/test/support/code_workflow_example.rb +0 -73
- data/test/support/dummy_example.rb +130 -0
- data/test/support/test_execution_log.rb +41 -0
- data/test/test_helper.rb +222 -116
- data/test/testing_test.rb +10 -10
- data/test/web_console_test.rb +3 -3
- data/test/world_test.rb +23 -0
- data/web/assets/images/logo-square.png +0 -0
- data/web/assets/stylesheets/application.css +9 -0
- data/web/assets/vendor/bootstrap/config.json +429 -0
- data/web/assets/vendor/bootstrap/css/bootstrap-theme.css +479 -0
- data/web/assets/vendor/bootstrap/css/bootstrap-theme.min.css +10 -0
- data/web/assets/vendor/bootstrap/css/bootstrap.css +5377 -4980
- data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -8
- data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
- data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
- data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
- data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
- data/web/assets/vendor/bootstrap/js/bootstrap.js +1674 -1645
- data/web/assets/vendor/bootstrap/js/bootstrap.min.js +11 -5
- data/web/views/execution_history.erb +17 -0
- data/web/views/index.erb +4 -6
- data/web/views/layout.erb +44 -8
- data/web/views/show.erb +4 -5
- data/web/views/worlds.erb +26 -0
- metadata +116 -23
- checksums.yaml +0 -15
- data/lib/dynflow/daemon.rb +0 -30
- data/lib/dynflow/executors/remote_via_socket.rb +0 -43
- data/lib/dynflow/executors/remote_via_socket/core.rb +0 -184
- data/lib/dynflow/future.rb +0 -173
- data/lib/dynflow/listeners.rb +0 -7
- data/lib/dynflow/listeners/abstract.rb +0 -17
- data/lib/dynflow/listeners/serialization.rb +0 -77
- data/lib/dynflow/listeners/socket.rb +0 -117
- data/lib/dynflow/micro_actor.rb +0 -102
- data/lib/dynflow/simple_world.rb +0 -19
- data/test/remote_via_socket_test.rb +0 -170
- data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +0 -1109
- data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +0 -9
- data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
- data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
data/lib/dynflow/web_console.rb
CHANGED
|
@@ -1,312 +1,5 @@
|
|
|
1
|
-
require 'dynflow'
|
|
2
|
-
require 'pp'
|
|
3
|
-
require 'sinatra/base'
|
|
4
|
-
require 'yaml'
|
|
1
|
+
require 'dynflow/web'
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
class WebConsole < Sinatra::Base
|
|
3
|
+
warn %{"require 'dynflow/web_console'" is deprecated, use "require 'dynflow/web'" instead}
|
|
8
4
|
|
|
9
|
-
|
|
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] ? "▼" : "▲"
|
|
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 :
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@
|
|
16
|
-
@
|
|
17
|
-
@
|
|
18
|
-
@
|
|
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
|
|
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
|
-
@
|
|
37
|
+
@before_termination_hooks = Queue.new
|
|
24
38
|
|
|
25
|
-
|
|
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
|
|
29
|
-
@
|
|
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
|
|
38
|
-
|
|
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
|
-
|
|
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,
|
|
93
|
+
variants PlaningFailed, Triggered
|
|
70
94
|
end
|
|
71
95
|
|
|
72
96
|
module TriggerResult
|
|
73
97
|
def planned?
|
|
74
|
-
match self, PlaningFailed => false,
|
|
98
|
+
match self, PlaningFailed => false, Triggered => true
|
|
75
99
|
end
|
|
76
100
|
|
|
77
101
|
def triggered?
|
|
78
|
-
match self, PlaningFailed => false,
|
|
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
|
-
|
|
106
|
-
|
|
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,
|
|
143
|
-
|
|
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
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
218
|
+
|
|
219
|
+
@terminated.tangle(future)
|
|
220
|
+
future
|
|
159
221
|
end
|
|
160
222
|
|
|
161
223
|
def terminating?
|
|
162
|
-
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|