dynflow 1.8.4 → 1.9.1
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/examples/halt.rb +71 -0
- data/lib/dynflow/action/v2/with_sub_plans.rb +23 -8
- data/lib/dynflow/coordinator.rb +13 -0
- data/lib/dynflow/director/execution_plan_manager.rb +8 -1
- data/lib/dynflow/director/running_steps_manager.rb +9 -0
- data/lib/dynflow/director.rb +28 -0
- data/lib/dynflow/dispatcher/client_dispatcher.rb +5 -1
- data/lib/dynflow/dispatcher/executor_dispatcher.rb +7 -1
- data/lib/dynflow/dispatcher.rb +5 -1
- data/lib/dynflow/execution_plan.rb +10 -0
- data/lib/dynflow/executors/abstract/core.rb +4 -0
- data/lib/dynflow/executors/parallel.rb +4 -0
- data/lib/dynflow/persistence.rb +4 -0
- data/lib/dynflow/persistence_adapters/abstract.rb +4 -0
- data/lib/dynflow/persistence_adapters/sequel.rb +4 -0
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/world/invalidation.rb +15 -0
- data/lib/dynflow/world.rb +7 -0
- data/test/executor_test.rb +93 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b7e50b6a77767c3cae603ef773f86c0ef218572ab37d4cf20eaa39a7baeb788
|
4
|
+
data.tar.gz: 3a38fce650541ffc1626a9eab628e0f4e2ccebf8a86ff8e25591ef334805293c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aea5e2c7ef158cf395e10b7d487070772e9b3a58f72feaea97c12b90c7ad680f69c039a0f443a938f160feceb3993d58bb9eadc91e0be933d6cee43172a35e40
|
7
|
+
data.tar.gz: db00fb0a44f91871d6b0a3b39b6c4d8da7ffe3eaa363ee816e1de95aae576991a83c24a62ecb45212626fdb7de4de3342d84c5ca4fb2c7c766d0c5688301320e
|
data/examples/halt.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative 'example_helper'
|
5
|
+
|
6
|
+
example_description = <<DESC
|
7
|
+
|
8
|
+
Halting example
|
9
|
+
===================
|
10
|
+
|
11
|
+
This example shows, how halting works in Dynflow. It spawns a single action,
|
12
|
+
which in turn spawns a few evented actions and a single action which occupies
|
13
|
+
the executor for a long time.
|
14
|
+
|
15
|
+
Once the halt event is sent, the execution plan is halted, suspended steps
|
16
|
+
stay suspended forever, running steps stay running until they actually finish
|
17
|
+
the current run and the execution state is flipped over to stopped state.
|
18
|
+
|
19
|
+
You can see the details at #{ExampleHelper::DYNFLOW_URL}
|
20
|
+
|
21
|
+
DESC
|
22
|
+
|
23
|
+
class EventedCounter < Dynflow::Action
|
24
|
+
def run(event = nil)
|
25
|
+
output[:counter] ||= 0
|
26
|
+
output[:counter] += 1
|
27
|
+
action_logger.info "Iteration #{output[:counter]}"
|
28
|
+
|
29
|
+
if output[:counter] < input[:count]
|
30
|
+
plan_event(:tick, 5)
|
31
|
+
suspend
|
32
|
+
end
|
33
|
+
action_logger.info "Done"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Sleeper < Dynflow::Action
|
38
|
+
def run
|
39
|
+
sleep input[:time]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Wrapper < Dynflow::Action
|
44
|
+
def plan
|
45
|
+
sequence do
|
46
|
+
concurrence do
|
47
|
+
5.times { |i| plan_action(EventedCounter, :count => i + 1) }
|
48
|
+
plan_action Sleeper, :time => 20
|
49
|
+
end
|
50
|
+
plan_self
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def run
|
55
|
+
# Noop
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
if $PROGRAM_NAME == __FILE__
|
60
|
+
puts example_description
|
61
|
+
|
62
|
+
ExampleHelper.world.action_logger.level = Logger::DEBUG
|
63
|
+
ExampleHelper.world
|
64
|
+
t = ExampleHelper.world.trigger(Wrapper)
|
65
|
+
Thread.new do
|
66
|
+
sleep 8
|
67
|
+
ExampleHelper.world.halt(t.id)
|
68
|
+
end
|
69
|
+
|
70
|
+
ExampleHelper.run_web_console
|
71
|
+
end
|
@@ -102,7 +102,7 @@ module Dynflow::Action::V2
|
|
102
102
|
|
103
103
|
def increase_counts(planned, failed)
|
104
104
|
output[:planned_count] += planned + failed
|
105
|
-
output[:failed_count]
|
105
|
+
output[:failed_count] = output.fetch(:failed_count, 0) + failed
|
106
106
|
output[:pending_count] = output.fetch(:pending_count, 0) + planned
|
107
107
|
output[:success_count] ||= 0
|
108
108
|
end
|
@@ -129,12 +129,20 @@ module Dynflow::Action::V2
|
|
129
129
|
end
|
130
130
|
|
131
131
|
def recalculate_counts
|
132
|
-
total
|
133
|
-
|
132
|
+
total = total_count
|
133
|
+
if output[:cancelled_timestamp]
|
134
|
+
cancelled_scheduled_plans = sub_plans_count_after(output[:cancelled_timestamp], { 'state' => %w(paused stopped), 'result' => %w(error warning) })
|
135
|
+
cancelled_unscheduled_plans = total_count - output[:planned_count]
|
136
|
+
cancelled = cancelled_unscheduled_plans + cancelled_scheduled_plans
|
137
|
+
else
|
138
|
+
cancelled = cancelled_scheduled_plans = 0
|
139
|
+
end
|
140
|
+
failed = sub_plans_count('state' => %w(paused stopped), 'result' => %w(error warning)) - cancelled_scheduled_plans
|
134
141
|
success = sub_plans_count('state' => 'stopped', 'result' => 'success')
|
135
|
-
output.update(:pending_count => total - failed - success,
|
136
|
-
|
137
|
-
|
142
|
+
output.update(:pending_count => total - failed - success - cancelled_scheduled_plans,
|
143
|
+
:failed_count => failed - output.fetch(:resumed_count, 0),
|
144
|
+
:success_count => success,
|
145
|
+
:cancelled_count => cancelled)
|
138
146
|
end
|
139
147
|
|
140
148
|
def counts_set?
|
@@ -142,7 +150,7 @@ module Dynflow::Action::V2
|
|
142
150
|
end
|
143
151
|
|
144
152
|
def check_for_errors!
|
145
|
-
raise SubtaskFailedException.new("A sub task failed") if output[:failed_count] > 0
|
153
|
+
raise SubtaskFailedException.new("A sub task failed") if output[:failed_count] + output[:cancelled_count] > 0
|
146
154
|
end
|
147
155
|
|
148
156
|
# Helper for creating sub plans
|
@@ -173,6 +181,7 @@ module Dynflow::Action::V2
|
|
173
181
|
def cancel!(force = false)
|
174
182
|
# Count the not-yet-planned tasks as cancelled
|
175
183
|
output[:cancelled_count] = total_count - output[:planned_count]
|
184
|
+
output[:cancelled_timestamp] ||= Time.now.utc.iso8601 # time in UTC for comparison with UTC times in the database
|
176
185
|
on_planning_finished if output[:cancelled_count].positive?
|
177
186
|
# Pass the cancel event to running sub plans if they can be cancelled
|
178
187
|
sub_plans(:state => 'running').each { |sub_plan| sub_plan.cancel(force) if sub_plan.cancellable? }
|
@@ -198,7 +207,9 @@ module Dynflow::Action::V2
|
|
198
207
|
end
|
199
208
|
|
200
209
|
def remaining_count
|
201
|
-
|
210
|
+
return 0 if output[:cancelled_timestamp]
|
211
|
+
|
212
|
+
total_count - output[:planned_count]
|
202
213
|
end
|
203
214
|
|
204
215
|
private
|
@@ -216,5 +227,9 @@ module Dynflow::Action::V2
|
|
216
227
|
def sub_plans_count(filter = {})
|
217
228
|
world.persistence.find_execution_plan_counts(filters: sub_plan_filter.merge(filter))
|
218
229
|
end
|
230
|
+
|
231
|
+
def sub_plans_count_after(timestamp, filter = {})
|
232
|
+
world.persistence.find_execution_plan_counts_after(timestamp, { filters: sub_plan_filter.merge(filter) })
|
233
|
+
end
|
219
234
|
end
|
220
235
|
end
|
data/lib/dynflow/coordinator.rb
CHANGED
@@ -266,6 +266,19 @@ module Dynflow
|
|
266
266
|
end
|
267
267
|
end
|
268
268
|
|
269
|
+
class ExecutionInhibitionLock < Lock
|
270
|
+
def initialize(execution_plan_id)
|
271
|
+
super
|
272
|
+
@data[:owner_id] = "execution-plan:#{execution_plan_id}"
|
273
|
+
@data[:execution_plan_id] = execution_plan_id
|
274
|
+
@data[:id] = self.class.lock_id(execution_plan_id)
|
275
|
+
end
|
276
|
+
|
277
|
+
def self.lock_id(execution_plan_id)
|
278
|
+
"execution-plan:#{execution_plan_id}"
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
269
282
|
class ExecutionLock < LockByWorld
|
270
283
|
def initialize(world, execution_plan_id, client_world_id, request_id)
|
271
284
|
super(world)
|
@@ -13,6 +13,7 @@ module Dynflow
|
|
13
13
|
@execution_plan = Type! execution_plan, ExecutionPlan
|
14
14
|
@future = Type! future, Concurrent::Promises::ResolvableFuture
|
15
15
|
@running_steps_manager = RunningStepsManager.new(world)
|
16
|
+
@halted = false
|
16
17
|
|
17
18
|
unless [:planned, :paused].include? execution_plan.state
|
18
19
|
raise "execution_plan is not in pending or paused state, it's #{execution_plan.state}"
|
@@ -25,6 +26,11 @@ module Dynflow
|
|
25
26
|
start_run or start_finalize or finish
|
26
27
|
end
|
27
28
|
|
29
|
+
def halt
|
30
|
+
@halted = true
|
31
|
+
@running_steps_manager.terminate
|
32
|
+
end
|
33
|
+
|
28
34
|
def restart
|
29
35
|
@run_manager = nil
|
30
36
|
@finalize_manager = nil
|
@@ -72,7 +78,7 @@ module Dynflow
|
|
72
78
|
end
|
73
79
|
|
74
80
|
def done?
|
75
|
-
(!@run_manager || @run_manager.done?) && (!@finalize_manager || @finalize_manager.done?)
|
81
|
+
@halted || (!@run_manager || @run_manager.done?) && (!@finalize_manager || @finalize_manager.done?)
|
76
82
|
end
|
77
83
|
|
78
84
|
def terminate
|
@@ -88,6 +94,7 @@ module Dynflow
|
|
88
94
|
def compute_next_from_step(step)
|
89
95
|
raise "run manager not set" unless @run_manager
|
90
96
|
raise "run manager already done" if @run_manager.done?
|
97
|
+
return [] if @halted
|
91
98
|
|
92
99
|
next_steps = @run_manager.what_is_next(step)
|
93
100
|
if @run_manager.done?
|
@@ -16,6 +16,7 @@ module Dynflow
|
|
16
16
|
# to handle potential updates of the step object (that is part of the event)
|
17
17
|
@events = QueueHash.new(Integer, Director::Event)
|
18
18
|
@events_by_request_id = {}
|
19
|
+
@halted = false
|
19
20
|
end
|
20
21
|
|
21
22
|
def terminate
|
@@ -27,6 +28,10 @@ module Dynflow
|
|
27
28
|
end
|
28
29
|
end
|
29
30
|
|
31
|
+
def halt
|
32
|
+
@halted = true
|
33
|
+
end
|
34
|
+
|
30
35
|
def add(step, work)
|
31
36
|
Type! step, ExecutionPlan::Steps::RunStep
|
32
37
|
@running_steps[step.id] = step
|
@@ -84,6 +89,10 @@ module Dynflow
|
|
84
89
|
event.result.reject UnprocessableEvent.new('step is not suspended, it cannot process events')
|
85
90
|
return []
|
86
91
|
end
|
92
|
+
if @halted
|
93
|
+
event.result.reject UnprocessableEvent.new('execution plan is halted, it cannot receive events')
|
94
|
+
return []
|
95
|
+
end
|
87
96
|
|
88
97
|
can_run_event = @work_items.empty?(step.id)
|
89
98
|
@events_by_request_id[event.request_id] = event
|
data/lib/dynflow/director.rb
CHANGED
@@ -246,8 +246,28 @@ module Dynflow
|
|
246
246
|
end
|
247
247
|
end
|
248
248
|
|
249
|
+
def halt(event)
|
250
|
+
halt_execution(event.execution_plan_id)
|
251
|
+
end
|
252
|
+
|
249
253
|
private
|
250
254
|
|
255
|
+
def halt_execution(execution_plan_id)
|
256
|
+
manager = @execution_plan_managers[execution_plan_id]
|
257
|
+
@logger.warn "Halting execution plan #{execution_plan_id}"
|
258
|
+
return halt_inactive(execution_plan_id) unless manager
|
259
|
+
|
260
|
+
manager.halt
|
261
|
+
finish_manager manager
|
262
|
+
end
|
263
|
+
|
264
|
+
def halt_inactive(execution_plan_id)
|
265
|
+
plan = @world.persistence.load_execution_plan(execution_plan_id)
|
266
|
+
plan.update_state(:stopped)
|
267
|
+
rescue => e
|
268
|
+
@logger.error e
|
269
|
+
end
|
270
|
+
|
251
271
|
def unless_done(manager, work_items)
|
252
272
|
return [] unless manager
|
253
273
|
if manager.done?
|
@@ -310,6 +330,14 @@ module Dynflow
|
|
310
330
|
"cannot execute execution_plan_id:#{execution_plan_id} it's stopped"
|
311
331
|
end
|
312
332
|
|
333
|
+
lock_class = Coordinator::ExecutionInhibitionLock
|
334
|
+
filters = { class: lock_class.to_s, owner_id: lock_class.lock_id(execution_plan_id) }
|
335
|
+
if @world.coordinator.find_records(filters).any?
|
336
|
+
halt_execution(execution_plan_id)
|
337
|
+
raise Dynflow::Error,
|
338
|
+
"cannot execute execution_plan_id:#{execution_plan_id} it's execution is inhibited"
|
339
|
+
end
|
340
|
+
|
313
341
|
@execution_plan_managers[execution_plan_id] =
|
314
342
|
ExecutionPlanManager.new(@world, execution_plan, finished)
|
315
343
|
rescue Dynflow::Error => e
|
@@ -141,6 +141,10 @@ module Dynflow
|
|
141
141
|
ignore_unknown = event.optional
|
142
142
|
find_executor(event.execution_plan_id)
|
143
143
|
end),
|
144
|
+
(on ~Halt do |event|
|
145
|
+
executor = find_executor(event.execution_plan_id)
|
146
|
+
executor == Dispatcher::UnknownWorld ? AnyExecutor : executor
|
147
|
+
end),
|
144
148
|
(on Ping.(~any, ~any) | Status.(~any, ~any) do |receiver_id, _|
|
145
149
|
receiver_id
|
146
150
|
end)
|
@@ -236,7 +240,7 @@ module Dynflow
|
|
236
240
|
(on Execution.(execution_plan_id: ~any) do |uuid|
|
237
241
|
@world.persistence.load_execution_plan(uuid)
|
238
242
|
end),
|
239
|
-
(on Event | Ping do
|
243
|
+
(on Event | Ping | Halt do
|
240
244
|
true
|
241
245
|
end)
|
242
246
|
@tracked_requests.delete(id).success! resolve_to
|
@@ -13,7 +13,8 @@ module Dynflow
|
|
13
13
|
on(Planning) { perform_planning(envelope, envelope.message) },
|
14
14
|
on(Execution) { perform_execution(envelope, envelope.message) },
|
15
15
|
on(Event) { perform_event(envelope, envelope.message) },
|
16
|
-
on(Status) { get_execution_status(envelope, envelope.message) }
|
16
|
+
on(Status) { get_execution_status(envelope, envelope.message) },
|
17
|
+
on(Halt) { halt_execution_plan(envelope, envelope.message) })
|
17
18
|
end
|
18
19
|
|
19
20
|
protected
|
@@ -52,6 +53,11 @@ module Dynflow
|
|
52
53
|
end
|
53
54
|
end
|
54
55
|
|
56
|
+
def halt_execution_plan(envelope, execution_plan_id)
|
57
|
+
@world.executor.halt execution_plan_id
|
58
|
+
respond(envelope, Done)
|
59
|
+
end
|
60
|
+
|
55
61
|
def perform_event(envelope, event_request)
|
56
62
|
future = on_finish do |f|
|
57
63
|
f.then do
|
data/lib/dynflow/dispatcher.rb
CHANGED
@@ -29,7 +29,11 @@ module Dynflow
|
|
29
29
|
execution_plan_id: type { variants String, NilClass }
|
30
30
|
end
|
31
31
|
|
32
|
-
|
32
|
+
Halt = type do
|
33
|
+
fields! execution_plan_id: String, optional: Algebrick::Types::Boolean
|
34
|
+
end
|
35
|
+
|
36
|
+
variants Event, Execution, Ping, Status, Planning, Halt
|
33
37
|
end
|
34
38
|
|
35
39
|
Response = Algebrick.type do
|
@@ -133,7 +133,9 @@ module Dynflow
|
|
133
133
|
telemetry_common_options.merge(:result => key.to_s))
|
134
134
|
end
|
135
135
|
hooks_to_run << key
|
136
|
+
world.persistence.delete_delayed_plans(:execution_plan_uuid => id) if delay_record && original == :scheduled
|
136
137
|
unlock_all_singleton_locks!
|
138
|
+
unlock_execution_inhibition_lock!
|
137
139
|
when :paused
|
138
140
|
unlock_all_singleton_locks!
|
139
141
|
else
|
@@ -566,6 +568,14 @@ module Dynflow
|
|
566
568
|
end
|
567
569
|
end
|
568
570
|
|
571
|
+
def unlock_execution_inhibition_lock!
|
572
|
+
filter = { :owner_id => 'execution-plan:' + self.id,
|
573
|
+
:class => Dynflow::Coordinator::ExecutionInhibitionLock.to_s }
|
574
|
+
world.coordinator.find_locks(filter).each do |lock|
|
575
|
+
world.coordinator.release(lock)
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
569
579
|
def toggle_telemetry_state(original, new)
|
570
580
|
return if original == new
|
571
581
|
@label = root_plan_step.action_class if @label.nil?
|
data/lib/dynflow/persistence.rb
CHANGED
@@ -73,6 +73,10 @@ module Dynflow
|
|
73
73
|
adapter.find_execution_plan_counts(options)
|
74
74
|
end
|
75
75
|
|
76
|
+
def find_execution_plan_counts_after(timestamp, options)
|
77
|
+
adapter.find_execution_plan_counts_after(timestamp, options)
|
78
|
+
end
|
79
|
+
|
76
80
|
def delete_execution_plans(filters, batch_size = 1000, enforce_backup_dir = nil)
|
77
81
|
backup_dir = enforce_backup_dir || current_backup_dir
|
78
82
|
adapter.delete_execution_plans(filters, batch_size, backup_dir)
|
@@ -46,6 +46,10 @@ module Dynflow
|
|
46
46
|
filter(:execution_plan, options[:filters]).count
|
47
47
|
end
|
48
48
|
|
49
|
+
def find_execution_plan_counts_after(timestamp, options = {})
|
50
|
+
raise NotImplementedError
|
51
|
+
end
|
52
|
+
|
49
53
|
def find_execution_plan_statuses(options)
|
50
54
|
raise NotImplementedError
|
51
55
|
end
|
@@ -78,6 +78,10 @@ module Dynflow
|
|
78
78
|
filter(:execution_plan, table(:execution_plan), options[:filters]).count
|
79
79
|
end
|
80
80
|
|
81
|
+
def find_execution_plan_counts_after(timestamp, options = {})
|
82
|
+
filter(:execution_plan, table(:execution_plan), options[:filters]).filter(::Sequel.lit('ended_at >= ?', timestamp)).count
|
83
|
+
end
|
84
|
+
|
81
85
|
def find_execution_plan_statuses(options)
|
82
86
|
plans = filter(:execution_plan, table(:execution_plan), options[:filters])
|
83
87
|
.select(:uuid, :state, :result)
|
data/lib/dynflow/version.rb
CHANGED
@@ -29,12 +29,27 @@ module Dynflow
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
+
prune_execution_inhibition_locks!
|
33
|
+
|
32
34
|
pruned = persistence.prune_envelopes(world.id)
|
33
35
|
logger.error("Pruned #{pruned} envelopes for invalidated world #{world.id}") unless pruned.zero?
|
34
36
|
coordinator.delete_world(world)
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
40
|
+
# Prunes execution inhibition locks which got somehow left behind.
|
41
|
+
# Any execution inhibition locks, which have their corresponding execution
|
42
|
+
# plan in stopped state, will be removed.
|
43
|
+
def prune_execution_inhibition_locks!
|
44
|
+
locks = coordinator.find_locks(class: Coordinator::ExecutionInhibitionLock.name)
|
45
|
+
uuids = locks.map { |lock| lock.data[:execution_plan_id] }
|
46
|
+
plan_uuids = persistence.find_execution_plans(filters: { uuid: uuids, state: 'stopped' }).map(&:id)
|
47
|
+
|
48
|
+
locks.select { |lock| plan_uuids.include? lock.data[:execution_plan_id] }.each do |lock|
|
49
|
+
coordinator.release(lock)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
38
53
|
def invalidate_planning_lock(planning_lock)
|
39
54
|
with_valid_execution_plan_for_lock(planning_lock) do |plan|
|
40
55
|
plan.steps.values.each { |step| invalidate_step step }
|
data/lib/dynflow/world.rb
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
require 'dynflow/world/invalidation'
|
5
5
|
|
6
6
|
module Dynflow
|
7
|
+
# rubocop:disable Metrics/ClassLength
|
7
8
|
class World
|
8
9
|
include Algebrick::TypeCheck
|
9
10
|
include Algebrick::Matching
|
@@ -252,6 +253,11 @@ module Dynflow
|
|
252
253
|
publish_request(Dispatcher::Status[world_id, execution_plan_id], done, false, timeout)
|
253
254
|
end
|
254
255
|
|
256
|
+
def halt(execution_plan_id, accepted = Concurrent::Promises.resolvable_future)
|
257
|
+
coordinator.acquire(Coordinator::ExecutionInhibitionLock.new(execution_plan_id))
|
258
|
+
publish_request(Dispatcher::Halt[execution_plan_id], accepted, false)
|
259
|
+
end
|
260
|
+
|
255
261
|
def publish_request(request, done, wait_for_accepted, timeout = nil)
|
256
262
|
accepted = Concurrent::Promises.resolvable_future
|
257
263
|
accepted.rescue do |reason|
|
@@ -390,4 +396,5 @@ module Dynflow
|
|
390
396
|
return actor
|
391
397
|
end
|
392
398
|
end
|
399
|
+
# rubocop:enable Metrics/ClassLength
|
393
400
|
end
|
data/test/executor_test.rb
CHANGED
@@ -718,6 +718,99 @@ module Dynflow
|
|
718
718
|
assert [world.terminate, world.terminate].map(&:value).all?
|
719
719
|
end
|
720
720
|
end
|
721
|
+
|
722
|
+
describe 'halting' do
|
723
|
+
include TestHelpers
|
724
|
+
let(:world) { WorldFactory.create_world }
|
725
|
+
|
726
|
+
it 'halts an execution plan with a suspended step' do
|
727
|
+
triggered = world.trigger(Support::DummyExample::PlanEventsAction, ping_time: 1)
|
728
|
+
plan = world.persistence.load_execution_plan(triggered.id)
|
729
|
+
wait_for do
|
730
|
+
plan = world.persistence.load_execution_plan(triggered.id)
|
731
|
+
plan.state == :running
|
732
|
+
end
|
733
|
+
world.halt(triggered.id)
|
734
|
+
wait_for('the execution plan to halt') do
|
735
|
+
plan = world.persistence.load_execution_plan(triggered.id)
|
736
|
+
plan.state == :stopped
|
737
|
+
end
|
738
|
+
_(plan.steps[2].state).must_equal :suspended
|
739
|
+
end
|
740
|
+
|
741
|
+
it 'halts a paused execution plan' do
|
742
|
+
triggered = world.trigger(Support::DummyExample::FailingDummy)
|
743
|
+
plan = world.persistence.load_execution_plan(triggered.id)
|
744
|
+
wait_for do
|
745
|
+
plan = world.persistence.load_execution_plan(triggered.id)
|
746
|
+
plan.state == :paused
|
747
|
+
end
|
748
|
+
world.halt(plan.id)
|
749
|
+
wait_for('the execution plan to halt') do
|
750
|
+
plan = world.persistence.load_execution_plan(triggered.id)
|
751
|
+
plan.state == :stopped
|
752
|
+
end
|
753
|
+
_(plan.steps[2].state).must_equal :error
|
754
|
+
end
|
755
|
+
|
756
|
+
it 'halts a planned execution plan' do
|
757
|
+
plan = world.plan(Support::DummyExample::Dummy)
|
758
|
+
wait_for do
|
759
|
+
plan = world.persistence.load_execution_plan(plan.id)
|
760
|
+
plan.state == :planned
|
761
|
+
end
|
762
|
+
world.halt(plan.id)
|
763
|
+
wait_for('the execution plan to halt') do
|
764
|
+
plan = world.persistence.load_execution_plan(plan.id)
|
765
|
+
plan.state == :stopped
|
766
|
+
end
|
767
|
+
_(plan.steps[2].state).must_equal :pending
|
768
|
+
end
|
769
|
+
|
770
|
+
it 'halts a scheduled execution plan' do
|
771
|
+
plan = world.delay(Support::DummyExample::Dummy, { start_at: Time.now + 120 })
|
772
|
+
wait_for do
|
773
|
+
plan = world.persistence.load_execution_plan(plan.id)
|
774
|
+
plan.state == :scheduled
|
775
|
+
end
|
776
|
+
world.halt(plan.id)
|
777
|
+
wait_for('the execution plan to halt') do
|
778
|
+
plan = world.persistence.load_execution_plan(plan.id)
|
779
|
+
plan.state == :stopped
|
780
|
+
end
|
781
|
+
_(plan.delay_record).must_be :nil?
|
782
|
+
_(plan.steps[1].state).must_equal :pending
|
783
|
+
end
|
784
|
+
|
785
|
+
it 'halts a pending execution plan' do
|
786
|
+
plan = ExecutionPlan.new(world, nil)
|
787
|
+
plan.save
|
788
|
+
world.halt(plan.id)
|
789
|
+
wait_for('the execution plan to halt') do
|
790
|
+
plan = world.persistence.load_execution_plan(plan.id)
|
791
|
+
plan.state == :stopped
|
792
|
+
end
|
793
|
+
end
|
794
|
+
end
|
795
|
+
|
796
|
+
describe 'execution inhibition locks' do
|
797
|
+
include TestHelpers
|
798
|
+
let(:world) { WorldFactory.create_world }
|
799
|
+
|
800
|
+
it 'inhibits execution' do
|
801
|
+
plan = world.plan(Support::DummyExample::Dummy)
|
802
|
+
world.coordinator.acquire(Coordinator::ExecutionInhibitionLock.new(plan.id))
|
803
|
+
triggered = world.execute(plan.id)
|
804
|
+
triggered.wait
|
805
|
+
_(triggered).must_be :rejected?
|
806
|
+
|
807
|
+
plan = world.persistence.load_execution_plan(plan.id)
|
808
|
+
_(plan.state).must_equal :stopped
|
809
|
+
|
810
|
+
locks = world.coordinator.find_locks({ class: Coordinator::ExecutionInhibitionLock.to_s, owner_id: "execution-plan:#{plan.id}" })
|
811
|
+
_(locks).must_be :empty?
|
812
|
+
end
|
813
|
+
end
|
721
814
|
end
|
722
815
|
end
|
723
816
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dynflow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.9.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Necas
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2025-04-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: algebrick
|
@@ -412,6 +412,7 @@ files:
|
|
412
412
|
- examples/clock_benchmark.rb
|
413
413
|
- examples/example_helper.rb
|
414
414
|
- examples/future_execution.rb
|
415
|
+
- examples/halt.rb
|
415
416
|
- examples/memory_limit_watcher.rb
|
416
417
|
- examples/orchestrate.rb
|
417
418
|
- examples/orchestrate_evented.rb
|
@@ -682,7 +683,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
682
683
|
- !ruby/object:Gem::Version
|
683
684
|
version: '0'
|
684
685
|
requirements: []
|
685
|
-
rubygems_version: 3.3.
|
686
|
+
rubygems_version: 3.3.27
|
686
687
|
signing_key:
|
687
688
|
specification_version: 4
|
688
689
|
summary: DYNamic workFLOW engine
|