dynflow 1.9.3 → 2.0.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.
- checksums.yaml +4 -4
- data/.github/workflows/bats.yml +50 -0
- data/.github/workflows/release.yml +1 -1
- data/.github/workflows/ruby.yml +27 -46
- data/.gitignore +2 -0
- data/.rubocop.yml +3 -0
- data/Dockerfile +1 -1
- data/Gemfile +7 -8
- data/README.md +4 -4
- data/doc/pages/source/documentation/index.md +4 -4
- data/dynflow.gemspec +2 -3
- data/examples/example_helper.rb +1 -1
- data/examples/execution_plan_chaining.rb +56 -0
- data/examples/remote_executor.rb +5 -8
- data/lib/dynflow/action/format.rb +4 -33
- data/lib/dynflow/debug/telemetry/persistence.rb +1 -1
- data/lib/dynflow/delayed_executors/abstract_core.rb +1 -1
- data/lib/dynflow/delayed_plan.rb +6 -0
- data/lib/dynflow/director.rb +9 -1
- data/lib/dynflow/executors/sidekiq/core.rb +1 -1
- data/lib/dynflow/executors/sidekiq/redis_locking.rb +10 -3
- data/lib/dynflow/extensions/msgpack.rb +4 -0
- data/lib/dynflow/persistence.rb +14 -2
- data/lib/dynflow/persistence_adapters/abstract.rb +9 -1
- data/lib/dynflow/persistence_adapters/sequel.rb +91 -48
- data/lib/dynflow/persistence_adapters/sequel_migrations/025_create_execution_plan_dependencies.rb +22 -0
- data/lib/dynflow/rails/daemon.rb +16 -7
- data/lib/dynflow/testing.rb +1 -1
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/world.rb +34 -13
- data/lib/dynflow.rb +0 -1
- data/test/action_test.rb +3 -3
- data/test/bats/helpers/common.bash +67 -0
- data/test/bats/helpers/containers.bash +146 -0
- data/test/bats/setup_suite.bash +46 -0
- data/test/bats/sidekiq-orchestrator.bats +178 -0
- data/test/bats/teardown_suite.bash +16 -0
- data/test/concurrency_control_test.rb +0 -1
- data/test/daemon_test.rb +21 -2
- data/test/extensions_test.rb +3 -3
- data/test/future_execution_test.rb +150 -3
- data/test/persistence_test.rb +70 -3
- data/test/support/dummy_example.rb +4 -0
- data/test/test_helper.rb +19 -4
- data/web/views/show.erb +24 -0
- metadata +15 -17
- data/.github/install_dependencies.sh +0 -35
|
@@ -39,7 +39,8 @@ module Dynflow
|
|
|
39
39
|
envelope: %w(receiver_id),
|
|
40
40
|
coordinator_record: %w(id owner_id class),
|
|
41
41
|
delayed: %w(execution_plan_uuid start_at start_before args_serializer frozen),
|
|
42
|
-
output_chunk: %w(execution_plan_uuid action_id kind timestamp)
|
|
42
|
+
output_chunk: %w(execution_plan_uuid action_id kind timestamp),
|
|
43
|
+
execution_plan_dependency: %w(execution_plan_uuid blocked_by_uuid) }
|
|
43
44
|
|
|
44
45
|
SERIALIZABLE_COLUMNS = { action: %w(input output),
|
|
45
46
|
delayed: %w(serialized_args),
|
|
@@ -71,20 +72,23 @@ module Dynflow
|
|
|
71
72
|
paginate(table(table_name), options),
|
|
72
73
|
options),
|
|
73
74
|
options[:filters])
|
|
74
|
-
|
|
75
|
+
records = with_retry { data_set.all }
|
|
76
|
+
records.map { |record| execution_plan_column_map(load_data(record, table_name)) }
|
|
75
77
|
end
|
|
76
78
|
|
|
77
79
|
def find_execution_plan_counts(options = {})
|
|
78
|
-
filter(:execution_plan, table(:execution_plan), options[:filters]).count
|
|
80
|
+
with_retry { filter(:execution_plan, table(:execution_plan), options[:filters]).count }
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
def find_execution_plan_counts_after(timestamp, options = {})
|
|
82
|
-
filter(:execution_plan, table(:execution_plan), options[:filters]).filter(::Sequel.lit('ended_at >= ?', timestamp)).count
|
|
84
|
+
with_retry { filter(:execution_plan, table(:execution_plan), options[:filters]).filter(::Sequel.lit('ended_at >= ?', timestamp)).count }
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
def find_execution_plan_statuses(options)
|
|
86
|
-
plans =
|
|
87
|
-
|
|
88
|
+
plans = with_retry do
|
|
89
|
+
filter(:execution_plan, table(:execution_plan), options[:filters])
|
|
90
|
+
.select(:uuid, :state, :result)
|
|
91
|
+
end
|
|
88
92
|
|
|
89
93
|
plans.each_with_object({}) do |current, acc|
|
|
90
94
|
uuid = current.delete(:uuid)
|
|
@@ -94,27 +98,29 @@ module Dynflow
|
|
|
94
98
|
|
|
95
99
|
def delete_execution_plans(filters, batch_size = 1000, backup_dir = nil)
|
|
96
100
|
count = 0
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
with_retry do
|
|
102
|
+
filter(:execution_plan, table(:execution_plan), filters).each_slice(batch_size) do |plans|
|
|
103
|
+
uuids = plans.map { |p| p.fetch(:uuid) }
|
|
104
|
+
@db.transaction do
|
|
105
|
+
table(:delayed).where(execution_plan_uuid: uuids).delete
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
steps = table(:step).where(execution_plan_uuid: uuids)
|
|
108
|
+
backup_to_csv(:step, steps, backup_dir, 'steps.csv') if backup_dir
|
|
109
|
+
steps.delete
|
|
105
110
|
|
|
106
|
-
|
|
111
|
+
output_chunks = table(:output_chunk).where(execution_plan_uuid: uuids).delete
|
|
107
112
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
actions = table(:action).where(execution_plan_uuid: uuids)
|
|
114
|
+
backup_to_csv(:action, actions, backup_dir, 'actions.csv') if backup_dir
|
|
115
|
+
actions.delete
|
|
111
116
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
execution_plans = table(:execution_plan).where(uuid: uuids)
|
|
118
|
+
backup_to_csv(:execution_plan, execution_plans, backup_dir, 'execution_plans.csv') if backup_dir
|
|
119
|
+
count += execution_plans.delete
|
|
120
|
+
end
|
|
115
121
|
end
|
|
122
|
+
return count
|
|
116
123
|
end
|
|
117
|
-
return count
|
|
118
124
|
end
|
|
119
125
|
|
|
120
126
|
def load_execution_plan(execution_plan_id)
|
|
@@ -127,10 +133,12 @@ module Dynflow
|
|
|
127
133
|
|
|
128
134
|
def delete_delayed_plans(filters, batch_size = 1000)
|
|
129
135
|
count = 0
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
with_retry do
|
|
137
|
+
filter(:delayed, table(:delayed), filters).each_slice(batch_size) do |plans|
|
|
138
|
+
uuids = plans.map { |p| p.fetch(:execution_plan_uuid) }
|
|
139
|
+
@db.transaction do
|
|
140
|
+
count += table(:delayed).where(execution_plan_uuid: uuids).delete
|
|
141
|
+
end
|
|
134
142
|
end
|
|
135
143
|
end
|
|
136
144
|
count
|
|
@@ -138,19 +146,43 @@ module Dynflow
|
|
|
138
146
|
|
|
139
147
|
def find_old_execution_plans(age)
|
|
140
148
|
table_name = :execution_plan
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
records = with_retry do
|
|
150
|
+
table(table_name)
|
|
151
|
+
.where(::Sequel.lit('ended_at <= ? AND state = ?', age, 'stopped'))
|
|
152
|
+
.all
|
|
153
|
+
end
|
|
154
|
+
records.map { |plan| execution_plan_column_map(load_data plan, table_name) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def find_execution_plan_dependencies(execution_plan_id)
|
|
158
|
+
table(:execution_plan_dependency)
|
|
159
|
+
.where(execution_plan_uuid: execution_plan_id)
|
|
160
|
+
.select_map(:blocked_by_uuid)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def find_blocked_execution_plans(execution_plan_id)
|
|
164
|
+
table(:execution_plan_dependency)
|
|
165
|
+
.where(blocked_by_uuid: execution_plan_id)
|
|
166
|
+
.select_map(:execution_plan_uuid)
|
|
144
167
|
end
|
|
145
168
|
|
|
146
|
-
def
|
|
169
|
+
def find_ready_delayed_plans(time)
|
|
147
170
|
table_name = :delayed
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
171
|
+
# Subquery to find delayed plans that have at least one non-stopped dependency
|
|
172
|
+
plans_with_unfinished_deps = table(:execution_plan_dependency)
|
|
173
|
+
.join(TABLES[:execution_plan], uuid: :blocked_by_uuid)
|
|
174
|
+
.where(::Sequel.~(state: 'stopped'))
|
|
175
|
+
.select(:execution_plan_uuid)
|
|
176
|
+
|
|
177
|
+
records = with_retry do
|
|
178
|
+
table(table_name)
|
|
179
|
+
.where(::Sequel.lit('start_at IS NULL OR (start_at <= ? OR (start_before IS NOT NULL AND start_before <= ?))', time, time))
|
|
180
|
+
.where(:frozen => false)
|
|
181
|
+
.exclude(execution_plan_uuid: plans_with_unfinished_deps)
|
|
182
|
+
.order_by(:start_at)
|
|
183
|
+
.all
|
|
184
|
+
end
|
|
185
|
+
records.map { |plan| load_data(plan, table_name) }
|
|
154
186
|
end
|
|
155
187
|
|
|
156
188
|
def load_delayed_plan(execution_plan_id)
|
|
@@ -163,6 +195,10 @@ module Dynflow
|
|
|
163
195
|
save :delayed, { execution_plan_uuid: execution_plan_id }, value, with_data: false
|
|
164
196
|
end
|
|
165
197
|
|
|
198
|
+
def chain_execution_plan(first, second)
|
|
199
|
+
save :execution_plan_dependency, {}, { execution_plan_uuid: second, blocked_by_uuid: first }, with_data: false
|
|
200
|
+
end
|
|
201
|
+
|
|
166
202
|
def load_step(execution_plan_id, step_id)
|
|
167
203
|
load :step, execution_plan_uuid: execution_plan_id, id: step_id
|
|
168
204
|
end
|
|
@@ -205,7 +241,9 @@ module Dynflow
|
|
|
205
241
|
end
|
|
206
242
|
|
|
207
243
|
def delete_output_chunks(execution_plan_id, action_id)
|
|
208
|
-
|
|
244
|
+
with_retry do
|
|
245
|
+
filter(:output_chunk, table(:output_chunk), { execution_plan_uuid: execution_plan_id, action_id: action_id }).delete
|
|
246
|
+
end
|
|
209
247
|
end
|
|
210
248
|
|
|
211
249
|
def connector_feature!
|
|
@@ -221,28 +259,30 @@ module Dynflow
|
|
|
221
259
|
|
|
222
260
|
def pull_envelopes(receiver_id)
|
|
223
261
|
connector_feature!
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
262
|
+
with_retry do
|
|
263
|
+
db.transaction do
|
|
264
|
+
data_set = table(:envelope).where(receiver_id: receiver_id).all
|
|
265
|
+
envelopes = data_set.map { |record| load_data(record) }
|
|
227
266
|
|
|
228
|
-
|
|
229
|
-
|
|
267
|
+
table(:envelope).where(id: data_set.map { |d| d[:id] }).delete
|
|
268
|
+
return envelopes
|
|
269
|
+
end
|
|
230
270
|
end
|
|
231
271
|
end
|
|
232
272
|
|
|
233
273
|
def push_envelope(envelope)
|
|
234
274
|
connector_feature!
|
|
235
|
-
table(:envelope).insert(prepare_record(:envelope, envelope))
|
|
275
|
+
with_retry { table(:envelope).insert(prepare_record(:envelope, envelope)) }
|
|
236
276
|
end
|
|
237
277
|
|
|
238
278
|
def prune_envelopes(receiver_ids)
|
|
239
279
|
connector_feature!
|
|
240
|
-
table(:envelope).where(receiver_id: receiver_ids).delete
|
|
280
|
+
with_retry { table(:envelope).where(receiver_id: receiver_ids).delete }
|
|
241
281
|
end
|
|
242
282
|
|
|
243
283
|
def prune_undeliverable_envelopes
|
|
244
284
|
connector_feature!
|
|
245
|
-
table(:envelope).where(receiver_id: table(:coordinator_record).select(:id)).invert.delete
|
|
285
|
+
with_retry { table(:envelope).where(receiver_id: table(:coordinator_record).select(:id)).invert.delete }
|
|
246
286
|
end
|
|
247
287
|
|
|
248
288
|
def coordinator_feature!
|
|
@@ -263,7 +303,7 @@ module Dynflow
|
|
|
263
303
|
|
|
264
304
|
def delete_coordinator_record(class_name, record_id)
|
|
265
305
|
coordinator_feature!
|
|
266
|
-
table(:coordinator_record).where(class: class_name, id: record_id).delete
|
|
306
|
+
with_retry { table(:coordinator_record).where(class: class_name, id: record_id).delete }
|
|
267
307
|
end
|
|
268
308
|
|
|
269
309
|
def find_coordinator_records(options)
|
|
@@ -275,7 +315,9 @@ module Dynflow
|
|
|
275
315
|
if exclude_owner_id
|
|
276
316
|
data_set = data_set.exclude(:owner_id => exclude_owner_id)
|
|
277
317
|
end
|
|
278
|
-
|
|
318
|
+
with_retry do
|
|
319
|
+
data_set.all.map { |record| load_data(record) }
|
|
320
|
+
end
|
|
279
321
|
end
|
|
280
322
|
|
|
281
323
|
def to_hash
|
|
@@ -301,7 +343,8 @@ module Dynflow
|
|
|
301
343
|
envelope: :dynflow_envelopes,
|
|
302
344
|
coordinator_record: :dynflow_coordinator_records,
|
|
303
345
|
delayed: :dynflow_delayed_plans,
|
|
304
|
-
output_chunk: :dynflow_output_chunks
|
|
346
|
+
output_chunk: :dynflow_output_chunks,
|
|
347
|
+
execution_plan_dependency: :dynflow_execution_plan_dependencies }
|
|
305
348
|
|
|
306
349
|
def table(which)
|
|
307
350
|
db[TABLES.fetch(which)]
|
|
@@ -444,7 +487,7 @@ module Dynflow
|
|
|
444
487
|
end
|
|
445
488
|
|
|
446
489
|
def delete(what, condition)
|
|
447
|
-
table(what).where(Utils.symbolize_keys(condition)).delete
|
|
490
|
+
with_retry { table(what).where(Utils.symbolize_keys(condition)).delete }
|
|
448
491
|
end
|
|
449
492
|
|
|
450
493
|
def extract_metadata(what, value)
|
data/lib/dynflow/persistence_adapters/sequel_migrations/025_create_execution_plan_dependencies.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do
|
|
4
|
+
up do
|
|
5
|
+
type = database_type
|
|
6
|
+
create_table(:dynflow_execution_plan_dependencies) do
|
|
7
|
+
column_properties = if type.to_s.include?('postgres')
|
|
8
|
+
{ type: :uuid }
|
|
9
|
+
else
|
|
10
|
+
{ type: String, size: 36, fixed: true, null: false }
|
|
11
|
+
end
|
|
12
|
+
foreign_key :execution_plan_uuid, :dynflow_execution_plans, on_delete: :cascade, **column_properties
|
|
13
|
+
foreign_key :blocked_by_uuid, :dynflow_execution_plans, on_delete: :cascade, **column_properties
|
|
14
|
+
index :blocked_by_uuid
|
|
15
|
+
index :execution_plan_uuid
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
down do
|
|
20
|
+
drop_table(:dynflow_execution_plan_dependencies)
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/dynflow/rails/daemon.rb
CHANGED
|
@@ -23,23 +23,32 @@ module Dynflow
|
|
|
23
23
|
@daemons_class || ::Daemons
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
def stdout
|
|
27
|
+
STDOUT
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def stderr
|
|
31
|
+
STDERR
|
|
32
|
+
end
|
|
33
|
+
|
|
26
34
|
# Load the Rails environment and initialize the executor in this thread.
|
|
27
35
|
def run(rails_root = Dir.pwd, options = {})
|
|
28
|
-
|
|
36
|
+
stdout.puts('Starting Rails environment')
|
|
29
37
|
rails_env_file = File.expand_path('./config/environment.rb', rails_root)
|
|
30
38
|
unless File.exist?(rails_env_file)
|
|
31
39
|
raise "#{rails_root} doesn't seem to be a Rails root directory"
|
|
32
40
|
end
|
|
33
41
|
|
|
34
|
-
|
|
42
|
+
stderr.puts("Starting dynflow with the following options: #{options}")
|
|
35
43
|
|
|
36
44
|
::Rails.application.dynflow.executor!
|
|
37
45
|
|
|
38
46
|
if options[:memory_limit] && options[:memory_limit].to_i > 0
|
|
39
47
|
::Rails.application.dynflow.config.on_init do |world|
|
|
48
|
+
stdout_cap = stdout
|
|
40
49
|
memory_watcher = initialize_memory_watcher(world, options[:memory_limit], options)
|
|
41
50
|
world.terminated.on_resolution do
|
|
42
|
-
|
|
51
|
+
stdout_cap.puts("World has been terminated")
|
|
43
52
|
memory_watcher = nil # the object can be disposed
|
|
44
53
|
end
|
|
45
54
|
end
|
|
@@ -48,10 +57,10 @@ module Dynflow
|
|
|
48
57
|
require rails_env_file
|
|
49
58
|
::Rails.application.dynflow.initialize!
|
|
50
59
|
world_id = ::Rails.application.dynflow.world.id
|
|
51
|
-
|
|
60
|
+
stdout.puts("Everything ready for world: #{world_id}")
|
|
52
61
|
sleep
|
|
53
62
|
ensure
|
|
54
|
-
|
|
63
|
+
stdout.puts('Exiting')
|
|
55
64
|
end
|
|
56
65
|
|
|
57
66
|
# run the executor as a daemon
|
|
@@ -68,7 +77,7 @@ module Dynflow
|
|
|
68
77
|
raise "Command exptected to be 'start', 'stop', 'restart', 'run', was #{command.inspect}"
|
|
69
78
|
end
|
|
70
79
|
|
|
71
|
-
|
|
80
|
+
stdout.puts("Dynflow Executor: #{command} in progress")
|
|
72
81
|
|
|
73
82
|
options[:executors_count].times do
|
|
74
83
|
daemons_class.run_proc(
|
|
@@ -79,7 +88,7 @@ module Dynflow
|
|
|
79
88
|
::Logging.reopen
|
|
80
89
|
run(options[:rails_root], options)
|
|
81
90
|
rescue => e
|
|
82
|
-
|
|
91
|
+
stderr.puts e.message
|
|
83
92
|
::Rails.logger.fatal('Failed running Dynflow daemon')
|
|
84
93
|
::Rails.logger.fatal(e)
|
|
85
94
|
exit 1
|
data/lib/dynflow/testing.rb
CHANGED
data/lib/dynflow/version.rb
CHANGED
data/lib/dynflow/world.rb
CHANGED
|
@@ -202,6 +202,16 @@ module Dynflow
|
|
|
202
202
|
Scheduled[execution_plan.id]
|
|
203
203
|
end
|
|
204
204
|
|
|
205
|
+
def chain(plan_uuids, action_class, *args)
|
|
206
|
+
plan_uuids = [plan_uuids] unless plan_uuids.is_a? Array
|
|
207
|
+
result = delay_with_options(action_class: action_class, args: args, delay_options: { frozen: true })
|
|
208
|
+
plan_uuids.each do |plan_uuid|
|
|
209
|
+
persistence.chain_execution_plan(plan_uuid, result.execution_plan_id)
|
|
210
|
+
end
|
|
211
|
+
persistence.set_delayed_plan_frozen(result.execution_plan_id, false)
|
|
212
|
+
result
|
|
213
|
+
end
|
|
214
|
+
|
|
205
215
|
def plan_elsewhere(action_class, *args)
|
|
206
216
|
execution_plan = ExecutionPlan.new(self, nil)
|
|
207
217
|
execution_plan.delay(nil, action_class, {}, *args)
|
|
@@ -325,17 +335,7 @@ module Dynflow
|
|
|
325
335
|
logger.info "start terminating throttle_limiter..."
|
|
326
336
|
throttle_limiter.terminate.wait(termination_timeout)
|
|
327
337
|
|
|
328
|
-
|
|
329
|
-
connector.stop_receiving_new_work(self, termination_timeout)
|
|
330
|
-
|
|
331
|
-
logger.info "start terminating executor..."
|
|
332
|
-
executor.terminate.wait(termination_timeout)
|
|
333
|
-
|
|
334
|
-
logger.info "start terminating executor dispatcher..."
|
|
335
|
-
executor_dispatcher_terminated = Concurrent::Promises.resolvable_future
|
|
336
|
-
executor_dispatcher.ask([:start_termination, executor_dispatcher_terminated])
|
|
337
|
-
executor_dispatcher_terminated.wait(termination_timeout)
|
|
338
|
-
end
|
|
338
|
+
terminate_executor
|
|
339
339
|
|
|
340
340
|
logger.info "start terminating client dispatcher..."
|
|
341
341
|
client_dispatcher_terminated = Concurrent::Promises.resolvable_future
|
|
@@ -350,7 +350,11 @@ module Dynflow
|
|
|
350
350
|
clock.ask(:terminate!).wait(termination_timeout)
|
|
351
351
|
end
|
|
352
352
|
|
|
353
|
-
|
|
353
|
+
begin
|
|
354
|
+
coordinator.delete_world(registered_world, true)
|
|
355
|
+
rescue Dynflow::Errors::FatalPersistenceError => e
|
|
356
|
+
nil
|
|
357
|
+
end
|
|
354
358
|
@terminated.resolve
|
|
355
359
|
true
|
|
356
360
|
rescue => e
|
|
@@ -361,7 +365,10 @@ module Dynflow
|
|
|
361
365
|
termination_future.wait(termination_timeout)
|
|
362
366
|
end.on_resolution do
|
|
363
367
|
@terminated.resolve
|
|
364
|
-
Thread.new
|
|
368
|
+
Thread.new do
|
|
369
|
+
logger.info 'World terminated, exiting.'
|
|
370
|
+
Kernel.exit if @exit_on_terminate.true?
|
|
371
|
+
end
|
|
365
372
|
end
|
|
366
373
|
end
|
|
367
374
|
end
|
|
@@ -395,6 +402,20 @@ module Dynflow
|
|
|
395
402
|
initialized.wait
|
|
396
403
|
return actor
|
|
397
404
|
end
|
|
405
|
+
|
|
406
|
+
def terminate_executor
|
|
407
|
+
return unless executor
|
|
408
|
+
|
|
409
|
+
connector.stop_receiving_new_work(self, termination_timeout)
|
|
410
|
+
|
|
411
|
+
logger.info "start terminating executor..."
|
|
412
|
+
executor.terminate.wait(termination_timeout)
|
|
413
|
+
|
|
414
|
+
logger.info "start terminating executor dispatcher..."
|
|
415
|
+
executor_dispatcher_terminated = Concurrent::Promises.resolvable_future
|
|
416
|
+
executor_dispatcher.ask([:start_termination, executor_dispatcher_terminated])
|
|
417
|
+
executor_dispatcher_terminated.wait(termination_timeout)
|
|
418
|
+
end
|
|
398
419
|
end
|
|
399
420
|
# rubocop:enable Metrics/ClassLength
|
|
400
421
|
end
|
data/lib/dynflow.rb
CHANGED
data/test/action_test.rb
CHANGED
|
@@ -135,7 +135,7 @@ module Dynflow
|
|
|
135
135
|
plan = create_and_plan_action(PlanEventedAction, { time: 0.5 })
|
|
136
136
|
action = run_action plan
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
assert_nil action.output[:status]
|
|
139
139
|
_(action.world.clock.pending_pings.first).wont_be_nil
|
|
140
140
|
_(action.state).must_equal :suspended
|
|
141
141
|
|
|
@@ -150,7 +150,7 @@ module Dynflow
|
|
|
150
150
|
plan = create_and_plan_action(PlanEventedAction, { time: nil })
|
|
151
151
|
action = run_action plan
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
assert_nil action.output[:status]
|
|
154
154
|
_(action.world.clock.pending_pings.first).must_be_nil
|
|
155
155
|
_(action.world.executor.events_to_process.first).wont_be_nil
|
|
156
156
|
_(action.state).must_equal :suspended
|
|
@@ -905,7 +905,7 @@ module Dynflow
|
|
|
905
905
|
|
|
906
906
|
it 'collects and drops output chunks' do
|
|
907
907
|
action = create_and_plan_action(OutputChunkAction)
|
|
908
|
-
|
|
908
|
+
assert_nil action.pending_output_chunks
|
|
909
909
|
|
|
910
910
|
action = run_action(action)
|
|
911
911
|
_(action.pending_output_chunks.count).must_equal 1
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Common helper functions for bats tests
|
|
3
|
+
|
|
4
|
+
# Get the project root directory
|
|
5
|
+
get_project_root() {
|
|
6
|
+
local dir="${BATS_TEST_DIRNAME}"
|
|
7
|
+
while [ "${dir}" != "/" ]; do
|
|
8
|
+
if [ -f "${dir}/dynflow.gemspec" ]; then
|
|
9
|
+
echo "${dir}"
|
|
10
|
+
return 0
|
|
11
|
+
fi
|
|
12
|
+
dir="$(dirname "${dir}")"
|
|
13
|
+
done
|
|
14
|
+
echo "ERROR: Could not find project root" >&2
|
|
15
|
+
return 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Setup test environment variables
|
|
19
|
+
setup_test_env() {
|
|
20
|
+
export PROJECT_ROOT="$(get_project_root)"
|
|
21
|
+
export BUNDLE_GEMFILE="${PROJECT_ROOT}/Gemfile"
|
|
22
|
+
|
|
23
|
+
# Set database URLs for tests
|
|
24
|
+
export DATABASE_URL="$(get_postgres_url)"
|
|
25
|
+
export REDIS_URL="$(get_redis_url)"
|
|
26
|
+
export DB_CONN_STRING="$DATABASE_URL"
|
|
27
|
+
|
|
28
|
+
# Test directories
|
|
29
|
+
export TEST_PIDDIR="${BATS_TEST_TMPDIR}/pids"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
run_background() {
|
|
33
|
+
local label="$1"
|
|
34
|
+
shift
|
|
35
|
+
|
|
36
|
+
local log_file="$(bg_output_file "$label")"
|
|
37
|
+
mkdir -p "$TEST_PIDDIR"
|
|
38
|
+
(
|
|
39
|
+
"$@" 2>&1 &
|
|
40
|
+
echo $! >"${TEST_PIDDIR}/${label}.pid"
|
|
41
|
+
) | tee "$log_file" | sed "s/^/${label}: /" &
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
bg_output_file() {
|
|
45
|
+
local label="$1"
|
|
46
|
+
|
|
47
|
+
echo "${BATS_TEST_TMPDIR}/${label}.log"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# A function that polls a given command until it succeeds or until it runs out
|
|
51
|
+
wait_for() {
|
|
52
|
+
local timeout="$1"
|
|
53
|
+
local interval="$2"
|
|
54
|
+
shift 2
|
|
55
|
+
|
|
56
|
+
local elapsed=0
|
|
57
|
+
while [ "$elapsed" -lt "$timeout" ]; do
|
|
58
|
+
if "$@" >/dev/null 2>&1; then
|
|
59
|
+
return 0
|
|
60
|
+
fi
|
|
61
|
+
sleep "$interval"
|
|
62
|
+
elapsed=$((elapsed + interval))
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
echo "Timeout after ${timeout}s waiting for: $*" >&2
|
|
66
|
+
return 1
|
|
67
|
+
}
|