dynflow 1.1.6 → 1.2.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/dynflow.gemspec +2 -2
  4. data/examples/clock_benchmark.rb +35 -0
  5. data/examples/memory_limit_watcher.rb +1 -1
  6. data/lib/dynflow/action.rb +2 -2
  7. data/lib/dynflow/action/suspended.rb +1 -1
  8. data/lib/dynflow/action/with_sub_plans.rb +1 -1
  9. data/lib/dynflow/actor.rb +2 -2
  10. data/lib/dynflow/actors/execution_plan_cleaner.rb +1 -1
  11. data/lib/dynflow/clock.rb +11 -8
  12. data/lib/dynflow/delayed_executors/abstract.rb +1 -1
  13. data/lib/dynflow/delayed_plan.rb +1 -1
  14. data/lib/dynflow/director.rb +4 -4
  15. data/lib/dynflow/director/execution_plan_manager.rb +2 -2
  16. data/lib/dynflow/director/running_steps_manager.rb +4 -4
  17. data/lib/dynflow/dispatcher/client_dispatcher.rb +13 -12
  18. data/lib/dynflow/dispatcher/executor_dispatcher.rb +5 -5
  19. data/lib/dynflow/execution_plan.rb +1 -1
  20. data/lib/dynflow/execution_plan/steps/plan_step.rb +4 -2
  21. data/lib/dynflow/executors/abstract.rb +6 -6
  22. data/lib/dynflow/executors/parallel.rb +6 -6
  23. data/lib/dynflow/executors/parallel/core.rb +1 -1
  24. data/lib/dynflow/rails/daemon.rb +1 -1
  25. data/lib/dynflow/testing/dummy_executor.rb +2 -2
  26. data/lib/dynflow/testing/dummy_world.rb +1 -1
  27. data/lib/dynflow/testing/in_thread_executor.rb +5 -5
  28. data/lib/dynflow/testing/in_thread_world.rb +6 -6
  29. data/lib/dynflow/throttle_limiter.rb +5 -5
  30. data/lib/dynflow/utils.rb +3 -140
  31. data/lib/dynflow/utils/indifferent_hash.rb +143 -0
  32. data/lib/dynflow/utils/priority_queue.rb +64 -0
  33. data/lib/dynflow/version.rb +1 -1
  34. data/lib/dynflow/world.rb +22 -22
  35. data/lib/dynflow/world/invalidation.rb +1 -1
  36. data/test/batch_sub_tasks_test.rb +4 -4
  37. data/test/concurrency_control_test.rb +6 -6
  38. data/test/daemon_test.rb +2 -2
  39. data/test/dispatcher_test.rb +6 -6
  40. data/test/execution_plan_test.rb +11 -0
  41. data/test/executor_test.rb +1 -1
  42. data/test/support/dummy_example.rb +1 -1
  43. data/test/test_helper.rb +17 -17
  44. data/test/utils_test.rb +56 -0
  45. data/test/world_test.rb +2 -2
  46. metadata +14 -9
@@ -0,0 +1,64 @@
1
+ module Dynflow
2
+ module Utils
3
+ # Heavily inspired by rubyworks/pqueue
4
+ class PriorityQueue
5
+ def initialize(&block) # :yields: a, b
6
+ @backing_store = []
7
+ @comparator = block || :<=>.to_proc
8
+ end
9
+
10
+ def size
11
+ @backing_store.size
12
+ end
13
+
14
+ def top
15
+ @backing_store.last
16
+ end
17
+
18
+ def push(element)
19
+ @backing_store << element
20
+ reposition_element(@backing_store.size - 1)
21
+ end
22
+
23
+ def pop
24
+ @backing_store.pop
25
+ end
26
+
27
+ def to_a
28
+ @backing_store
29
+ end
30
+
31
+ private
32
+
33
+ # The element at index k will be repositioned to its proper place.
34
+ def reposition_element(index)
35
+ return if size <= 1
36
+
37
+ element = @backing_store.delete_at(index)
38
+ index = binary_index(@backing_store, element)
39
+
40
+ @backing_store.insert(index, element)
41
+ end
42
+
43
+ # Find index where a new element should be inserted using binary search
44
+ def binary_index(array, target)
45
+ upper = array.size - 1
46
+ lower = 0
47
+
48
+ while upper >= lower
49
+ center = lower + (upper - lower) / 2
50
+
51
+ case @comparator.call(target, array[center])
52
+ when 0
53
+ return center
54
+ when 1
55
+ lower = center + 1
56
+ when -1
57
+ upper = center - 1
58
+ end
59
+ end
60
+ lower
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = '1.1.6'.freeze
2
+ VERSION = '1.2.0.pre1'.freeze
3
3
  end
@@ -21,8 +21,8 @@ module Dynflow
21
21
  Dynflow::Telemetry.register_metrics!
22
22
 
23
23
  @id = SecureRandom.uuid
24
- @clock = spawn_and_wait(Clock, 'clock')
25
24
  @logger_adapter = @config.logger_adapter
25
+ @clock = spawn_and_wait(Clock, 'clock', logger)
26
26
  @config.validate
27
27
  @transaction_adapter = @config.transaction_adapter
28
28
  @persistence = Persistence.new(self, @config.persistence_adapter,
@@ -41,7 +41,7 @@ module Dynflow
41
41
  @auto_validity_check = @config.auto_validity_check
42
42
  @validity_check_timeout = @config.validity_check_timeout
43
43
  @throttle_limiter = @config.throttle_limiter
44
- @terminated = Concurrent.event
44
+ @terminated = Concurrent::Promises.resolvable_event
45
45
  @termination_timeout = @config.termination_timeout
46
46
  calculate_subscription_index
47
47
 
@@ -132,7 +132,7 @@ module Dynflow
132
132
  PlaningFailed = type { fields! execution_plan_id: String, error: Exception }
133
133
  # Returned by #trigger when planning is successful, #future will resolve after
134
134
  # ExecutionPlan is executed.
135
- Triggered = type { fields! execution_plan_id: String, future: Concurrent::Edge::Future }
135
+ Triggered = type { fields! execution_plan_id: String, future: Concurrent::Promises::ResolvableFuture }
136
136
 
137
137
  Scheduled = type { fields! execution_plan_id: String }
138
138
 
@@ -176,7 +176,7 @@ module Dynflow
176
176
  planned = execution_plan.state == :planned
177
177
 
178
178
  if planned
179
- done = execute(execution_plan.id, Concurrent.future)
179
+ done = execute(execution_plan.id, Concurrent::Promises.resolvable_future)
180
180
  Triggered[execution_plan.id, done]
181
181
  else
182
182
  PlaningFailed[execution_plan.id, execution_plan.errors.first.exception]
@@ -208,41 +208,41 @@ module Dynflow
208
208
  end
209
209
  end
210
210
 
211
- # @return [Concurrent::Edge::Future] containing execution_plan when finished
211
+ # @return [Concurrent::Promises::ResolvableFuture] containing execution_plan when finished
212
212
  # raises when ExecutionPlan is not accepted for execution
213
- def execute(execution_plan_id, done = Concurrent.future)
213
+ def execute(execution_plan_id, done = Concurrent::Promises.resolvable_future)
214
214
  publish_request(Dispatcher::Execution[execution_plan_id], done, true)
215
215
  end
216
216
 
217
- def event(execution_plan_id, step_id, event, done = Concurrent.future)
217
+ def event(execution_plan_id, step_id, event, done = Concurrent::Promises.resolvable_future)
218
218
  publish_request(Dispatcher::Event[execution_plan_id, step_id, event], done, false)
219
219
  end
220
220
 
221
- def ping(world_id, timeout, done = Concurrent.future)
221
+ def ping(world_id, timeout, done = Concurrent::Promises.resolvable_future)
222
222
  publish_request(Dispatcher::Ping[world_id, true], done, false, timeout)
223
223
  end
224
224
 
225
- def ping_without_cache(world_id, timeout, done = Concurrent.future)
225
+ def ping_without_cache(world_id, timeout, done = Concurrent::Promises.resolvable_future)
226
226
  publish_request(Dispatcher::Ping[world_id, false], done, false, timeout)
227
227
  end
228
228
 
229
- def get_execution_status(world_id, execution_plan_id, timeout, done = Concurrent.future)
229
+ def get_execution_status(world_id, execution_plan_id, timeout, done = Concurrent::Promises.resolvable_future)
230
230
  publish_request(Dispatcher::Status[world_id, execution_plan_id], done, false, timeout)
231
231
  end
232
232
 
233
233
  def publish_request(request, done, wait_for_accepted, timeout = nil)
234
- accepted = Concurrent.future
234
+ accepted = Concurrent::Promises.resolvable_future
235
235
  accepted.rescue do |reason|
236
- done.fail reason if reason
236
+ done.reject reason if reason
237
237
  end
238
238
  client_dispatcher.ask([:publish_request, done, request, timeout], accepted)
239
239
  accepted.wait if wait_for_accepted
240
240
  done
241
241
  rescue => e
242
- accepted.fail e
242
+ accepted.reject e
243
243
  end
244
244
 
245
- def terminate(future = Concurrent.future)
245
+ def terminate(future = Concurrent::Promises.resolvable_future)
246
246
  start_termination.tangle(future)
247
247
  future
248
248
  end
@@ -285,7 +285,7 @@ module Dynflow
285
285
  def start_termination
286
286
  @termination_barrier.synchronize do
287
287
  return @terminating if @terminating
288
- termination_future ||= Concurrent.future do
288
+ termination_future ||= Concurrent::Promises.future do
289
289
  begin
290
290
  run_before_termination_hooks
291
291
 
@@ -304,13 +304,13 @@ module Dynflow
304
304
  executor.terminate.wait(termination_timeout)
305
305
 
306
306
  logger.info "start terminating executor dispatcher..."
307
- executor_dispatcher_terminated = Concurrent.future
307
+ executor_dispatcher_terminated = Concurrent::Promises.resolvable_future
308
308
  executor_dispatcher.ask([:start_termination, executor_dispatcher_terminated])
309
309
  executor_dispatcher_terminated.wait(termination_timeout)
310
310
  end
311
311
 
312
312
  logger.info "start terminating client dispatcher..."
313
- client_dispatcher_terminated = Concurrent.future
313
+ client_dispatcher_terminated = Concurrent::Promises.resolvable_future
314
314
  client_dispatcher.ask([:start_termination, client_dispatcher_terminated])
315
315
  client_dispatcher_terminated.wait(termination_timeout)
316
316
 
@@ -323,15 +323,15 @@ module Dynflow
323
323
  end
324
324
 
325
325
  coordinator.delete_world(registered_world)
326
- @terminated.complete
326
+ @terminated.resolve
327
327
  true
328
328
  rescue => e
329
329
  logger.fatal(e)
330
330
  end
331
331
  end
332
- @terminating = Concurrent.future do
332
+ @terminating = Concurrent::Promises.future do
333
333
  termination_future.wait(termination_timeout)
334
- end.on_completion do
334
+ end.on_resolution do
335
335
  @terminated.complete
336
336
  Thread.new { Kernel.exit } if @exit_on_terminate.true?
337
337
  end
@@ -350,7 +350,7 @@ module Dynflow
350
350
 
351
351
  def run_before_termination_hooks
352
352
  until @before_termination_hooks.empty?
353
- hook_run = Concurrent.future do
353
+ hook_run = Concurrent::Promises.future do
354
354
  begin
355
355
  @before_termination_hooks.pop.call
356
356
  rescue => e
@@ -362,7 +362,7 @@ module Dynflow
362
362
  end
363
363
 
364
364
  def spawn_and_wait(klass, name, *args)
365
- initialized = Concurrent.future
365
+ initialized = Concurrent::Promises.resolvable_future
366
366
  actor = klass.spawn(name: name, args: args, initialized: initialized)
367
367
  initialized.wait
368
368
  return actor
@@ -118,7 +118,7 @@ module Dynflow
118
118
 
119
119
  results = {}
120
120
  world_checks.each do |world, check|
121
- if check.success?
121
+ if check.fulfilled?
122
122
  result = :valid
123
123
  else
124
124
  if auto_invalidate
@@ -68,7 +68,7 @@ module Dynflow
68
68
  FailureSimulator.wont_fail!
69
69
  plan = world.plan(ParentAction, 20)
70
70
  future = world.execute plan.id
71
- wait_for { future.completed? }
71
+ wait_for { future.resolved? }
72
72
  plan = world.persistence.load_execution_plan(plan.id)
73
73
  action = plan.entry_action
74
74
 
@@ -79,7 +79,7 @@ module Dynflow
79
79
  FailureSimulator.should_fail!
80
80
  plan = world.plan(ParentAction, 20)
81
81
  future = world.execute plan.id
82
- wait_for { future.completed? }
82
+ wait_for { future.resolved? }
83
83
  plan = world.persistence.load_execution_plan(plan.id)
84
84
  action = plan.entry_action
85
85
  action.output[:batch_count].must_equal 1
@@ -87,7 +87,7 @@ module Dynflow
87
87
 
88
88
  FailureSimulator.wont_fail!
89
89
  future = world.execute plan.id
90
- wait_for { future.completed? }
90
+ wait_for { future.resolved? }
91
91
  action = future.value.entry_action
92
92
  future.value.state.must_equal :stopped
93
93
  action.output[:batch_count].must_equal (action.total_count / action.batch_size) + 1
@@ -98,7 +98,7 @@ module Dynflow
98
98
  it 'is controlled only by total_count and output[:planned_count]' do
99
99
  plan = world.plan(ParentAction, 10)
100
100
  future = world.execute plan.id
101
- wait_for { future.completed? }
101
+ wait_for { future.resolved? }
102
102
  plan = world.persistence.load_execution_plan(plan.id)
103
103
  action = plan.entry_action
104
104
  action.send(:can_spawn_next_batch?).must_equal false
@@ -124,7 +124,7 @@ module Dynflow
124
124
  total = 10
125
125
  plan = world.plan(ParentAction, 10)
126
126
  future = world.execute plan.id
127
- wait_for { future.completed? }
127
+ wait_for { future.resolved? }
128
128
  plan.sub_plans.all? { |sub| successful? sub }
129
129
  world.throttle_limiter.core.ask!(:running).must_equal [0]
130
130
  end
@@ -134,7 +134,7 @@ module Dynflow
134
134
  level = 4
135
135
  plan = world.plan(ParentAction, total, level)
136
136
  future = world.execute plan.id
137
- wait_for { future.completed? }
137
+ wait_for { future.resolved? }
138
138
  world.throttle_limiter.core.ask!(:running).max.must_be :<=, level
139
139
  end
140
140
 
@@ -145,7 +145,7 @@ module Dynflow
145
145
  triggered = world.execute(plan.id)
146
146
  wait_for { plan.sub_plans_count == total }
147
147
  world.event(plan.id, plan.steps.values.last.id, ::Dynflow::Action::Cancellable::Cancel)
148
- wait_for { triggered.completed? }
148
+ wait_for { triggered.resolved? }
149
149
  plan = world.persistence.load_execution_plan(plan.id)
150
150
  plan.entry_action.output[:failed_count].must_equal total
151
151
  world.throttle_limiter.core.ask!(:running).max.must_be :<=, 0
@@ -198,7 +198,7 @@ module Dynflow
198
198
  world.throttle_limiter.observe(plan.id).dup.each do |triggered|
199
199
  triggered.future.tap do |future|
200
200
  klok.progress
201
- wait_for { future.completed? }
201
+ wait_for { future.resolved? }
202
202
  end
203
203
  finished += 1
204
204
  check_step(plan, total, finished)
@@ -217,7 +217,7 @@ module Dynflow
217
217
  time_span = 10
218
218
  plan = world.plan(ParentAction, total, level, time_span)
219
219
  future = world.execute(plan.id)
220
- wait_for { future.completed? }
220
+ wait_for { future.resolved? }
221
221
  plan.sub_plans.all? { |sub| sub.result == :error }.must_equal true
222
222
  end
223
223
 
@@ -234,7 +234,7 @@ module Dynflow
234
234
  running.count.must_equal level
235
235
  world.throttle_limiter.observe(plan.id).length.must_equal (total - 1)
236
236
  4.times { klok.progress }
237
- wait_for { future.completed? }
237
+ wait_for { future.resolved? }
238
238
  finished, stopped = plan.sub_plans.partition { |sub| successful? sub }
239
239
  finished.count.must_equal level
240
240
  stopped.count.must_equal (total - level)
@@ -18,7 +18,7 @@ class DaemonTest < ActiveSupport::TestCase
18
18
  @dummy_world.stubs(:id => '123')
19
19
  @dummy_world.stubs(:auto_execute)
20
20
  @dummy_world.stubs(:perform_validity_checks => 0)
21
- @event = Concurrent.event
21
+ @event = Concurrent::Promises.resolvable_event
22
22
  @dummy_world.stubs(:terminated).returns(@event)
23
23
  @world_class.stubs(:new).returns(@dummy_world)
24
24
  @dynflow = ::Dynflow::Rails.new(
@@ -38,7 +38,7 @@ class DaemonTest < ActiveSupport::TestCase
38
38
  end
39
39
 
40
40
  teardown do
41
- @event.complete
41
+ @event.resolve
42
42
  @event.wait
43
43
  end
44
44
 
@@ -40,7 +40,7 @@ module Dynflow
40
40
  step = plan.steps.values.first
41
41
  future = client_world.event(plan.id, step.id, 'finish')
42
42
  future.wait
43
- assert future.failed?
43
+ assert future.rejected?
44
44
  end
45
45
 
46
46
  it 'succeeds when executor acts as client' do
@@ -73,27 +73,27 @@ module Dynflow
73
73
  it 'succeeds when the world is available' do
74
74
  ping_response = client_world.ping(executor_world.id, 0.5)
75
75
  ping_response.wait
76
- assert ping_response.success?
76
+ assert ping_response.fulfilled?
77
77
  end
78
78
 
79
79
  it 'succeeds when the world is available without cache' do
80
80
  ping_response = client_world.ping_without_cache(executor_world.id, 0.5)
81
81
  ping_response.wait
82
- assert ping_response.success?
82
+ assert ping_response.fulfilled?
83
83
  end
84
84
 
85
85
  it 'time-outs when the world is not responding' do
86
86
  executor_world.terminate.wait
87
87
  ping_response = client_world.ping(executor_world.id, 0.5)
88
88
  ping_response.wait
89
- assert ping_response.failed?
89
+ assert ping_response.rejected?
90
90
  end
91
91
 
92
92
  it 'time-outs when the world is not responding without cache' do
93
93
  executor_world.terminate.wait
94
94
  ping_response = client_world.ping_without_cache(executor_world.id, 0.5)
95
95
  ping_response.wait
96
- assert ping_response.failed?
96
+ assert ping_response.rejected?
97
97
  end
98
98
 
99
99
  it 'caches the pings and pongs' do
@@ -121,7 +121,7 @@ module Dynflow
121
121
  executor_world_2.terminate.wait
122
122
  result = client_world.trigger(Support::DummyExample::Dummy)
123
123
  result.finished.wait
124
- assert result.finished.failed?
124
+ assert result.finished.rejected?
125
125
  assert_match(/No executor available/, result.finished.reason.message)
126
126
  end
127
127
  end
@@ -141,6 +141,17 @@ module Dynflow
141
141
 
142
142
  end
143
143
 
144
+ describe 'sub plans' do
145
+ let(:execution_plan) do
146
+ world.plan(Support::CodeWorkflowExample::IncomingIssues, issues_data)
147
+ end
148
+
149
+ it 'does not have itself as a sub plan' do
150
+ assert execution_plan.actions.count >= 2
151
+ execution_plan.sub_plans.must_be :empty?
152
+ end
153
+ end
154
+
144
155
  describe 'plan steps' do
145
156
  let :execution_plan do
146
157
  world.plan(Support::CodeWorkflowExample::IncomingIssues, issues_data)
@@ -671,7 +671,7 @@ module Dynflow
671
671
  result = world.trigger(Support::DummyExample::Slow, 0.02)
672
672
  result.must_be :planned?
673
673
  result.finished.wait
674
- assert result.finished.failed?
674
+ assert result.finished.rejected?
675
675
  result.finished.reason.must_be_kind_of Concurrent::Actor::ActorTerminated
676
676
  end
677
677
 
@@ -143,7 +143,7 @@ module Support
143
143
  world.clock.ping suspended_action, input[:timeout], "timeout"
144
144
  end
145
145
 
146
- sub_plan.finished.on_success! { suspended_action << 'finish' }
146
+ sub_plan.finished.on_fulfillment! { suspended_action << 'finish' }
147
147
  end
148
148
  end),
149
149
  (on 'finish' do
@@ -25,8 +25,8 @@ Concurrent.disable_at_exit_handlers!
25
25
  class TestPause
26
26
 
27
27
  def self.setup
28
- @pause = Concurrent.future
29
- @ready = Concurrent.future
28
+ @pause = Concurrent::Promises.resolvable_future
29
+ @ready = Concurrent::Promises.resolvable_future
30
30
  end
31
31
 
32
32
  def self.teardown
@@ -38,10 +38,10 @@ class TestPause
38
38
  def self.pause
39
39
  if !@pause
40
40
  raise 'the TestPause class was not setup'
41
- elsif @ready.completed?
41
+ elsif @ready.resolved?
42
42
  raise 'you can pause only once'
43
43
  else
44
- @ready.success(true)
44
+ @ready.fulfill(true)
45
45
  @pause.wait
46
46
  end
47
47
  end
@@ -51,7 +51,7 @@ class TestPause
51
51
  if @pause
52
52
  @ready.wait # wait till we are paused
53
53
  yield
54
- @pause.success(true) # resume the run
54
+ @pause.fulfill(true) # resume the run
55
55
  else
56
56
  raise 'the TestPause class was not setup'
57
57
  end
@@ -249,17 +249,17 @@ events_test = -> do
249
249
  event_creations = {}
250
250
  non_ready_events = {}
251
251
 
252
- Concurrent::Edge::Event.singleton_class.send :define_method, :new do |*args, &block|
252
+ Concurrent::Promises::Event.singleton_class.send :define_method, :new do |*args, &block|
253
253
  super(*args, &block).tap do |event|
254
254
  event_creations[event.object_id] = caller(4)
255
255
  end
256
256
  end
257
257
 
258
- [Concurrent::Edge::Event, Concurrent::Edge::Future].each do |future_class|
259
- original_complete_method = future_class.instance_method :complete_with
260
- future_class.send :define_method, :complete_with do |*args|
258
+ [Concurrent::Promises::Event, Concurrent::Promises::ResolvableFuture].each do |future_class|
259
+ original_resolved_method = future_class.instance_method :resolve_with
260
+ future_class.send :define_method, :resolve_with do |*args|
261
261
  begin
262
- original_complete_method.bind(self).call(*args)
262
+ original_resolved_method.bind(self).call(*args)
263
263
  ensure
264
264
  event_creations.delete(self.object_id)
265
265
  end
@@ -269,9 +269,9 @@ events_test = -> do
269
269
  MiniTest.after_run do
270
270
  Concurrent::Actor.root.ask!(:terminate!)
271
271
 
272
- non_ready_events = ObjectSpace.each_object(Concurrent::Edge::Event).map do |event|
272
+ non_ready_events = ObjectSpace.each_object(Concurrent::Promises::Event).map do |event|
273
273
  event.wait(1)
274
- unless event.completed?
274
+ unless event.resolved?
275
275
  event.object_id
276
276
  end
277
277
  end.compact
@@ -294,9 +294,9 @@ events_test = -> do
294
294
 
295
295
  # time out all futures by default
296
296
  default_timeout = 8
297
- wait_method = Concurrent::Edge::Event.instance_method(:wait)
297
+ wait_method = Concurrent::Promises::AbstractEventFuture.instance_method(:wait)
298
298
 
299
- Concurrent::Edge::Event.class_eval do
299
+ Concurrent::Promises::AbstractEventFuture.class_eval do
300
300
  define_method :wait do |timeout = nil|
301
301
  wait_method.bind(self).call(timeout || default_timeout)
302
302
  end
@@ -308,7 +308,7 @@ events_test.call
308
308
 
309
309
  class ConcurrentRunTester
310
310
  def initialize
311
- @enter_future, @exit_future = Concurrent.future, Concurrent.future
311
+ @enter_future, @exit_future = Concurrent::Promises.resolvable_future, Concurrent::Promises.resolvable_future
312
312
  end
313
313
 
314
314
  def while_executing(&block)
@@ -319,12 +319,12 @@ class ConcurrentRunTester
319
319
  end
320
320
 
321
321
  def pause
322
- @enter_future.success(true)
322
+ @enter_future.fulfill(true)
323
323
  @exit_future.wait(1)
324
324
  end
325
325
 
326
326
  def finish
327
- @exit_future.success(true)
327
+ @exit_future.fulfill(true)
328
328
  @thread.join
329
329
  end
330
330
  end