dynflow 0.8.9 → 0.8.10
Sign up to get free protection for your applications and to get access to all the features.
- 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
|