dynflow 0.8.9 → 0.8.10
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/.travis.yml +1 -0
- data/Gemfile +1 -1
- data/dynflow.gemspec +2 -2
- data/examples/example_helper.rb +4 -1
- data/examples/future_execution.rb +4 -5
- data/examples/orchestrate.rb +1 -0
- data/examples/sub_plan_concurrency_control.rb +73 -0
- data/lib/dynflow.rb +2 -0
- data/lib/dynflow/action/with_sub_plans.rb +42 -4
- data/lib/dynflow/config.rb +8 -0
- data/lib/dynflow/delayed_plan.rb +4 -3
- data/lib/dynflow/dispatcher/executor_dispatcher.rb +11 -7
- data/lib/dynflow/execution_plan.rb +5 -5
- data/lib/dynflow/execution_plan/steps/run_step.rb +1 -1
- data/lib/dynflow/semaphores.rb +8 -0
- data/lib/dynflow/semaphores/abstract.rb +46 -0
- data/lib/dynflow/semaphores/aggregating.rb +63 -0
- data/lib/dynflow/semaphores/dummy.rb +32 -0
- data/lib/dynflow/semaphores/stateful.rb +70 -0
- data/lib/dynflow/throttle_limiter.rb +135 -0
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/world.rb +16 -8
- data/test/action_test.rb +2 -2
- data/test/concurrency_control_test.rb +241 -0
- data/test/future_execution_test.rb +5 -5
- data/test/middleware_test.rb +2 -2
- data/test/prepare_travis_env.sh +1 -1
- data/test/semaphores_test.rb +98 -0
- data/test/test_helper.rb +1 -1
- metadata +18 -7
@@ -0,0 +1,32 @@
|
|
1
|
+
module Dynflow
|
2
|
+
module Semaphores
|
3
|
+
class Dummy < Abstract
|
4
|
+
|
5
|
+
def wait(thing)
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_waiting
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def has_waiting?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def release(*args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def save
|
21
|
+
end
|
22
|
+
|
23
|
+
def get(n)
|
24
|
+
n
|
25
|
+
end
|
26
|
+
|
27
|
+
def free
|
28
|
+
1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Dynflow
|
2
|
+
module Semaphores
|
3
|
+
class Stateful < Abstract
|
4
|
+
|
5
|
+
attr_reader :free, :tickets, :waiting, :meta
|
6
|
+
|
7
|
+
def initialize(tickets, free = tickets, meta = {})
|
8
|
+
@tickets = tickets
|
9
|
+
@free = free
|
10
|
+
@waiting = []
|
11
|
+
@meta = meta
|
12
|
+
end
|
13
|
+
|
14
|
+
def wait(thing)
|
15
|
+
if get > 0
|
16
|
+
true
|
17
|
+
else
|
18
|
+
@waiting << thing
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_waiting
|
24
|
+
@waiting.shift
|
25
|
+
end
|
26
|
+
|
27
|
+
def has_waiting?
|
28
|
+
!@waiting.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def release(n = 1)
|
32
|
+
@free += n
|
33
|
+
@free = @tickets unless @tickets.nil? || @free <= @tickets
|
34
|
+
save
|
35
|
+
end
|
36
|
+
|
37
|
+
def save
|
38
|
+
end
|
39
|
+
|
40
|
+
def get(n = 1)
|
41
|
+
if n > @free
|
42
|
+
drain
|
43
|
+
else
|
44
|
+
@free -= n
|
45
|
+
save
|
46
|
+
n
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def drain
|
51
|
+
@free.tap do
|
52
|
+
@free = 0
|
53
|
+
save
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_hash
|
58
|
+
{
|
59
|
+
:tickets => @tickets,
|
60
|
+
:free => @free,
|
61
|
+
:meta => @meta
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.new_from_hash(hash)
|
66
|
+
self.new(*hash.values_at(:tickets, :free, :meta))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class ThrottleLimiter
|
3
|
+
|
4
|
+
attr_reader :core
|
5
|
+
|
6
|
+
def initialize(world)
|
7
|
+
@world = world
|
8
|
+
spawn
|
9
|
+
end
|
10
|
+
|
11
|
+
def handle_plans!(*args)
|
12
|
+
core.ask!([:handle_plans, *args])
|
13
|
+
end
|
14
|
+
|
15
|
+
def cancel!(plan_id)
|
16
|
+
core.tell([:cancel, plan_id])
|
17
|
+
end
|
18
|
+
|
19
|
+
def terminate
|
20
|
+
core.ask(:terminate!)
|
21
|
+
end
|
22
|
+
|
23
|
+
def observe(parent_id = nil)
|
24
|
+
core.ask!([:observe, parent_id])
|
25
|
+
end
|
26
|
+
|
27
|
+
def core_class
|
28
|
+
Core
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def spawn
|
34
|
+
Concurrent.future.tap do |initialized|
|
35
|
+
@core = core_class.spawn(:name => 'throttle-limiter',
|
36
|
+
:args => [@world],
|
37
|
+
:initialized => initialized)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Core < Actor
|
42
|
+
def initialize(world)
|
43
|
+
@world = world
|
44
|
+
@semaphores = {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def handle_plans(parent_id, planned_ids, failed_ids, semaphores_hash)
|
48
|
+
@semaphores[parent_id] = create_semaphores(semaphores_hash)
|
49
|
+
set_up_clock_for(parent_id, true)
|
50
|
+
|
51
|
+
failed = failed_ids.map do |plan_id|
|
52
|
+
::Dynflow::World::Triggered[plan_id, Concurrent.future].tap do |triggered|
|
53
|
+
execute_triggered(triggered)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
planned_ids.map do |child_id|
|
58
|
+
::Dynflow::World::Triggered[child_id, Concurrent.future].tap do |triggered|
|
59
|
+
triggered.future.on_completion! { self << [:release, parent_id] }
|
60
|
+
execute_triggered(triggered) if @semaphores[parent_id].wait(triggered)
|
61
|
+
end
|
62
|
+
end + failed
|
63
|
+
end
|
64
|
+
|
65
|
+
def observe(parent_id = nil)
|
66
|
+
if parent_id.nil?
|
67
|
+
@semaphores.reduce([]) do |acc, cur|
|
68
|
+
acc << { cur.first => cur.last.waiting }
|
69
|
+
end
|
70
|
+
elsif @semaphores.key? parent_id
|
71
|
+
@semaphores[parent_id].waiting
|
72
|
+
else
|
73
|
+
[]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def release(plan_id, key = :level)
|
78
|
+
return unless @semaphores.key? plan_id
|
79
|
+
set_up_clock_for(plan_id) if key == :time
|
80
|
+
semaphore = @semaphores[plan_id]
|
81
|
+
semaphore.release(1, key) if semaphore.children.key?(key)
|
82
|
+
if semaphore.has_waiting? && semaphore.get == 1
|
83
|
+
execute_triggered(semaphore.get_waiting)
|
84
|
+
end
|
85
|
+
@semaphores.delete(plan_id) unless semaphore.has_waiting?
|
86
|
+
end
|
87
|
+
|
88
|
+
def cancel(parent_id, reason = nil)
|
89
|
+
if @semaphores.key?(parent_id)
|
90
|
+
reason ||= 'The task was cancelled.'
|
91
|
+
@semaphores[parent_id].waiting.each do |triggered|
|
92
|
+
cancel_plan_id(triggered.execution_plan_id, reason)
|
93
|
+
triggered.future.fail(reason)
|
94
|
+
end
|
95
|
+
@semaphores.delete(parent_id)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def cancel_plan_id(plan_id, reason)
|
102
|
+
plan = @world.persistence.load_execution_plan(plan_id)
|
103
|
+
steps = plan.steps.values.select { |step| step.is_a?(::Dynflow::ExecutionPlan::Steps::RunStep) }
|
104
|
+
steps.each do |step|
|
105
|
+
step.state = :error
|
106
|
+
step.error = ::Dynflow::ExecutionPlan::Steps::Error.new(reason)
|
107
|
+
step.save
|
108
|
+
end
|
109
|
+
plan.update_state(:stopped)
|
110
|
+
plan.save
|
111
|
+
end
|
112
|
+
|
113
|
+
def execute_triggered(triggered)
|
114
|
+
@world.execute(triggered.execution_plan_id, triggered.finished)
|
115
|
+
end
|
116
|
+
|
117
|
+
def set_up_clock_for(plan_id, initial = false)
|
118
|
+
if @semaphores[plan_id].children.key? :time
|
119
|
+
timeout_message = 'The task could not be started within the maintenance window.'
|
120
|
+
interval = @semaphores[plan_id].children[:time].meta[:interval]
|
121
|
+
timeout = @semaphores[plan_id].children[:time].meta[:time_span]
|
122
|
+
@world.clock.ping(self, interval, [:release, plan_id, :time])
|
123
|
+
@world.clock.ping(self, timeout, [:cancel, plan_id, timeout_message]) if initial
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def create_semaphores(hash)
|
128
|
+
semaphores = hash.keys.reduce(Utils.indifferent_hash({})) do |acc, key|
|
129
|
+
acc.merge(key => ::Dynflow::Semaphores::Stateful.new_from_hash(hash[key]))
|
130
|
+
end
|
131
|
+
::Dynflow::Semaphores::Aggregating.new(semaphores)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/lib/dynflow/version.rb
CHANGED
data/lib/dynflow/world.rb
CHANGED
@@ -7,7 +7,7 @@ module Dynflow
|
|
7
7
|
attr_reader :id, :client_dispatcher, :executor_dispatcher, :executor, :connector,
|
8
8
|
:transaction_adapter, :logger_adapter, :coordinator,
|
9
9
|
:persistence, :action_classes, :subscription_index,
|
10
|
-
:middleware, :auto_rescue, :clock, :meta, :delayed_executor, :auto_validity_check, :validity_check_timeout
|
10
|
+
:middleware, :auto_rescue, :clock, :meta, :delayed_executor, :auto_validity_check, :validity_check_timeout, :throttle_limiter
|
11
11
|
|
12
12
|
def initialize(config)
|
13
13
|
@id = SecureRandom.uuid
|
@@ -21,7 +21,7 @@ module Dynflow
|
|
21
21
|
@executor = config_for_world.executor
|
22
22
|
@action_classes = config_for_world.action_classes
|
23
23
|
@auto_rescue = config_for_world.auto_rescue
|
24
|
-
@exit_on_terminate = config_for_world.exit_on_terminate
|
24
|
+
@exit_on_terminate = Concurrent::AtomicBoolean.new(config_for_world.exit_on_terminate)
|
25
25
|
@connector = config_for_world.connector
|
26
26
|
@middleware = Middleware::World.new
|
27
27
|
@middleware.use Middleware::Common::Transaction if @transaction_adapter
|
@@ -29,10 +29,11 @@ module Dynflow
|
|
29
29
|
@meta = config_for_world.meta
|
30
30
|
@auto_validity_check = config_for_world.auto_validity_check
|
31
31
|
@validity_check_timeout = config_for_world.validity_check_timeout
|
32
|
+
@throttle_limiter = config_for_world.throttle_limiter
|
32
33
|
calculate_subscription_index
|
33
34
|
|
34
35
|
if executor
|
35
|
-
@executor_dispatcher = spawn_and_wait(Dispatcher::ExecutorDispatcher, "executor-dispatcher", self)
|
36
|
+
@executor_dispatcher = spawn_and_wait(Dispatcher::ExecutorDispatcher, "executor-dispatcher", self, config_for_world.executor_semaphore)
|
36
37
|
executor.initialized.wait
|
37
38
|
end
|
38
39
|
if auto_validity_check
|
@@ -48,7 +49,7 @@ module Dynflow
|
|
48
49
|
|
49
50
|
if config_for_world.auto_terminate
|
50
51
|
at_exit do
|
51
|
-
@exit_on_terminate
|
52
|
+
@exit_on_terminate.make_false # make sure we don't terminate twice
|
52
53
|
self.terminate.wait
|
53
54
|
end
|
54
55
|
end
|
@@ -150,10 +151,14 @@ module Dynflow
|
|
150
151
|
end
|
151
152
|
end
|
152
153
|
|
153
|
-
def delay(
|
154
|
+
def delay(*args)
|
155
|
+
delay_with_caller(nil, *args)
|
156
|
+
end
|
157
|
+
|
158
|
+
def delay_with_caller(caller_action, action_class, delay_options, *args)
|
154
159
|
raise 'No action_class given' if action_class.nil?
|
155
|
-
execution_plan = ExecutionPlan.new
|
156
|
-
execution_plan.delay(action_class, delay_options, *args)
|
160
|
+
execution_plan = ExecutionPlan.new(self)
|
161
|
+
execution_plan.delay(caller_action, action_class, delay_options, *args)
|
157
162
|
Scheduled[execution_plan.id]
|
158
163
|
end
|
159
164
|
|
@@ -208,6 +213,9 @@ module Dynflow
|
|
208
213
|
delayed_executor.terminate.wait
|
209
214
|
end
|
210
215
|
|
216
|
+
logger.info "start terminating throttle_limiter..."
|
217
|
+
throttle_limiter.terminate.wait
|
218
|
+
|
211
219
|
if executor
|
212
220
|
connector.stop_receiving_new_work(self)
|
213
221
|
|
@@ -239,7 +247,7 @@ module Dynflow
|
|
239
247
|
logger.fatal(e)
|
240
248
|
end
|
241
249
|
end.on_completion do
|
242
|
-
Kernel.exit if @exit_on_terminate
|
250
|
+
Thread.new { Kernel.exit } if @exit_on_terminate.true?
|
243
251
|
end
|
244
252
|
end
|
245
253
|
|
data/test/action_test.rb
CHANGED
@@ -0,0 +1,241 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
module Dynflow
|
4
|
+
module ConcurrencyControlTest
|
5
|
+
describe 'Concurrency Control' do
|
6
|
+
include PlanAssertions
|
7
|
+
include Dynflow::Testing::Assertions
|
8
|
+
include Dynflow::Testing::Factories
|
9
|
+
include TestHelpers
|
10
|
+
|
11
|
+
class FailureSimulator
|
12
|
+
def self.should_fail?
|
13
|
+
@should_fail || false
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.will_fail!
|
17
|
+
@should_fail = true
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.wont_fail!
|
21
|
+
@should_fail = false
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.will_sleep!
|
25
|
+
@should_sleep = true
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.wont_sleep!
|
29
|
+
@should_sleep = false
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.should_sleep?
|
33
|
+
@should_sleep
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
before do
|
38
|
+
FailureSimulator.wont_fail!
|
39
|
+
FailureSimulator.wont_sleep!
|
40
|
+
end
|
41
|
+
|
42
|
+
after do
|
43
|
+
klok.clear
|
44
|
+
end
|
45
|
+
|
46
|
+
class ChildAction < ::Dynflow::Action
|
47
|
+
def plan(should_sleep = false)
|
48
|
+
raise "Simulated failure" if FailureSimulator.should_fail?
|
49
|
+
plan_self :should_sleep => should_sleep
|
50
|
+
end
|
51
|
+
|
52
|
+
def run(event = nil)
|
53
|
+
unless output[:slept]
|
54
|
+
output[:slept] = true
|
55
|
+
puts "SLEEPING" if input[:should_sleep]
|
56
|
+
suspend { |suspended| world.clock.ping(suspended, 100, [:run]) } if input[:should_sleep]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class ParentAction < ::Dynflow::Action
|
62
|
+
include Dynflow::Action::WithSubPlans
|
63
|
+
|
64
|
+
def plan(count, concurrency_level = nil, time_span = nil, should_sleep = nil)
|
65
|
+
limit_concurrency_level(concurrency_level) unless concurrency_level.nil?
|
66
|
+
distribute_over_time(time_span) unless time_span.nil?
|
67
|
+
plan_self :count => count, :should_sleep => should_sleep
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_sub_plans
|
71
|
+
input[:count].times.map { |i| trigger(::Dynflow::ConcurrencyControlTest::ChildAction, input[:should_sleep]) }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
let(:klok) { Dynflow::Testing::ManagedClock.new }
|
76
|
+
let(:world) do
|
77
|
+
WorldFactory.create_world do |config|
|
78
|
+
config.throttle_limiter = proc { |world| LoggingThrottleLimiter.new world }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def check_step(plan, total, finished)
|
83
|
+
world.throttle_limiter.observe(plan.id).length.must_equal (total - finished)
|
84
|
+
plan.sub_plans.select { |sub| planned? sub }.count.must_equal (total - finished)
|
85
|
+
plan.sub_plans.select { |sub| successful? sub }.count.must_equal finished
|
86
|
+
end
|
87
|
+
|
88
|
+
def planned?(plan)
|
89
|
+
plan.state == :planned && plan.result == :pending
|
90
|
+
end
|
91
|
+
|
92
|
+
def successful?(plan)
|
93
|
+
plan.state == :stopped && plan.result == :success
|
94
|
+
end
|
95
|
+
|
96
|
+
class LoggingThrottleLimiter < Dynflow::ThrottleLimiter
|
97
|
+
|
98
|
+
class LoggingCore < Dynflow::ThrottleLimiter::Core
|
99
|
+
|
100
|
+
attr_reader :running
|
101
|
+
|
102
|
+
def initialize(*args)
|
103
|
+
@running = [0]
|
104
|
+
super *args
|
105
|
+
end
|
106
|
+
|
107
|
+
def release(*args)
|
108
|
+
# Discard semaphores without tickets, find the one with least tickets from the rest
|
109
|
+
if @semaphores.key? args.first
|
110
|
+
tickets = @semaphores[args.first].children.values.map { |sem| sem.tickets }.compact.min
|
111
|
+
# Add running count to the log
|
112
|
+
@running << (tickets - @semaphores[args.first].free) unless tickets.nil?
|
113
|
+
end
|
114
|
+
super(*args)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def core_class
|
119
|
+
LoggingThrottleLimiter::LoggingCore
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'can be disabled' do
|
124
|
+
total = 10
|
125
|
+
plan = world.plan(ParentAction, 10)
|
126
|
+
future = world.execute plan.id
|
127
|
+
wait_for { future.completed? }
|
128
|
+
plan.sub_plans.all? { |sub| successful? sub }
|
129
|
+
world.throttle_limiter.core.ask!(:running).must_equal [0]
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'limits by concurrency level' do
|
133
|
+
total = 10
|
134
|
+
level = 4
|
135
|
+
plan = world.plan(ParentAction, total, level)
|
136
|
+
future = world.execute plan.id
|
137
|
+
wait_for { future.completed? }
|
138
|
+
world.throttle_limiter.core.ask!(:running).max.must_be :<=, level
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'allows to cancel' do
|
142
|
+
total = 5
|
143
|
+
world.stub :clock, klok do
|
144
|
+
plan = world.plan(ParentAction, total, 0)
|
145
|
+
triggered = world.execute(plan.id)
|
146
|
+
wait_for { plan.sub_plans.count == total }
|
147
|
+
world.event(plan.id, plan.steps.values.last.id, ::Dynflow::Action::Cancellable::Cancel)
|
148
|
+
wait_for { triggered.completed? }
|
149
|
+
plan.entry_action.output[:failed_count].must_equal total
|
150
|
+
world.throttle_limiter.core.ask!(:running).max.must_be :<=, 0
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'calculates time interval correctly' do
|
155
|
+
world.stub :clock, klok do
|
156
|
+
total = 10
|
157
|
+
get_interval = ->(plan) { plan.entry_action.input[:concurrency_control][:time][:meta][:interval] }
|
158
|
+
|
159
|
+
plan = world.plan(ParentAction, total, 1, 10)
|
160
|
+
future = world.execute(plan.id)
|
161
|
+
wait_for { plan.sub_plans.count == total }
|
162
|
+
wait_for { klok.progress; plan.sub_plans.all? { |sub| successful? sub } }
|
163
|
+
# 10 tasks over 10 seconds, one task at a time, 1 task every second
|
164
|
+
get_interval.call(plan).must_equal 1.0
|
165
|
+
|
166
|
+
plan = world.plan(ParentAction, total, 4, 10)
|
167
|
+
world.execute(plan.id)
|
168
|
+
wait_for { plan.sub_plans.count == total }
|
169
|
+
wait_for { klok.progress; plan.sub_plans.all? { |sub| successful? sub } }
|
170
|
+
# 10 tasks over 10 seconds, four tasks at a time, 1 task every 0.25 second
|
171
|
+
get_interval.call(plan).must_equal 0.25
|
172
|
+
|
173
|
+
plan = world.plan(ParentAction, total, nil, 10)
|
174
|
+
world.execute(plan.id)
|
175
|
+
wait_for { plan.sub_plans.count == total }
|
176
|
+
wait_for { klok.progress; plan.sub_plans.all? { |sub| successful? sub } }
|
177
|
+
# 1o tasks over 10 seconds, one task at a time (default), 1 task every second
|
178
|
+
get_interval.call(plan).must_equal 1.0
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'uses the throttle limiter to handle the plans' do
|
183
|
+
world.stub :clock, klok do
|
184
|
+
time_span = 10.0
|
185
|
+
total = 10
|
186
|
+
level = 2
|
187
|
+
plan = world.plan(ParentAction, total, level, time_span)
|
188
|
+
start_time = klok.current_time
|
189
|
+
world.execute(plan.id)
|
190
|
+
wait_for { plan.sub_plans.count == total }
|
191
|
+
wait_for { plan.sub_plans.select { |sub| successful? sub }.count == level }
|
192
|
+
finished = 2
|
193
|
+
check_step(plan, total, finished)
|
194
|
+
world.throttle_limiter.observe(plan.id).dup.each do |triggered|
|
195
|
+
triggered.future.tap do |future|
|
196
|
+
klok.progress
|
197
|
+
wait_for { future.completed? }
|
198
|
+
end
|
199
|
+
finished += 1
|
200
|
+
check_step(plan, total, finished)
|
201
|
+
end
|
202
|
+
end_time = klok.current_time
|
203
|
+
(end_time - start_time).must_equal 4
|
204
|
+
world.throttle_limiter.observe(plan.id).must_equal []
|
205
|
+
world.throttle_limiter.core.ask!(:running).max.must_be :<=, level
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'fails tasks which failed to plan immediately' do
|
210
|
+
FailureSimulator.will_fail!
|
211
|
+
total = 5
|
212
|
+
level = 1
|
213
|
+
time_span = 10
|
214
|
+
plan = world.plan(ParentAction, total, level, time_span)
|
215
|
+
future = world.execute(plan.id)
|
216
|
+
wait_for { future.completed? }
|
217
|
+
plan.sub_plans.all? { |sub| sub.result == :error }.must_equal true
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'cancels tasks which could not be started within the time window' do
|
221
|
+
world.stub :clock, klok do
|
222
|
+
time_span = 10.0
|
223
|
+
level = 1
|
224
|
+
total = 10
|
225
|
+
plan = world.plan(ParentAction, total, level, time_span, true)
|
226
|
+
future = world.execute(plan.id)
|
227
|
+
wait_for { plan.sub_plans.count == total && plan.sub_plans.all? { |sub| sub.result == :pending } }
|
228
|
+
planned, running = plan.sub_plans.partition { |sub| planned? sub }
|
229
|
+
planned.count.must_equal total - level
|
230
|
+
running.count.must_equal level
|
231
|
+
world.throttle_limiter.observe(plan.id).length.must_equal (total - 1)
|
232
|
+
4.times { klok.progress }
|
233
|
+
wait_for { future.completed? }
|
234
|
+
finished, stopped = plan.sub_plans.partition { |sub| successful? sub }
|
235
|
+
finished.count.must_equal level
|
236
|
+
stopped.count.must_equal (total - level)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|