dynflow 1.6.11 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db2725603e440298025d434778ebf2e218e6c3e30fb7db07aaddea80648cbb2d
4
- data.tar.gz: 4d8003de94bec301bd411c1c3ee92df9e1d10cc45bc36d4d843a75c2eb664171
3
+ metadata.gz: f067dee815c7352328f4a08c6cf7aa5f60c45e8c46ae0bb561ed3eff052a3f47
4
+ data.tar.gz: 37d1bf9a5bed948a9c0c966ac8649d6a1ba097801fcf3b3608a6f353e3b8cd14
5
5
  SHA512:
6
- metadata.gz: ab0fa706aa8b41c1197aef44bf57a87f4e5c37d7d343a9ca7e83e20af8de89b4e0fb2e72e7a1b93fffa05ac3265c78a48d598d7b8e0292785cd2276057edd0f0
7
- data.tar.gz: 07e82b78806a61511b0ddff0240dd596172dfe79735bc8b931d7972b4d322d29bca1686be8be0087a105171e7348c0ee24cfdb2b01cc1a8dcf8d97ad8b0134c8
6
+ metadata.gz: 3eb25b7a20ac5022e0f1f32062ac6d7445d5fbe2fb482e5d34278a402f49e5feb53e6a7116f5fb06b1758faa6ea253fdda496e76d94dac0a6e2e497653c98fe8
7
+ data.tar.gz: 242ade0c666d77988957f473ed76aebef323f985adaa4a851c0620c14768fc19db7ef0087d0ab2d2983059aa0fd671721d377dfb9531d00c8f5a4a9b6a8380d2
@@ -35,10 +35,9 @@ jobs:
35
35
  fail-fast: false
36
36
  matrix:
37
37
  ruby_version:
38
- - 2.5.0
39
- - 2.6.0
40
38
  - 2.7.0
41
39
  - 3.0.0
40
+ - 3.2.0
42
41
  concurrent_ruby_ext:
43
42
  - 'true'
44
43
  - 'false'
@@ -54,30 +53,23 @@ jobs:
54
53
  - db: sqlite3
55
54
  conn_string: sqlite:/
56
55
  exclude:
57
- - db: mysql
58
- ruby_version: 2.5.0
59
- - db: mysql
60
- ruby_version: 2.6.0
61
56
  - db: mysql
62
57
  ruby_version: 3.0.0
58
+ - db: mysql
59
+ ruby_version: 3.2.0
63
60
  - db: mysql
64
61
  concurrent_ruby_ext: 'true'
65
- - db: sqlite3
66
- ruby_version: 2.5.0
67
- - db: sqlite3
68
- ruby_version: 2.6.0
69
62
  - db: sqlite3
70
63
  ruby_version: 3.0.0
64
+ - db: sqlite3
65
+ ruby_version: 3.2.0
71
66
  - db: sqlite3
72
67
  concurrent_ruby_ext: 'true'
73
68
  - db: postgresql
74
- ruby_version: 2.5.0
75
- concurrent_ruby_ext: 'true'
76
- - db: postgresql
77
- ruby_version: 2.6.0
69
+ ruby_version: 3.0.0
78
70
  concurrent_ruby_ext: 'true'
79
71
  - db: postgresql
80
- ruby_version: 3.0.0
72
+ ruby_version: 3.2.0
81
73
  concurrent_ruby_ext: 'true'
82
74
 
83
75
  services:
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ example_description = <<DESC
4
+ Sub Plans Example
5
+ ===================
6
+
7
+ This example shows, how to trigger the execution plans from within a
8
+ run method of some action and waing for them to finish.
9
+
10
+ This is useful, when doing bulk actions, where having one big
11
+ execution plan would not be effective, or in case all the data are
12
+ not available by the time of original action planning.
13
+
14
+ DESC
15
+
16
+ require_relative 'example_helper'
17
+ require_relative 'orchestrate_evented'
18
+
19
+ COUNT = (ARGV[0] || 25).to_i
20
+
21
+ class Foo < Dynflow::Action
22
+ def plan
23
+ plan_self
24
+ end
25
+
26
+ def run(event = nil)
27
+ case event
28
+ when nil
29
+ rng = Random.new
30
+ plan_event(:ping, rng.rand(25) + 1)
31
+ suspend
32
+ when :ping
33
+ # Finish
34
+ end
35
+ end
36
+ end
37
+
38
+ class SubPlansExample < Dynflow::Action
39
+ include Dynflow::Action::V2::WithSubPlans
40
+
41
+ def initiate
42
+ limit_concurrency_level! 3
43
+ super
44
+ end
45
+
46
+ def create_sub_plans
47
+ current_batch.map { |i| trigger(Foo) }
48
+ end
49
+
50
+ def batch_size
51
+ 15
52
+ end
53
+
54
+ def batch(from, size)
55
+ COUNT.times.drop(from).take(size)
56
+ end
57
+
58
+ def total_count
59
+ COUNT
60
+ end
61
+ end
62
+
63
+ if $0 == __FILE__
64
+ ExampleHelper.world.action_logger.level = Logger::DEBUG
65
+ ExampleHelper.world
66
+ t1 = ExampleHelper.world.trigger(SubPlansExample)
67
+ puts example_description
68
+ puts <<-MSG.gsub(/^.*\|/, '')
69
+ | Execution plans #{t1.id} with sub plans triggered
70
+ | You can see the details at
71
+ | #{ExampleHelper::DYNFLOW_URL}/#{t1.id}
72
+ MSG
73
+
74
+ ExampleHelper.run_web_console
75
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynflow::Action::V2
4
+ module WithSubPlans
5
+ include Dynflow::Action::Cancellable
6
+
7
+ DEFAULT_BATCH_SIZE = 100
8
+ DEFAULT_POLLING_INTERVAL = 15
9
+ Ping = Algebrick.atom
10
+
11
+ class SubtaskFailedException < RuntimeError
12
+ def backtrace
13
+ []
14
+ end
15
+ end
16
+
17
+ # Methods to be overridden
18
+ def create_sub_plans
19
+ raise NotImplementedError
20
+ end
21
+
22
+ # Should return the expected total count of tasks
23
+ def total_count
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def batch_size
28
+ DEFAULT_BATCH_SIZE
29
+ end
30
+
31
+ # Should return a slice of size items starting from item with index from
32
+ def batch(from, size)
33
+ raise NotImplementedError
34
+ end
35
+
36
+ # Polling
37
+ def polling_interval
38
+ DEFAULT_POLLING_INTERVAL
39
+ end
40
+
41
+ # Callbacks
42
+ def on_finish
43
+ end
44
+
45
+ def on_planning_finished
46
+ end
47
+
48
+ def run(event = nil)
49
+ case event
50
+ when nil
51
+ if output[:total_count]
52
+ resume
53
+ else
54
+ initiate
55
+ end
56
+ when Ping
57
+ tick
58
+ when ::Dynflow::Action::Cancellable::Cancel
59
+ cancel!
60
+ when ::Dynflow::Action::Cancellable::Abort
61
+ abort!
62
+ end
63
+ try_to_finish || suspend_and_ping
64
+ end
65
+
66
+ def initiate
67
+ output[:planned_count] = 0
68
+ output[:cancelled_count] = 0
69
+ output[:total_count] = total_count
70
+ spawn_plans
71
+ end
72
+
73
+ def resume
74
+ if sub_plans.all? { |sub_plan| sub_plan.error_in_plan? }
75
+ output[:resumed_count] ||= 0
76
+ output[:resumed_count] += output[:failed_count]
77
+ # We're starting over and need to reset the counts
78
+ %w(total failed pending success).each { |key| output.delete("#{key}_count".to_sym) }
79
+ initiate
80
+ else
81
+ tick
82
+ end
83
+ end
84
+
85
+ def tick
86
+ recalculate_counts
87
+ spawn_plans if can_spawn_next_batch?
88
+ end
89
+
90
+ def suspend_and_ping
91
+ delay = (concurrency_limit.nil? || concurrency_limit_capacity > 0) && can_spawn_next_batch? ? nil : polling_interval
92
+ plan_event(Ping, delay)
93
+ suspend
94
+ end
95
+
96
+ def spawn_plans
97
+ sub_plans = create_sub_plans
98
+ sub_plans = Array[sub_plans] unless sub_plans.is_a? Array
99
+ increase_counts(sub_plans.count, 0)
100
+ on_planning_finished unless can_spawn_next_batch?
101
+ end
102
+
103
+ def increase_counts(planned, failed)
104
+ output[:planned_count] += planned + failed
105
+ output[:failed_count] = output.fetch(:failed_count, 0) + failed
106
+ output[:pending_count] = output.fetch(:pending_count, 0) + planned
107
+ output[:success_count] ||= 0
108
+ end
109
+
110
+ def try_to_finish
111
+ return false unless done?
112
+
113
+ check_for_errors!
114
+ on_finish
115
+ true
116
+ end
117
+
118
+ def done?
119
+ return false if can_spawn_next_batch? || !counts_set?
120
+
121
+ total_count - output[:success_count] - output[:failed_count] - output[:cancelled_count] <= 0
122
+ end
123
+
124
+ def run_progress
125
+ return 0.1 unless counts_set? && total_count > 0
126
+
127
+ sum = output.values_at(:success_count, :cancelled_count, :failed_count).reduce(:+)
128
+ sum.to_f / total_count
129
+ end
130
+
131
+ def recalculate_counts
132
+ total = total_count
133
+ failed = sub_plans_count('state' => %w(paused stopped), 'result' => 'error')
134
+ success = sub_plans_count('state' => 'stopped', 'result' => 'success')
135
+ output.update(:pending_count => total - failed - success,
136
+ :failed_count => failed - output.fetch(:resumed_count, 0),
137
+ :success_count => success)
138
+ end
139
+
140
+ def counts_set?
141
+ output[:total_count] && output[:success_count] && output[:failed_count] && output[:pending_count]
142
+ end
143
+
144
+ def check_for_errors!
145
+ raise SubtaskFailedException.new("A sub task failed") if output[:failed_count] > 0
146
+ end
147
+
148
+ # Helper for creating sub plans
149
+ def trigger(action_class, *args)
150
+ world.trigger { world.plan_with_options(action_class: action_class, args: args, caller_action: self) }
151
+ end
152
+
153
+ # Concurrency limitting
154
+ def limit_concurrency_level!(level)
155
+ input[:dynflow] ||= {}
156
+ input[:dynflow][:concurrency_limit] = level
157
+ end
158
+
159
+ def concurrency_limit
160
+ input[:dynflow] ||= {}
161
+ input[:dynflow][:concurrency_limit]
162
+ end
163
+
164
+ def concurrency_limit_capacity
165
+ if limit = concurrency_limit
166
+ return limit unless counts_set?
167
+ capacity = limit - (output[:planned_count] - (output[:success_count] + output[:failed_count]))
168
+ [0, capacity].max
169
+ end
170
+ end
171
+
172
+ # Cancellation handling
173
+ def cancel!(force = false)
174
+ # Count the not-yet-planned tasks as cancelled
175
+ output[:cancelled_count] = total_count - output[:planned_count]
176
+ # Pass the cancel event to running sub plans if they can be cancelled
177
+ sub_plans(:state => 'running').each { |sub_plan| sub_plan.cancel(force) if sub_plan.cancellable? }
178
+ suspend
179
+ end
180
+
181
+ def abort!
182
+ cancel! true
183
+ end
184
+
185
+ # Batching
186
+ # Returns the items in the current batch
187
+ def current_batch
188
+ start_position = output[:planned_count]
189
+ size = batch_size
190
+ size = concurrency_limit_capacity if concurrency_limit
191
+ size = start_position + size > total_count ? total_count - start_position : size
192
+ batch(start_position, size)
193
+ end
194
+
195
+ def can_spawn_next_batch?
196
+ remaining_count > 0
197
+ end
198
+
199
+ def remaining_count
200
+ total_count - output[:cancelled_count] - output[:planned_count]
201
+ end
202
+
203
+ private
204
+
205
+ # Sub-plan lookup
206
+ def sub_plan_filter
207
+ { 'caller_execution_plan_id' => execution_plan_id,
208
+ 'caller_action_id' => self.id }
209
+ end
210
+
211
+ def sub_plans(filter = {})
212
+ world.persistence.find_execution_plans(filters: sub_plan_filter.merge(filter))
213
+ end
214
+
215
+ def sub_plans_count(filter = {})
216
+ world.persistence.find_execution_plan_counts(filters: sub_plan_filter.merge(filter))
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynflow
4
+ class Action
5
+ module V2
6
+ require 'dynflow/action/v2/with_sub_plans'
7
+ end
8
+ end
9
+ end
@@ -26,6 +26,7 @@ module Dynflow
26
26
  require 'dynflow/action/with_sub_plans'
27
27
  require 'dynflow/action/with_bulk_sub_plans'
28
28
  require 'dynflow/action/with_polling_sub_plans'
29
+ require 'dynflow/action/v2'
29
30
 
30
31
  def self.all_children
31
32
  children.values.inject(children.values) do |children, child|
@@ -179,6 +180,12 @@ module Dynflow
179
180
  @output_chunks ||= world.persistence.load_output_chunks(@execution_plan_id, @id)
180
181
  end
181
182
 
183
+ def drop_output_chunks!
184
+ @pending_output_chunks = []
185
+ @output_chunks = []
186
+ world.persistence.delete_output_chunks(@execution_plan_id, @id)
187
+ end
188
+
182
189
  def caller_action
183
190
  phase! Present
184
191
  return nil if @caller_action_id
@@ -56,12 +56,20 @@ module Dynflow
56
56
  adapter.load_output_chunks(execution_plan_id, action_id)
57
57
  end
58
58
 
59
+ def delete_output_chunks(execution_plan_id, action_id)
60
+ adapter.delete_output_chunks(execution_plan_id, action_id)
61
+ end
62
+
59
63
  def find_execution_plans(options)
60
64
  adapter.find_execution_plans(options).map do |execution_plan_hash|
61
65
  ExecutionPlan.new_from_hash(execution_plan_hash, @world)
62
66
  end
63
67
  end
64
68
 
69
+ def find_execution_plan_statuses(options)
70
+ adapter.find_execution_plan_statuses(options)
71
+ end
72
+
65
73
  def find_execution_plan_counts(options)
66
74
  adapter.find_execution_plan_counts(options)
67
75
  end
@@ -46,6 +46,10 @@ module Dynflow
46
46
  filter(:execution_plan, options[:filters]).count
47
47
  end
48
48
 
49
+ def find_execution_plan_statuses(options)
50
+ raise NotImplementedError
51
+ end
52
+
49
53
  # @param filters [Hash{ String => Object }] filters to determine
50
54
  # what to delete
51
55
  # @param batch_size the size of the chunks to iterate over when
@@ -104,6 +108,18 @@ module Dynflow
104
108
  raise NotImplementedError
105
109
  end
106
110
 
111
+ def save_output_chunks(execution_plan_id, action_id, chunks)
112
+ raise NotImplementedError
113
+ end
114
+
115
+ def load_output_chunks(execution_plan_id, action_id)
116
+ raise NotImplementedError
117
+ end
118
+
119
+ def delete_output_chunks(execution_plan_id, action_id)
120
+ raise NotImplementedError
121
+ end
122
+
107
123
  # for debug purposes
108
124
  def to_hash
109
125
  raise NotImplementedError
@@ -78,6 +78,16 @@ module Dynflow
78
78
  filter(:execution_plan, table(:execution_plan), options[:filters]).count
79
79
  end
80
80
 
81
+ def find_execution_plan_statuses(options)
82
+ plans = filter(:execution_plan, table(:execution_plan), options[:filters])
83
+ .select(:uuid, :state, :result)
84
+
85
+ plans.each_with_object({}) do |current, acc|
86
+ uuid = current.delete(:uuid)
87
+ acc[uuid] = current
88
+ end
89
+ end
90
+
81
91
  def delete_execution_plans(filters, batch_size = 1000, backup_dir = nil)
82
92
  count = 0
83
93
  filter(:execution_plan, table(:execution_plan), filters).each_slice(batch_size) do |plans|
@@ -190,6 +200,10 @@ module Dynflow
190
200
  load_records :output_chunk, { execution_plan_uuid: execution_plan_id, action_id: action_id }, [:timestamp, :kind, :chunk]
191
201
  end
192
202
 
203
+ def delete_output_chunks(execution_plan_id, action_id)
204
+ filter(:output_chunk, table(:output_chunk), { execution_plan_uuid: execution_plan_id, action_id: action_id }).delete
205
+ end
206
+
193
207
  def connector_feature!
194
208
  unless @additional_responsibilities[:connector]
195
209
  raise "The sequel persistence adapter connector feature used but not enabled in additional_features"
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Dynflow
3
- VERSION = '1.6.11'
3
+ VERSION = '1.8.0'
4
4
  end
data/test/action_test.rb CHANGED
@@ -889,5 +889,48 @@ module Dynflow
889
889
  end
890
890
  end
891
891
  end
892
+
893
+ describe 'output chunks' do
894
+ include ::Dynflow::Testing::Factories
895
+
896
+ class OutputChunkAction < ::Dynflow::Action
897
+ def run(event = nil)
898
+ output[:counter] ||= 0
899
+ case event
900
+ when nil
901
+ output_chunk("Chunk #{output[:counter]}")
902
+ output[:counter] += 1
903
+ suspend
904
+ when :exit
905
+ return
906
+ end
907
+ end
908
+
909
+ def finalize
910
+ drop_output_chunks!
911
+ end
912
+ end
913
+
914
+ it 'collects and drops output chunks' do
915
+ action = create_and_plan_action(OutputChunkAction)
916
+ _(action.pending_output_chunks).must_equal nil
917
+
918
+ action = run_action(action)
919
+ _(action.pending_output_chunks.count).must_equal 1
920
+
921
+ action = run_action(action)
922
+ _(action.pending_output_chunks.count).must_equal 2
923
+
924
+ action = run_action(action, :exit)
925
+ _(action.pending_output_chunks.count).must_equal 2
926
+
927
+ persistence = mock()
928
+ persistence.expects(:delete_output_chunks).with(action.execution_plan_id, action.id)
929
+ action.world.stubs(:persistence).returns(persistence)
930
+
931
+ action = finalize_action(action)
932
+ _(action.pending_output_chunks.count).must_equal 0
933
+ end
934
+ end
892
935
  end
893
936
  end
@@ -159,6 +159,46 @@ module Dynflow
159
159
  end
160
160
  end
161
161
 
162
+ describe '#def find_execution_plan_statuses' do
163
+ before do
164
+ # the tests expect clean field
165
+ adapter.delete_execution_plans({})
166
+ end
167
+
168
+ it 'supports filtering' do
169
+ prepare_and_save_plans
170
+ if adapter.ordering_by.include?('state')
171
+ loaded_plans = adapter.find_execution_plan_statuses(filters: { label: ['test1'] })
172
+ _(loaded_plans).must_equal({ 'plan1' => { state: 'paused', result: nil} })
173
+
174
+ loaded_plans = adapter.find_execution_plan_statuses(filters: { state: ['paused'] })
175
+ _(loaded_plans).must_equal({"plan1"=>{:state=>"paused", :result=>nil},
176
+ "plan3"=>{:state=>"paused", :result=>nil},
177
+ "plan4"=>{:state=>"paused", :result=>nil}})
178
+
179
+ loaded_plans = adapter.find_execution_plan_statuses(filters: { state: ['stopped'] })
180
+ _(loaded_plans).must_equal({"plan2"=>{:state=>"stopped", :result=>nil}})
181
+
182
+ loaded_plans = adapter.find_execution_plan_statuses(filters: { state: [] })
183
+ _(loaded_plans).must_equal({})
184
+
185
+ loaded_plans = adapter.find_execution_plan_statuses(filters: { state: ['stopped', 'paused'] })
186
+ _(loaded_plans).must_equal({"plan1"=>{:state=>"paused", :result=>nil},
187
+ "plan2"=>{:state=>"stopped", :result=>nil},
188
+ "plan3"=>{:state=>"paused", :result=>nil}, "plan4"=>{:state=>"paused", :result=>nil}})
189
+
190
+ loaded_plans = adapter.find_execution_plan_statuses(filters: { 'state' => ['stopped', 'paused'] })
191
+ _(loaded_plans).must_equal({"plan1"=>{:state=>"paused", :result=>nil},
192
+ "plan2"=>{:state=>"stopped", :result=>nil},
193
+ "plan3"=>{:state=>"paused", :result=>nil},
194
+ "plan4"=>{:state=>"paused", :result=>nil}})
195
+
196
+ loaded_plans = adapter.find_execution_plan_statuses(filters: { label: ['test1'], :delayed => true })
197
+ _(loaded_plans).must_equal({})
198
+ end
199
+ end
200
+ end
201
+
162
202
  describe '#def find_execution_plan_counts' do
163
203
  before do
164
204
  # the tests expect clean field
@@ -331,6 +371,20 @@ module Dynflow
331
371
  _(plans.first[:execution_plan_uuid]).must_equal 'plan1'
332
372
  end
333
373
  end
374
+
375
+ describe '#delete_output_chunks' do
376
+ it 'deletes output chunks' do
377
+ prepare_plans_with_actions
378
+
379
+ adapter.save_output_chunks('plan1', 1, [{chunk: "Hello", timestamp: Time.now}, {chunk: "Bye", timestamp: Time.now}])
380
+ chunks = adapter.load_output_chunks('plan1', 1)
381
+ _(chunks.length).must_equal 2
382
+ deleted = adapter.delete_output_chunks('plan1', 1)
383
+ _(deleted).must_equal 2
384
+ chunks = adapter.load_output_chunks('plan1', 1)
385
+ _(chunks.length).must_equal 0
386
+ end
387
+ end
334
388
  end
335
389
 
336
390
  describe Dynflow::PersistenceAdapters::Sequel do
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'test_helper'
3
+ require 'mocha/minitest'
4
+
5
+ module Dynflow
6
+ module V2SubPlansTest
7
+ describe 'V2 sub-plans' do
8
+ include PlanAssertions
9
+ include Dynflow::Testing::Assertions
10
+ include Dynflow::Testing::Factories
11
+ include TestHelpers
12
+
13
+ let(:world) { WorldFactory.create_world }
14
+
15
+ class ChildAction < ::Dynflow::Action
16
+ def run
17
+ end
18
+ end
19
+
20
+ class ParentAction < ::Dynflow::Action
21
+ include Dynflow::Action::V2::WithSubPlans
22
+
23
+ def plan(count, concurrency_level = nil)
24
+ limit_concurrency_level!(concurrency_level) if concurrency_level
25
+ plan_self :count => count
26
+ end
27
+
28
+ def create_sub_plans
29
+ output[:batch_count] ||= 0
30
+ output[:batch_count] += 1
31
+ current_batch.map { |i| trigger(ChildAction) }
32
+ end
33
+
34
+ def batch(from, size)
35
+ (1..total_count).to_a.slice(from, size)
36
+ end
37
+
38
+ def batch_size
39
+ 5
40
+ end
41
+
42
+ def total_count
43
+ input[:count]
44
+ end
45
+ end
46
+
47
+ describe 'normal operation' do
48
+ it 'spawns all sub-plans in one go with high-enough batch size and polls until they are done' do
49
+ action = create_and_plan_action ParentAction, 3
50
+ action.world.expects(:trigger).times(3)
51
+ action = run_action action
52
+ _(action.output['total_count']).must_equal 3
53
+ _(action.output['planned_count']).must_equal 3
54
+ _(action.output['pending_count']).must_equal 3
55
+
56
+ ping = action.world.clock.pending_pings.first
57
+ _(ping.what.value.event).must_equal Dynflow::Action::V2::WithSubPlans::Ping
58
+ _(ping.when).must_be_within_delta(Time.now + action.polling_interval, 1)
59
+ persistence = mock()
60
+ persistence.stubs(:find_execution_plan_counts).returns(0)
61
+ action.world.stubs(:persistence).returns(persistence)
62
+
63
+ action.world.clock.progress
64
+ action.world.executor.progress
65
+ ping = action.world.clock.pending_pings.first
66
+ _(ping.what.value.event).must_equal Dynflow::Action::V2::WithSubPlans::Ping
67
+ _(ping.when).must_be_within_delta(Time.now + action.polling_interval * 2, 1)
68
+
69
+ persistence = mock()
70
+ persistence.stubs(:find_execution_plan_counts).returns(0).then.returns(3)
71
+ action.world.stubs(:persistence).returns(persistence)
72
+ action.world.clock.progress
73
+ action.world.executor.progress
74
+
75
+ _(action.state).must_equal :success
76
+ _(action.done?).must_equal true
77
+ end
78
+
79
+ it 'spawns sub-plans in multiple batches and polls until they are done' do
80
+ action = create_and_plan_action ParentAction, 7
81
+ action.world.expects(:trigger).times(5)
82
+ action = run_action action
83
+ _(action.output['total_count']).must_equal 7
84
+ _(action.output['planned_count']).must_equal 5
85
+ _(action.output['pending_count']).must_equal 5
86
+
87
+ _(action.world.clock.pending_pings).must_be :empty?
88
+ _, _, event, * = action.world.executor.events_to_process.first
89
+ _(event).must_equal Dynflow::Action::V2::WithSubPlans::Ping
90
+ persistence = mock()
91
+ # Simulate 3 finished
92
+ persistence.stubs(:find_execution_plan_counts).returns(0).then.returns(3)
93
+ action.world.stubs(:persistence).returns(persistence)
94
+
95
+ action.world.expects(:trigger).times(2)
96
+ action.world.executor.progress
97
+
98
+ ping = action.world.clock.pending_pings.first
99
+ _(ping.what.value.event).must_equal Dynflow::Action::V2::WithSubPlans::Ping
100
+ _(ping.when).must_be_within_delta(Time.now + action.polling_interval, 1)
101
+
102
+ persistence.stubs(:find_execution_plan_counts).returns(0).then.returns(7)
103
+ action.world.stubs(:persistence).returns(persistence)
104
+ action.world.clock.progress
105
+ action.world.executor.progress
106
+
107
+ _(action.state).must_equal :success
108
+ _(action.done?).must_equal true
109
+ end
110
+ end
111
+
112
+ describe 'with concurrency control' do
113
+ include Dynflow::Testing
114
+
115
+ it 'allows storage and retrieval' do
116
+ action = create_and_plan_action ParentAction, 0
117
+ action = run_action action
118
+ _(action.concurrency_limit).must_be_nil
119
+ _(action.concurrency_limit_capacity).must_be_nil
120
+
121
+ action = create_and_plan_action ParentAction, 0, 1
122
+ action = run_action action
123
+
124
+ _(action.input['dynflow']['concurrency_limit']).must_equal 1
125
+ _(action.concurrency_limit).must_equal 1
126
+ _(action.concurrency_limit_capacity).must_equal 1
127
+ end
128
+
129
+ it 'reduces the batch size to fit within the concurrency limit' do
130
+ action = create_and_plan_action ParentAction, 5, 2
131
+
132
+ # Plan first 2 sub-plans
133
+ action.world.expects(:trigger).times(2)
134
+
135
+ action = run_action action
136
+ _(action.output['total_count']).must_equal 5
137
+ _(action.output['planned_count']).must_equal 2
138
+ _(action.output['pending_count']).must_equal 2
139
+ _(action.concurrency_limit_capacity).must_equal 0
140
+ _(action.output['batch_count']).must_equal 1
141
+
142
+ ping = action.world.clock.pending_pings.first
143
+ _(ping.what.value.event).must_equal Dynflow::Action::V2::WithSubPlans::Ping
144
+ _(ping.when).must_be_within_delta(Time.now + action.polling_interval, 1)
145
+ persistence = mock()
146
+ # Simulate 1 sub-plan finished
147
+ persistence.stubs(:find_execution_plan_counts).returns(0).then.returns(1)
148
+ action.world.stubs(:persistence).returns(persistence)
149
+
150
+ # Only 1 sub-plans fits into the capacity
151
+ action.world.expects(:trigger).times(1)
152
+ action.world.clock.progress
153
+ action.world.executor.progress
154
+
155
+ _(action.output['planned_count']).must_equal 3
156
+
157
+ persistence = mock()
158
+ persistence.stubs(:find_execution_plan_counts).returns(0).then.returns(2)
159
+ action.world.stubs(:persistence).returns(persistence)
160
+ action.world.expects(:trigger).times(1)
161
+ action.world.clock.progress
162
+ action.world.executor.progress
163
+
164
+ _(action.output['planned_count']).must_equal 4
165
+
166
+ persistence = mock()
167
+ persistence.stubs(:find_execution_plan_counts).returns(0).then.returns(4)
168
+ action.world.stubs(:persistence).returns(persistence)
169
+ action.world.expects(:trigger).times(1)
170
+ action.world.clock.progress
171
+ action.world.executor.progress
172
+
173
+ _(action.output['planned_count']).must_equal 5
174
+ _(action.concurrency_limit_capacity).must_equal 1
175
+
176
+ persistence = mock()
177
+ persistence.stubs(:find_execution_plan_counts).returns(0).then.returns(5)
178
+ action.world.stubs(:persistence).returns(persistence)
179
+ action.world.expects(:trigger).never
180
+ action.world.clock.progress
181
+ action.world.executor.progress
182
+ _(action.state).must_equal :success
183
+ _(action.done?).must_equal true
184
+ _(action.concurrency_limit_capacity).must_equal 2
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.11
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Necas
8
8
  - Petr Chalupa
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-05-09 00:00:00.000000000 Z
12
+ date: 2023-08-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: multi_json
@@ -419,6 +419,7 @@ files:
419
419
  - examples/singletons.rb
420
420
  - examples/sub_plan_concurrency_control.rb
421
421
  - examples/sub_plans.rb
422
+ - examples/sub_plans_v2.rb
422
423
  - examples/termination.rb
423
424
  - extras/expand/Dockerfile
424
425
  - extras/expand/README.md
@@ -437,6 +438,8 @@ files:
437
438
  - lib/dynflow/action/singleton.rb
438
439
  - lib/dynflow/action/suspended.rb
439
440
  - lib/dynflow/action/timeouts.rb
441
+ - lib/dynflow/action/v2.rb
442
+ - lib/dynflow/action/v2/with_sub_plans.rb
440
443
  - lib/dynflow/action/with_bulk_sub_plans.rb
441
444
  - lib/dynflow/action/with_polling_sub_plans.rb
442
445
  - lib/dynflow/action/with_sub_plans.rb
@@ -632,6 +635,7 @@ files:
632
635
  - test/test_helper.rb
633
636
  - test/testing_test.rb
634
637
  - test/utils_test.rb
638
+ - test/v2_sub_plans_test.rb
635
639
  - test/web_console_test.rb
636
640
  - test/world_test.rb
637
641
  - web/assets/images/logo-square.png
@@ -663,7 +667,7 @@ homepage: https://github.com/Dynflow/dynflow
663
667
  licenses:
664
668
  - MIT
665
669
  metadata: {}
666
- post_install_message:
670
+ post_install_message:
667
671
  rdoc_options: []
668
672
  require_paths:
669
673
  - lib
@@ -678,8 +682,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
678
682
  - !ruby/object:Gem::Version
679
683
  version: '0'
680
684
  requirements: []
681
- rubygems_version: 3.4.12
682
- signing_key:
685
+ rubygems_version: 3.1.6
686
+ signing_key:
683
687
  specification_version: 4
684
688
  summary: DYNamic workFLOW engine
685
689
  test_files:
@@ -716,5 +720,6 @@ test_files:
716
720
  - test/test_helper.rb
717
721
  - test/testing_test.rb
718
722
  - test/utils_test.rb
723
+ - test/v2_sub_plans_test.rb
719
724
  - test/web_console_test.rb
720
725
  - test/world_test.rb