dynflow 1.8.4 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2bf4beeafa28798576c07fd3fcabd039c026bee7457270426152c6a80be5b86a
4
- data.tar.gz: 2ff9ad272fa222d27f4103188b98809edbe150e92ce18a573442215d8cc06b57
3
+ metadata.gz: c1eddd0493e79287abacde548a9237bf6d13c0ec6f5d2e67d802a85de9be29db
4
+ data.tar.gz: f0826ba6cc0c8066a70d6c7b2cf5f11e5e7d29e1c96915057b4f099546fd1a16
5
5
  SHA512:
6
- metadata.gz: cb049c6b32cf7a84bce0e616c64648ab13fecbb131233f7ae9a64b5308f3961cee00786b2ca0794bb764a2d42b488a4014b52da5d8de632887834230638e8145
7
- data.tar.gz: 4dd52c5e4c1e8644be534a0e48209c17a12ee03c358d81fdefd52b884b8d3b96c2f89417e46790544f68fe80a3004d209a12f2d0954393bb6dd65a4e82b6579e
6
+ metadata.gz: 7f2f82d41dbe9fa17ed1dc184dfaba98fc8e49ced49ec14ba523de6a4965b5ea0d9626d859b9f439647e47c1216a6d2b35a41d0dcb4a4e2d479e83cee736cd91
7
+ data.tar.gz: f362ca21ed154a5bd0b4ec5e5814cbb1fd48b78a51390bb17fb259d097fd5f12471c7022b85275ee4af3ee88e5efe2cc6dd199256ac6603bb13464c1ef1fe662
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
@@ -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
@@ -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.0'
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.0
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: 2024-06-11 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.1.2
686
687
  signing_key:
687
688
  specification_version: 4
688
689
  summary: DYNamic workFLOW engine