dynflow 0.8.25 → 0.8.26

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
  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