dynflow 0.8.25 → 0.8.26

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
  SHA1:
3
- metadata.gz: df86d8f76a1106ba36deefac06dbf223b7be53ac
4
- data.tar.gz: b0e2c8049e5cb81b3d930002192f8d857418e264
3
+ metadata.gz: ab4978b454a2f7066506b1abcf644184fcb5542b
4
+ data.tar.gz: db7286bce7f99037e24df8dedc227a85026d1d77
5
5
  SHA512:
6
- metadata.gz: 4559e454e25d46305c5438e0e9bdf68b23bca95a05e219cd727e839acedeec7037880d609683d598171b9f2d2df1e87937f697d4bc047a789ff424c39abe95dd
7
- data.tar.gz: fdf35384c85d1d1d752ba05b94a14c24f73fb23b7398d5595cfd5199fcf881c3f12822363cb1d26656b8231a6ee184a648b7d8f0d0ca533b6556c8e6b3759286
6
+ metadata.gz: 8df6257ff673232080c2867715ee5605d07bd197e2a1f950ed3f13a2fc88f9f8ce3415d47c88b3ef88a4523a0c8e4ccf7f81b435e90b6d694e758335b0ebbeb9
7
+ data.tar.gz: bf75b46a23911444fd9cb087d7403a2ff79c20f83bd6f091502cacc2e6628211315465ccb9e5e9bfa04de65cfe020d79f1db8870e233dfb67defbcfdd9a049e0
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
-
3
2
  example_description = <<DESC
4
3
  Sub Plans Example
5
4
  ===================
@@ -16,20 +15,44 @@ DESC
16
15
  require_relative 'example_helper'
17
16
  require_relative 'orchestrate_evented'
18
17
 
18
+ COUNT = (ARGV[0] || 25).to_i
19
+
19
20
  class SubPlansExample < Dynflow::Action
20
21
  include Dynflow::Action::WithSubPlans
22
+ include Dynflow::Action::WithBulkSubPlans
21
23
 
22
24
  def create_sub_plans
23
- 10.times.map { |i| trigger(OrchestrateEvented::CreateMachine, "host-#{i}", 'web_server') }
25
+ current_batch.map { |i| trigger(OrchestrateEvented::CreateMachine, "host-#{i}", 'web_server') }
26
+ end
27
+
28
+ def batch_size
29
+ 5
30
+ end
31
+
32
+ def batch(from, size)
33
+ COUNT.times.drop(from).take(size)
34
+ end
35
+
36
+ def total_count
37
+ COUNT
24
38
  end
25
39
  end
26
40
 
41
+ class PollingSubPlansExample < SubPlansExample
42
+ include Dynflow::Action::WithPollingSubPlans
43
+ end
44
+
27
45
  if $0 == __FILE__
28
- triggered = ExampleHelper.world.trigger(SubPlansExample)
46
+ ExampleHelper.world.action_logger.level = Logger::INFO
47
+ ExampleHelper.world
48
+ t1 = ExampleHelper.world.trigger(SubPlansExample)
49
+ t2 = ExampleHelper.world.trigger(PollingSubPlansExample)
29
50
  puts example_description
30
51
  puts <<-MSG.gsub(/^.*\|/, '')
31
- | Execution plan #{triggered.id} with sub plans triggered
32
- | You can see the details at http://localhost:4567/#{triggered.id}
52
+ | Execution plans #{t1.id} and #{t2.id} with sub plans triggered
53
+ | You can see the details at
54
+ | http://localhost:4567/#{t2.id}
55
+ | http://localhost:4567/#{t1.id}
33
56
  MSG
34
57
 
35
58
  ExampleHelper.run_web_console
@@ -23,6 +23,7 @@ module Dynflow
23
23
  require 'dynflow/action/cancellable'
24
24
  require 'dynflow/action/with_sub_plans'
25
25
  require 'dynflow/action/with_bulk_sub_plans'
26
+ require 'dynflow/action/with_polling_sub_plans'
26
27
 
27
28
  def self.all_children
28
29
  children.values.inject(children.values) do |children, child|
@@ -1,10 +1,14 @@
1
1
  module Dynflow
2
2
  module Action::Cancellable
3
3
  Cancel = Algebrick.atom
4
+ Abort = Algebrick.atom
4
5
 
5
6
  def run(event = nil)
6
- if Cancel === event
7
+ case event
8
+ when Cancel
7
9
  cancel!
10
+ when Abort
11
+ abort!
8
12
  else
9
13
  super event
10
14
  end
@@ -13,5 +17,9 @@ module Dynflow
13
17
  def cancel!
14
18
  raise NotImplementedError
15
19
  end
20
+
21
+ def abort!
22
+ cancel!
23
+ end
16
24
  end
17
25
  end
@@ -13,13 +13,21 @@ module Dynflow
13
13
 
14
14
  def run(event = nil)
15
15
  if event === PlanNextBatch
16
- spawn_plans if can_spawn_next_batch?
17
- suspend
16
+ if can_spawn_next_batch?
17
+ spawn_plans
18
+ suspend
19
+ else
20
+ on_planning_finished
21
+ end
18
22
  else
19
23
  super
20
24
  end
21
25
  end
22
26
 
27
+ def on_planning_finished
28
+ suspend
29
+ end
30
+
23
31
  def initiate
24
32
  output[:planned_count] = 0
25
33
  output[:total_count] = total_count
@@ -62,7 +70,7 @@ module Dynflow
62
70
  suspended_action << PlanNextBatch
63
71
  end
64
72
 
65
- def cancel!
73
+ def cancel!(force = false)
66
74
  # Count the not-yet-planned tasks as failed
67
75
  output[:failed_count] += total_count - output[:planned_count]
68
76
  if uses_concurrency_control
@@ -72,14 +80,17 @@ module Dynflow
72
80
  # Just stop the tasks which were not started yet
73
81
  sub_plans(:state => 'planned').each { |sub_plan| sub_plan.update_state(:stopped) }
74
82
  end
75
- running = sub_plans(:state => 'running')
76
83
  # Pass the cancel event to running sub plans if they can be cancelled
77
- running.each { |sub_plan| sub_plan.cancel! if sub_plan.cancellable? }
84
+ sub_plans(:state => 'running').each { |sub_plan| sub_plan.cancel(force) if sub_plan.cancellable? }
78
85
  suspend
79
86
  end
80
87
 
81
88
  private
82
89
 
90
+ def done?
91
+ !can_spawn_next_batch? && super
92
+ end
93
+
83
94
  def can_spawn_next_batch?
84
95
  total_count - output[:success_count] - output[:pending_count] - output[:failed_count] > 0
85
96
  end
@@ -0,0 +1,72 @@
1
+ module Dynflow
2
+ module Action::WithPollingSubPlans
3
+
4
+ REFRESH_INTERVAL = 10
5
+ Poll = Algebrick.atom
6
+
7
+ def run(event = nil)
8
+ case event
9
+ when Poll
10
+ poll
11
+ else
12
+ super(event)
13
+ end
14
+ end
15
+
16
+ def poll
17
+ recalculate_counts
18
+ try_to_finish || suspend_and_ping
19
+ end
20
+
21
+ def initiate
22
+ ping suspended_action
23
+ super
24
+ end
25
+
26
+ def wait_for_sub_plans(sub_plans)
27
+ increase_counts(sub_plans.count, 0)
28
+ suspend
29
+ end
30
+
31
+ def resume
32
+ if sub_plans.all? { |sub_plan| sub_plan.error_in_plan? }
33
+ output[:resumed_count] ||= 0
34
+ output[:resumed_count] += output[:failed_count]
35
+ # We're starting over and need to reset the counts
36
+ %w(total failed pending success).each { |key| output.delete("#{key}_count".to_sym) }
37
+ initiate
38
+ else
39
+ if self.is_a?(::Dynflow::Action::WithBulkSubPlans) && can_spawn_next_batch?
40
+ # Not everything was spawned
41
+ ping suspended_action
42
+ spawn_plans
43
+ suspend
44
+ else
45
+ poll
46
+ end
47
+ end
48
+ end
49
+
50
+ def notify_on_finish(_sub_plans)
51
+ suspend
52
+ end
53
+
54
+ def suspend_and_ping
55
+ suspend { |suspended_action| ping suspended_action }
56
+ end
57
+
58
+ def ping(suspended_action)
59
+ world.clock.ping suspended_action, REFRESH_INTERVAL, Poll
60
+ end
61
+
62
+ def recalculate_counts
63
+ total = sub_plans.count
64
+ failed = sub_plans('state' => %w(paused stopped), 'result' => 'error').count
65
+ success = sub_plans('state' => 'stopped', 'result' => 'success').count
66
+ output.update(:total_count => total - output.fetch(:resumed_count, 0),
67
+ :pending_count => 0,
68
+ :failed_count => failed - output.fetch(:resumed_count, 0),
69
+ :success_count => success)
70
+ end
71
+ end
72
+ end
@@ -22,6 +22,9 @@ module Dynflow
22
22
  end),
23
23
  (on Action::Cancellable::Cancel do
24
24
  cancel!
25
+ end),
26
+ (on Action::Cancellable::Abort do
27
+ abort!
25
28
  end)
26
29
  end
27
30
 
@@ -62,12 +65,16 @@ module Dynflow
62
65
  def on_finish
63
66
  end
64
67
 
65
- def cancel!
68
+ def cancel!(force = false)
66
69
  @world.throttle_limiter.cancel!(execution_plan_id)
67
- sub_plans('state' => 'running').each(&:cancel)
70
+ sub_plans('state' => 'running').each { |sub_plan| sub_plan.cancel(force) }
68
71
  suspend
69
72
  end
70
73
 
74
+ def abort!
75
+ cancel! true
76
+ end
77
+
71
78
  # Helper for creating sub plans
72
79
  def trigger(*args)
73
80
  if uses_concurrency_control
@@ -146,8 +153,13 @@ module Dynflow
146
153
  end
147
154
 
148
155
  def sub_plans(filter = {})
149
- @sub_plans ||= world.persistence.find_execution_plans(filters: { 'caller_execution_plan_id' => execution_plan_id,
150
- 'caller_action_id' => self.id }.merge(filter) )
156
+ filters = { 'caller_execution_plan_id' => execution_plan_id,
157
+ 'caller_action_id' => self.id }
158
+ if filter.empty?
159
+ @sub_plans ||= world.persistence.find_execution_plans(filters: filters)
160
+ else
161
+ world.persistence.find_execution_plans(filters: filters.merge(filter))
162
+ end
151
163
  end
152
164
 
153
165
  def notify_on_finish(plans)
@@ -115,6 +115,14 @@ module Dynflow
115
115
  { 'hostname' => Socket.gethostname, 'pid' => Process.pid }
116
116
  end
117
117
 
118
+ config_attr :backup_deleted_plans, Algebrick::Types::Boolean do
119
+ false
120
+ end
121
+
122
+ config_attr :backup_dir, String, NilClass do
123
+ './backup'
124
+ end
125
+
118
126
  def validate(config_for_world)
119
127
  if defined? ::ActiveRecord::Base
120
128
  ar_pool_size = ::ActiveRecord::Base.connection_pool.instance_variable_get(:@size)
@@ -260,12 +260,13 @@ module Dynflow
260
260
  # sends the cancel event to all currently running and cancellable steps.
261
261
  # if the plan is just scheduled, it cancels it (and returns an one-item
262
262
  # array with the future value of the cancel result)
263
- def cancel
263
+ def cancel(force = false)
264
264
  if state == :scheduled
265
265
  [Concurrent.future.tap { |f| f.success delay_record.cancel }]
266
266
  else
267
+ event = force ? ::Dynflow::Action::Cancellable::Abort : ::Dynflow::Action::Cancellable::Cancel
267
268
  steps_to_cancel.map do |step|
268
- world.event(id, step.id, ::Dynflow::Action::Cancellable::Cancel)
269
+ world.event(id, step.id, event)
269
270
  end
270
271
  end
271
272
  end
@@ -58,7 +58,7 @@ module Dynflow
58
58
  def on_message(message)
59
59
  super
60
60
  rescue Errors::PersistenceError => e
61
- self.tell(:handle_persistence_error, e)
61
+ self.tell([:handle_persistence_error, e])
62
62
  end
63
63
 
64
64
  def feed_pool(work_items)
@@ -54,7 +54,7 @@ module Dynflow
54
54
  end
55
55
 
56
56
  def handle_persistence_error(error)
57
- @executor_core.tell(:handle_persistence_error, error)
57
+ @executor_core.tell([:handle_persistence_error, error])
58
58
  end
59
59
 
60
60
  def start_termination(*args)
@@ -8,10 +8,12 @@ module Dynflow
8
8
 
9
9
  attr_reader :adapter
10
10
 
11
- def initialize(world, persistence_adapter)
11
+ def initialize(world, persistence_adapter, options = {})
12
12
  @world = world
13
13
  @adapter = persistence_adapter
14
14
  @adapter.register_world(world)
15
+ @backup_deleted_plans = options.fetch(:backup_deleted_plans, false)
16
+ @backup_dir = options.fetch(:backup_dir, './backup')
15
17
  end
16
18
 
17
19
  def load_action(step)
@@ -38,8 +40,13 @@ module Dynflow
38
40
  end
39
41
  end
40
42
 
41
- def delete_execution_plans(filters, batch_size = 1000)
42
- adapter.delete_execution_plans(filters, batch_size)
43
+ def delete_execution_plans(filters, batch_size = 1000, enforce_backup_dir = nil)
44
+ backup_dir = enforce_backup_dir || current_backup_dir
45
+ adapter.delete_execution_plans(filters, batch_size, backup_dir)
46
+ end
47
+
48
+ def current_backup_dir
49
+ @backup_deleted_plans ? File.join(@backup_dir, Date.today.strftime('%Y%m%d')) : nil
43
50
  end
44
51
 
45
52
  def load_execution_plan(id)
@@ -43,7 +43,9 @@ module Dynflow
43
43
  # what to delete
44
44
  # @param batch_size the size of the chunks to iterate over when
45
45
  # performing the deletion
46
- def delete_execution_plans(filters, batch_size = 1000)
46
+ # @param backup_dir where the backup of deleted plans will be created.
47
+ # Set to nil for no backup
48
+ def delete_execution_plans(filters, batch_size = 1000, backup_dir = nil)
47
49
  raise NotImplementedError
48
50
  end
49
51
 
@@ -1,5 +1,7 @@
1
1
  require 'sequel'
2
2
  require 'multi_json'
3
+ require 'fileutils'
4
+ require 'csv'
3
5
 
4
6
  module Dynflow
5
7
  module PersistenceAdapters
@@ -58,15 +60,24 @@ module Dynflow
58
60
  data_set.all.map { |record| load_data(record) }
59
61
  end
60
62
 
61
- def delete_execution_plans(filters, batch_size = 1000)
63
+ def delete_execution_plans(filters, batch_size = 1000, backup_dir = nil)
62
64
  count = 0
63
65
  filter(:execution_plan, table(:execution_plan), filters).each_slice(batch_size) do |plans|
64
66
  uuids = plans.map { |p| p.fetch(:uuid) }
65
67
  @db.transaction do
66
68
  table(:delayed).where(execution_plan_uuid: uuids).delete
67
- table(:step).where(execution_plan_uuid: uuids).delete
68
- table(:action).where(execution_plan_uuid: uuids).delete
69
- count += table(:execution_plan).where(uuid: uuids).delete
69
+
70
+ steps = table(:step).where(execution_plan_uuid: uuids)
71
+ backup_to_csv(steps, backup_dir, 'steps.csv') if backup_dir
72
+ steps.delete
73
+
74
+ actions = table(:action).where(execution_plan_uuid: uuids)
75
+ backup_to_csv(actions, backup_dir, 'actions.csv') if backup_dir
76
+ actions.delete
77
+
78
+ execution_plans = table(:execution_plan).where(uuid: uuids)
79
+ backup_to_csv(execution_plans, backup_dir, 'execution_plans.csv') if backup_dir
80
+ count += execution_plans.delete
70
81
  end
71
82
  end
72
83
  return count
@@ -270,6 +281,24 @@ module Dynflow
270
281
  Utils.indifferent_hash(MultiJson.load(record[:data]))
271
282
  end
272
283
 
284
+ def ensure_backup_dir(backup_dir)
285
+ FileUtils.mkdir_p(backup_dir) unless File.directory?(backup_dir)
286
+ end
287
+
288
+ def backup_to_csv(dataset, backup_dir, file_name)
289
+ ensure_backup_dir(backup_dir)
290
+ csv_file = File.join(backup_dir, file_name)
291
+ appending = File.exist?(csv_file)
292
+ columns = dataset.columns
293
+ File.open(csv_file, 'a') do |csv|
294
+ csv << columns.to_csv unless appending
295
+ dataset.each do |row|
296
+ csv << columns.collect { |col| row[col] }.to_csv
297
+ end
298
+ end
299
+ dataset
300
+ end
301
+
273
302
  def delete(what, condition)
274
303
  table(what).where(Utils.symbolize_keys(condition)).delete
275
304
  end
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = '0.8.25'
2
+ VERSION = '0.8.26'
3
3
  end
data/lib/dynflow/world.rb CHANGED
@@ -17,7 +17,9 @@ module Dynflow
17
17
  @logger_adapter = config_for_world.logger_adapter
18
18
  config_for_world.validate
19
19
  @transaction_adapter = config_for_world.transaction_adapter
20
- @persistence = Persistence.new(self, config_for_world.persistence_adapter)
20
+ @persistence = Persistence.new(self, config_for_world.persistence_adapter,
21
+ :backup_deleted_plans => config_for_world.backup_deleted_plans,
22
+ :backup_dir => config_for_world.backup_dir)
21
23
  @coordinator = Coordinator.new(config_for_world.coordinator_adapter)
22
24
  @executor = config_for_world.executor
23
25
  @action_classes = config_for_world.action_classes
data/test/action_test.rb CHANGED
@@ -371,10 +371,56 @@ module Dynflow
371
371
  if FailureSimulator.fail_in_child_run
372
372
  raise "Fail in child run"
373
373
  end
374
- if input[:suspend] && !(event == Dynflow::Action::Cancellable::Cancel)
374
+ if event == Dynflow::Action::Cancellable::Abort
375
+ output[:aborted] = true
376
+ end
377
+ if input[:suspend] && !cancel_event?(event)
375
378
  suspend
376
379
  end
377
380
  end
381
+
382
+ def cancel_event?(event)
383
+ event == Dynflow::Action::Cancellable::Cancel ||
384
+ event == Dynflow::Action::Cancellable::Abort
385
+ end
386
+ end
387
+
388
+ class PollingParentAction < ParentAction
389
+ include ::Dynflow::Action::WithPollingSubPlans
390
+ end
391
+
392
+ class PollingBulkParentAction < ParentAction
393
+ include ::Dynflow::Action::WithBulkSubPlans
394
+ include ::Dynflow::Action::WithPollingSubPlans
395
+
396
+ def poll
397
+ output[:poll] += 1
398
+ super
399
+ end
400
+
401
+ def on_planning_finished
402
+ output[:poll] = 0
403
+ output[:planning_finished] ||= 0
404
+ output[:planning_finished] += 1
405
+ super
406
+ end
407
+
408
+ def total_count
409
+ input[:count]
410
+ end
411
+
412
+ def batch_size
413
+ 1
414
+ end
415
+
416
+ def create_sub_plans
417
+ current_batch.map { trigger(ChildAction, suspend: input[:suspend]) }
418
+ end
419
+
420
+ def batch(from, size)
421
+ total_count.times.drop(from).take(size)
422
+ end
423
+
378
424
  end
379
425
 
380
426
  let(:execution_plan) { world.trigger(ParentAction, count: 2).finished.value }
@@ -442,6 +488,96 @@ module Dynflow
442
488
  resumed_plan.state.must_equal :stopped
443
489
  resumed_plan.result.must_equal :success
444
490
  end
491
+
492
+ describe ::Dynflow::Action::WithPollingSubPlans do
493
+ include TestHelpers
494
+
495
+ let(:clock) { Dynflow::Testing::ManagedClock.new }
496
+ let(:polling_plan) { world.trigger(PollingParentAction, count: 2).finished.value }
497
+
498
+ specify "by default, when no sub plans were planned successfully, it calls create_sub_plans again" do
499
+ world.stub(:clock, clock) do
500
+ total = 2
501
+ FailureSimulator.fail_in_child_plan = true
502
+ triggered_plan = world.trigger(PollingParentAction, count: total)
503
+ polling_plan = world.persistence.load_execution_plan(triggered_plan.id)
504
+
505
+ wait_for do # Waiting for the sub plans to be spawned
506
+ polling_plan = world.persistence.load_execution_plan(triggered_plan.id)
507
+ polling_plan.sub_plans.count == total
508
+ end
509
+
510
+ # Moving the clock to make the parent check on sub plans
511
+ clock.pending_pings.count.must_equal 1
512
+ clock.progress
513
+
514
+ wait_for do # Waiting for the parent to realise the sub plans failed
515
+ polling_plan = world.persistence.load_execution_plan(triggered_plan.id)
516
+ polling_plan.state == :paused
517
+ end
518
+
519
+ FailureSimulator.fail_in_child_plan = false
520
+
521
+ world.execute(polling_plan.id) # The actual resume
522
+
523
+ wait_for do # Waiting for new generation of sub plans to be spawned
524
+ polling_plan.sub_plans.count == 2 * total
525
+ end
526
+
527
+ # Move the clock again
528
+ clock.pending_pings.count.must_equal 1
529
+ clock.progress
530
+
531
+ wait_for do # Waiting for everything to finish successfully
532
+ polling_plan = world.persistence.load_execution_plan(triggered_plan.id)
533
+ polling_plan.state == :stopped && polling_plan.result == :success
534
+ end
535
+ end
536
+ end
537
+
538
+ specify "by default it starts polling again" do
539
+ world.stub(:clock, clock) do
540
+ total = 2
541
+ FailureSimulator.fail_in_child_run = true
542
+ triggered_plan = world.trigger(PollingParentAction, count: total)
543
+ polling_plan = world.persistence.load_execution_plan(triggered_plan.id)
544
+
545
+ wait_for do # Waiting for the sub plans to be spawned
546
+ polling_plan = world.persistence.load_execution_plan(triggered_plan.id)
547
+ polling_plan.sub_plans.count == total &&
548
+ polling_plan.sub_plans.all? { |sub| sub.state == :paused }
549
+ end
550
+
551
+ # Moving the clock to make the parent check on sub plans
552
+ clock.pending_pings.count.must_equal 1
553
+ clock.progress
554
+ clock.pending_pings.count.must_equal 0
555
+
556
+ wait_for do # Waiting for the parent to realise the sub plans failed
557
+ polling_plan = world.persistence.load_execution_plan(triggered_plan.id)
558
+ polling_plan.state == :paused
559
+ end
560
+
561
+ FailureSimulator.fail_in_child_run = false
562
+
563
+ # Resume the sub plans
564
+ polling_plan.sub_plans.each do |sub|
565
+ world.execute(sub.id)
566
+ end
567
+
568
+ wait_for do # Waiting for the child tasks to finish
569
+ polling_plan.sub_plans.all? { |sub| sub.state == :stopped }
570
+ end
571
+
572
+ world.execute(polling_plan.id) # The actual resume
573
+
574
+ wait_for do # Waiting for everything to finish successfully
575
+ polling_plan = world.persistence.load_execution_plan(triggered_plan.id)
576
+ polling_plan.state == :stopped && polling_plan.result == :success
577
+ end
578
+ end
579
+ end
580
+ end
445
581
  end
446
582
 
447
583
  describe 'cancelling' do
@@ -460,6 +596,81 @@ module Dynflow
460
596
  triggered_plan.finished.value.state.must_equal :stopped
461
597
  triggered_plan.finished.value.result.must_equal :success
462
598
  end
599
+
600
+ it "sends the abort event to all actions that are running and support cancelling" do
601
+ triggered_plan = world.trigger(ParentAction, count: 2, suspend: true)
602
+ plan = wait_for do
603
+ plan = world.persistence.load_execution_plan(triggered_plan.id)
604
+ if plan.cancellable?
605
+ plan
606
+ end
607
+ end
608
+ plan.cancel true
609
+ triggered_plan.finished.wait
610
+ triggered_plan.finished.value.state.must_equal :stopped
611
+ triggered_plan.finished.value.result.must_equal :success
612
+ plan.sub_plans.each do |sub_plan|
613
+ sub_plan.entry_action.output[:aborted].must_equal true
614
+ end
615
+ end
616
+ end
617
+
618
+ describe ::Dynflow::Action::WithPollingSubPlans do
619
+ include TestHelpers
620
+
621
+ let(:clock) { Dynflow::Testing::ManagedClock.new }
622
+
623
+ specify 'polls for sub plans state' do
624
+ world.stub :clock, clock do
625
+ total = 2
626
+ triggered_plan = world.trigger(PollingParentAction, count: total)
627
+ plan = world.persistence.load_execution_plan(triggered_plan.id)
628
+ plan.state.must_equal :planned
629
+ clock.pending_pings.count.must_equal 0
630
+ wait_for do
631
+ plan.sub_plans.count == total &&
632
+ plan.sub_plans.all? { |sub| sub.result == :success }
633
+ end
634
+ clock.pending_pings.count.must_equal 1
635
+ clock.progress
636
+ wait_for do
637
+ plan = world.persistence.load_execution_plan(triggered_plan.id)
638
+ plan.state == :stopped
639
+ end
640
+ clock.pending_pings.count.must_equal 0
641
+ end
642
+ end
643
+
644
+ specify 'starts polling for sub plans at the beginning' do
645
+ world.stub :clock, clock do
646
+ total = 2
647
+ triggered_plan = world.trigger(PollingBulkParentAction, count: total)
648
+ plan = world.persistence.load_execution_plan(triggered_plan.id)
649
+ assert_nil plan.entry_action.output[:planning_finished]
650
+ clock.pending_pings.count.must_equal 0
651
+ wait_for do
652
+ plan = world.persistence.load_execution_plan(triggered_plan.id)
653
+ plan.entry_action.output[:planning_finished] == 1
654
+ end
655
+ # Poll was set during #initiate
656
+ clock.pending_pings.count.must_equal 1
657
+
658
+ # Wait for the sub plans to finish
659
+ wait_for do
660
+ plan.sub_plans.count == total &&
661
+ plan.sub_plans.all? { |sub| sub.result == :success }
662
+ end
663
+
664
+ # Poll again
665
+ clock.progress
666
+ wait_for do
667
+ plan = world.persistence.load_execution_plan(triggered_plan.id)
668
+ plan.state == :stopped
669
+ end
670
+ plan.entry_action.output[:poll].must_equal 1
671
+ clock.pending_pings.count.must_equal 0
672
+ end
673
+ end
463
674
  end
464
675
  end
465
676
  end
@@ -300,6 +300,20 @@ module Dynflow
300
300
  cancel_events.each(&:wait)
301
301
  finished.wait
302
302
  end
303
+
304
+ it 'force cancels' do
305
+ finished = world.execute(execution_plan.id)
306
+ plan = wait_for do
307
+ plan = world.persistence.load_execution_plan(execution_plan.id)
308
+ if plan.cancellable?
309
+ plan
310
+ end
311
+ end
312
+ cancel_events = plan.cancel true
313
+ cancel_events.size.must_equal 1
314
+ cancel_events.each(&:wait)
315
+ finished.wait
316
+ end
303
317
  end
304
318
 
305
319
  describe 'accessing actions results' do
@@ -1,4 +1,5 @@
1
1
  require_relative 'test_helper'
2
+ require 'tmpdir'
2
3
 
3
4
  module Dynflow
4
5
  module PersistenceTest
@@ -154,6 +155,22 @@ module Dynflow
154
155
  adapter.load_execution_plan('plan2') # nothing raised
155
156
  -> { adapter.load_execution_plan('plan3') }.must_raise KeyError
156
157
  end
158
+
159
+ it 'creates backup dir and produce backup including steps and actions' do
160
+ prepare_plans_with_steps
161
+ Dir.mktmpdir do |backup_dir|
162
+ adapter.delete_execution_plans({'uuid' => 'plan1'}, 100, backup_dir).must_equal 1
163
+ plans = CSV.read(backup_dir + "/execution_plans.csv", :headers => true)
164
+ assert_equal 1, plans.count
165
+ assert_equal 'plan1', plans.first.to_hash['uuid']
166
+ actions = CSV.read(backup_dir + "/actions.csv", :headers => true)
167
+ assert_equal 1, actions.count
168
+ assert_equal 'plan1', actions.first.to_hash['execution_plan_uuid']
169
+ steps = CSV.read(backup_dir +"/steps.csv", :headers => true)
170
+ assert_equal 1, steps.count
171
+ assert_equal 'plan1', steps.first.to_hash['execution_plan_uuid']
172
+ end
173
+ end
157
174
  end
158
175
 
159
176
  describe '#load_action and #save_action' do
data/test/test_helper.rb CHANGED
@@ -82,16 +82,18 @@ module WorldFactory
82
82
  end
83
83
 
84
84
  def self.test_world_config
85
- config = Dynflow::Config.new
86
- config.persistence_adapter = persistence_adapter
87
- config.logger_adapter = logger_adapter
88
- config.coordinator_adapter = coordinator_adapter
89
- config.delayed_executor = nil
90
- config.auto_rescue = false
91
- config.auto_validity_check = false
92
- config.exit_on_terminate = false
93
- config.auto_execute = false
94
- config.auto_terminate = false
85
+ config = Dynflow::Config.new
86
+ config.persistence_adapter = persistence_adapter
87
+ config.logger_adapter = logger_adapter
88
+ config.coordinator_adapter = coordinator_adapter
89
+ config.delayed_executor = nil
90
+ config.auto_rescue = false
91
+ config.auto_validity_check = false
92
+ config.exit_on_terminate = false
93
+ config.auto_execute = false
94
+ config.auto_terminate = false
95
+ config.backup_deleted_plans = false
96
+ config.backup_dir = nil
95
97
  yield config if block_given?
96
98
  return config
97
99
  end
@@ -0,0 +1 @@
1
+ web/assets/images/../../../doc/pages/source/images/logo-square.png
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: 0.8.25
4
+ version: 0.8.26
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: 2017-07-19 00:00:00.000000000 Z
12
+ date: 2017-08-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: multi_json
@@ -390,6 +390,7 @@ files:
390
390
  - lib/dynflow/action/suspended.rb
391
391
  - lib/dynflow/action/timeouts.rb
392
392
  - lib/dynflow/action/with_bulk_sub_plans.rb
393
+ - lib/dynflow/action/with_polling_sub_plans.rb
393
394
  - lib/dynflow/action/with_sub_plans.rb
394
395
  - lib/dynflow/active_job/queue_adapter.rb
395
396
  - lib/dynflow/actor.rb
@@ -589,7 +590,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
589
590
  version: '0'
590
591
  requirements: []
591
592
  rubyforge_project:
592
- rubygems_version: 2.4.5
593
+ rubygems_version: 2.6.11
593
594
  signing_key:
594
595
  specification_version: 4
595
596
  summary: DYNamic workFLOW engine
Binary file