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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2bf4beeafa28798576c07fd3fcabd039c026bee7457270426152c6a80be5b86a
4
- data.tar.gz: 2ff9ad272fa222d27f4103188b98809edbe150e92ce18a573442215d8cc06b57
3
+ metadata.gz: 7b7e50b6a77767c3cae603ef773f86c0ef218572ab37d4cf20eaa39a7baeb788
4
+ data.tar.gz: 3a38fce650541ffc1626a9eab628e0f4e2ccebf8a86ff8e25591ef334805293c
5
5
  SHA512:
6
- metadata.gz: cb049c6b32cf7a84bce0e616c64648ab13fecbb131233f7ae9a64b5308f3961cee00786b2ca0794bb764a2d42b488a4014b52da5d8de632887834230638e8145
7
- data.tar.gz: 4dd52c5e4c1e8644be534a0e48209c17a12ee03c358d81fdefd52b884b8d3b96c2f89417e46790544f68fe80a3004d209a12f2d0954393bb6dd65a4e82b6579e
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] = output.fetch(:failed_count, 0) + failed
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 = total_count
133
- failed = sub_plans_count('state' => %w(paused stopped), 'result' => %w(error warning))
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
- :failed_count => failed - output.fetch(:resumed_count, 0),
137
- :success_count => success)
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
- total_count - output[:cancelled_count] - output[:planned_count]
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
@@ -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
@@ -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
@@ -29,7 +29,11 @@ module Dynflow
29
29
  execution_plan_id: type { variants String, NilClass }
30
30
  end
31
31
 
32
- variants Event, Execution, Ping, Status, Planning
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?
@@ -66,6 +66,10 @@ module Dynflow
66
66
  end
67
67
  end
68
68
 
69
+ def halt(execution_plan_id)
70
+ @director.halt execution_plan_id
71
+ end
72
+
69
73
  def start_termination(*args)
70
74
  logger.info 'shutting down Core ...'
71
75
  super
@@ -57,6 +57,10 @@ module Dynflow
57
57
  @core.ask!([:execution_status, execution_plan_id])
58
58
  end
59
59
 
60
+ def halt(execution_plan_id)
61
+ @core.tell([:halt, execution_plan_id])
62
+ end
63
+
60
64
  def initialized
61
65
  @core_initialized
62
66
  end
@@ -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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dynflow
4
- VERSION = '1.8.4'
4
+ VERSION = '1.9.1'
5
5
  end
@@ -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
@@ -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.8.4
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: 2024-05-16 00:00:00.000000000 Z
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.26
686
+ rubygems_version: 3.3.27
686
687
  signing_key:
687
688
  specification_version: 4
688
689
  summary: DYNamic workFLOW engine