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