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 +4 -4
- data/examples/sub_plans.rb +28 -5
- data/lib/dynflow/action.rb +1 -0
- data/lib/dynflow/action/cancellable.rb +9 -1
- data/lib/dynflow/action/with_bulk_sub_plans.rb +16 -5
- data/lib/dynflow/action/with_polling_sub_plans.rb +72 -0
- data/lib/dynflow/action/with_sub_plans.rb +16 -4
- data/lib/dynflow/config.rb +8 -0
- data/lib/dynflow/execution_plan.rb +3 -2
- data/lib/dynflow/executors/parallel/core.rb +1 -1
- data/lib/dynflow/executors/parallel/pool.rb +1 -1
- data/lib/dynflow/persistence.rb +10 -3
- data/lib/dynflow/persistence_adapters/abstract.rb +3 -1
- data/lib/dynflow/persistence_adapters/sequel.rb +33 -4
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/world.rb +3 -1
- data/test/action_test.rb +212 -1
- data/test/execution_plan_test.rb +14 -0
- data/test/persistence_test.rb +17 -0
- data/test/test_helper.rb +12 -10
- data/web/assets/images/logo-square.png +1 -0
- metadata +4 -3
- data/web/assets/images/logo-square.png +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab4978b454a2f7066506b1abcf644184fcb5542b
|
4
|
+
data.tar.gz: db7286bce7f99037e24df8dedc227a85026d1d77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8df6257ff673232080c2867715ee5605d07bd197e2a1f950ed3f13a2fc88f9f8ce3415d47c88b3ef88a4523a0c8e4ccf7f81b435e90b6d694e758335b0ebbeb9
|
7
|
+
data.tar.gz: bf75b46a23911444fd9cb087d7403a2ff79c20f83bd6f091502cacc2e6628211315465ccb9e5e9bfa04de65cfe020d79f1db8870e233dfb67defbcfdd9a049e0
|
data/examples/sub_plans.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
32
|
-
| You can see the details at
|
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
|
data/lib/dynflow/action.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
17
|
-
|
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
|
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(
|
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
|
-
|
150
|
-
|
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)
|
data/lib/dynflow/config.rb
CHANGED
@@ -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,
|
269
|
+
world.event(id, step.id, event)
|
269
270
|
end
|
270
271
|
end
|
271
272
|
end
|
data/lib/dynflow/persistence.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
68
|
-
table(:
|
69
|
-
|
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
|
data/lib/dynflow/version.rb
CHANGED
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
|
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
|
data/test/execution_plan_test.rb
CHANGED
@@ -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
|
data/test/persistence_test.rb
CHANGED
@@ -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
|
86
|
-
config.persistence_adapter
|
87
|
-
config.logger_adapter
|
88
|
-
config.coordinator_adapter
|
89
|
-
config.delayed_executor
|
90
|
-
config.auto_rescue
|
91
|
-
config.auto_validity_check
|
92
|
-
config.exit_on_terminate
|
93
|
-
config.auto_execute
|
94
|
-
config.auto_terminate
|
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.
|
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-
|
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.
|
593
|
+
rubygems_version: 2.6.11
|
593
594
|
signing_key:
|
594
595
|
specification_version: 4
|
595
596
|
summary: DYNamic workFLOW engine
|
Binary file
|