async_futures 0.1.2

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.
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error'
4
+ require_relative 'executor'
5
+
6
+ require 'set' # rubocop:disable Lint/RedundantRequireStatement
7
+
8
+ module AsyncFutures
9
+ # `Executor` implementation based on `Fiber` primitives.
10
+ # Requires that `Fiber.scheduler` be set in order to work.
11
+ #
12
+ # Several benefits of using `FiberExecutor` over using `Fiber.schedule` directly:
13
+ #
14
+ # - By default `Fiber` instances run via `Fiber.schedule`
15
+ # have no straightforward way of returning their final result
16
+ # upon completion
17
+ # like `Thread` and `Ractor` do
18
+ # (both support calling `value` to get the final result).
19
+ # `Future` makes this trivial to do for a scheduled `Fiber`.
20
+ # - `Fiber` instances cannot currently be shared across `Thread` instances
21
+ # (though this may change someday).
22
+ # However, `Future` instances can safely be shared
23
+ # across both `Threads` and `Fibers`
24
+ # (Ractors can share neither `Fiber` nor `Future`
25
+ # and that is unlikely to ever change due to their design).
26
+ #
27
+ # `FiberExecutor` specific details for submission:
28
+ #
29
+ # For `FiberExecutor` the tasks are run immediately upon submission
30
+ # using the `Fiber.schedule` method.
31
+ # This method will return
32
+ # as soon as the Fiber hits a blocking operation
33
+ # or runs the `Fiber` to completion.
34
+ # Thus it is completely possible
35
+ # that the returned `Future` is already completed
36
+ # by the time it is returned to the caller.
37
+ #
38
+ # The `FiberExecutor` implementation does _not_ guarantee
39
+ # that any particular task will be run concurrently
40
+ # with any other particular task;
41
+ # that is dependent
42
+ # on whether the submitted procs/blocks
43
+ # have blocking operations that yield control
44
+ # back to the `Fiber::Scheduler`
45
+ # and whether the `Fiber::Scheduler` properly implements
46
+ # `Fiber` switching for those operations.
47
+ class FiberExecutor
48
+ include Executor
49
+
50
+ # Create a new `FiberExecutor`.
51
+ #
52
+ # Spawns fibers via `Fiber.schedule`.
53
+ #
54
+ # Raises `AsyncFutures::Error`
55
+ # unless `Fiber.scheduler` is set.
56
+ #
57
+ # Because auto-fibers do not yield control
58
+ # unless they encounter a blocking operation,
59
+ # it is completely possible
60
+ # that the `Fiber` runs to completion upon submission.
61
+ # Thus, `submit_concurrent` fails by default
62
+ # unless the parameter `treat_as_concurrent` is set to `true`.
63
+ #
64
+ # All internal state is protected via mutex,
65
+ # so it is safe to use a single `FiberExecutor` instance across multiple threads.
66
+ # However each thread must have its own `Fiber::Scheduler` set
67
+ # in order to successfully call `submit`.
68
+ def initialize(treat_as_concurrent: false)
69
+ raise Error.new('No Fiber.scheduler set') unless Fiber.scheduler
70
+
71
+ super()
72
+ @treat_as_concurrent = treat_as_concurrent
73
+ @is_shutdown = false
74
+ @futures = Set.new
75
+ @mutex = Thread::Mutex.new
76
+
77
+ at_exit { shutdown(wait: false) }
78
+ end
79
+
80
+ # Asynchronously submit a task for execution.
81
+ #
82
+ # See `AsyncFutures::Executor.submit` method for full documentation.
83
+ def submit(*args, **kwargs, &block)
84
+ raise ArgumentError.new('No block given') unless block
85
+
86
+ Future.new.tap do |future|
87
+ @mutex.synchronize do
88
+ raise 'FiberExecutor instance is shutdown' if @is_shutdown
89
+
90
+ # Need to set this immediately to ensure DeadlockError is raised appropriately.
91
+ future.thread = Thread.current
92
+ @futures.add future
93
+ future.add_done_callback { |f| @mutex.synchronize { @futures.delete f } }
94
+ end
95
+
96
+ Fiber.schedule { future.complete(*args, **kwargs, &block) }
97
+ end
98
+ end
99
+
100
+ # Return `true`
101
+ # if `treat_as_concurrent` was passed as `true`
102
+ # to the `FiberExecutor` constructor.
103
+ #
104
+ # Otherwise,
105
+ # return `false`.
106
+ def support_concurrency?
107
+ !!@treat_as_concurrent
108
+ end
109
+
110
+ # Shutdown `FiberExecutor` instance.
111
+ #
112
+ # See `AsyncFutures::Executor.shutdown` for full documentation.
113
+ def shutdown(wait: true, cancel_futures: false, &block) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
114
+ block&.call(self)
115
+ ensure
116
+ unless check_and_set_shutdown!
117
+ futures_dup = @mutex.synchronize { @futures.dup } if wait || cancel_futures
118
+ futures_dup.reject!(&:cancel) if cancel_futures
119
+
120
+ # This will deadlock outside a FiberScheduler,
121
+ futures_dup.reject!(&:join) if wait
122
+ @mutex.synchronize { @futures.replace(@futures & futures_dup) } if wait || cancel_futures
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ # Returns the current shutdown state,
129
+ # then sets internal shutdown state to `true`.
130
+ # This is all done atomically to avoid race conditions.
131
+ def check_and_set_shutdown!
132
+ @mutex.synchronize do
133
+ return true if @is_shutdown
134
+
135
+ @is_shutdown = true
136
+ return false
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,553 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'logger'
4
+ require_relative 'error'
5
+
6
+ require 'timeout'
7
+
8
+ module AsyncFutures
9
+ # Class for async execution results.
10
+ #
11
+ # Heavily inspired by Python's `concurrent.futures.Future` class.
12
+ class Future # rubocop:disable Metrics/ClassLength
13
+ # The `Future.wait` method will return when any future finishes or is cancelled.
14
+ FIRST_COMPLETED = :FIRST_COMPLETED
15
+
16
+ # The `Future.wait` method will return when any future finishes by raising an exception.
17
+ # If no future raises an exception then it is equivalent to ALL_COMPLETED.
18
+ FIRST_EXCEPTION = :FIRST_EXCEPTION
19
+
20
+ # The `Future.wait` method will return when all futures finish or are cancelled.
21
+ ALL_COMPLETED = :ALL_COMPLETED
22
+
23
+ class << self
24
+ # Wait for the `Future` instances
25
+ # (possibly created by different Executor instances)
26
+ # given by `Enumerable` object `futures` to complete.
27
+ # Duplicate futures given to `futures` are removed
28
+ # and will be returned only once.
29
+ #
30
+ # Returns a `Hash` of sets.
31
+ # The first set,
32
+ # keyed to `:done`,
33
+ # contains the futures that completed
34
+ # (finished or cancelled futures)
35
+ # before the wait completed.
36
+ # The second set,
37
+ # keyed to `:not_done`,
38
+ # contains the futures that did not complete
39
+ # (pending or running futures).
40
+ #
41
+ # `timeout` can be used to control the maximum number of seconds to wait before returning.
42
+ # `timeout` can be an int or float.
43
+ # If `timeout` is not specified or `nil`,
44
+ # there is no limit to the wait time.
45
+ #
46
+ # A negative value for `timeout` is allowed
47
+ # and will just return immediately.
48
+ # Already completed futures are still included in this case.
49
+ # In this circumstance,
50
+ # all `return_when` values behave identically.
51
+ #
52
+ # `return_when` indicates when this function should return.
53
+ # See constant descriptions for details.
54
+ def wait(futures, timeout = nil, return_when = ALL_COMPLETED) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
55
+ clock_timeout = Time.now.to_f + timeout if timeout
56
+ mtx = Thread::Mutex.new
57
+ queue = Thread::Queue.new
58
+
59
+ fs_ary = futures.to_a.uniq
60
+ fs_cnt = fs_ary.size
61
+
62
+ done_set = Set.new
63
+ not_done_set = Set.new(fs_ary)
64
+
65
+ return { done: done_set, not_done: not_done_set } if fs_ary.empty?
66
+
67
+ case return_when
68
+ when FIRST_COMPLETED
69
+ fs_ary.each do |future|
70
+ future.add_done_callback do |ftr|
71
+ mtx.synchronize do
72
+ queue.push(ftr)
73
+ queue.close
74
+ rescue ClosedQueueError
75
+ # Do nothing
76
+ end
77
+ end
78
+ end
79
+ when FIRST_EXCEPTION
80
+ fs_ary.each do |future|
81
+ future.add_done_callback do |ftr|
82
+ mtx.synchronize do
83
+ queue.push(ftr)
84
+ queue.close if !ftr.cancelled? && ftr.exception
85
+ rescue ClosedQueueError
86
+ # Do nothing
87
+ end
88
+ end
89
+ end
90
+ when ALL_COMPLETED
91
+ fs_ary.each do |future|
92
+ future.add_done_callback do |ftr|
93
+ queue.push(ftr)
94
+
95
+ mtx.synchronize do
96
+ fs_cnt -= 1
97
+ queue.close if fs_cnt.zero?
98
+ end
99
+ rescue ClosedQueueError
100
+ # Do nothing
101
+ end
102
+ end
103
+ else
104
+ raise ArgumentError.new("Unknown 'return_when' value '#{return_when}'")
105
+ end
106
+
107
+ begin
108
+ cb_timeout = timeout && (clock_timeout - Time.now.to_f)
109
+ raise Timeout::Error unless cb_timeout.nil? || cb_timeout.positive?
110
+
111
+ Timeout.timeout(cb_timeout) do
112
+ while (dn_ftr = queue.pop)
113
+ done_set.add(dn_ftr)
114
+ end
115
+ end
116
+ rescue Timeout::Error
117
+ queue.close
118
+ while (dn_ftr = queue.pop)
119
+ done_set.add(dn_ftr)
120
+ end
121
+ end
122
+
123
+ done_set.merge(fs_ary.lazy.filter(&:done?))
124
+ { done: done_set, not_done: not_done_set.difference(done_set) }
125
+ end
126
+
127
+ # Returns an `Enumerator` over the `Future` instances
128
+ # (possibly created by different `Executor` instances)
129
+ # given by the `Enumerable` object `futures`
130
+ # that yields futures as they complete
131
+ # (finished or cancelled futures).
132
+ #
133
+ # The returned `Enumerator` can only be enumerated over once.
134
+ # Subsequent enumeration attempts will raise `RuntimeError`.
135
+ #
136
+ # Any futures given by `futures` that are duplicated will be returned once.
137
+ #
138
+ # Any futures that completed before `as_completed()` is called will be yielded first.
139
+ #
140
+ # The returned `Enumerator` raises a `Timeout::Error` if `each` or `next()` is called
141
+ # and the result isn’t available after `timeout` seconds
142
+ # from the original call to `as_completed()`.
143
+ # `timeout` can be an int or float.
144
+ # If `timeout` is not specified or `nil`,
145
+ # there is no limit to the wait time.
146
+ def as_completed(futures, timeout = nil) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
147
+ clock_timeout = Time.now.to_f + timeout if timeout
148
+ mtx = Thread::Mutex.new
149
+ queue = Thread::Queue.new
150
+ has_enumerated = false
151
+
152
+ fs_ary = futures.to_a.uniq
153
+ fs_sze = fs_ary.size
154
+ fs_cnt = fs_sze
155
+
156
+ cb_timeout = timeout && (clock_timeout - Time.now.to_f)
157
+ raise Timeout::Error unless cb_timeout.nil? || cb_timeout.positive?
158
+
159
+ Timeout.timeout(cb_timeout) do
160
+ fs_ary.each do |future|
161
+ future.add_done_callback do |done_future|
162
+ queue.push done_future
163
+
164
+ mtx.synchronize do
165
+ fs_cnt -= 1
166
+ queue.close if fs_cnt.zero?
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ Enumerator.new(fs_sze) do |yielder|
173
+ raise 'Enumerator already consumed' if mtx.synchronize { has_enumerated }
174
+
175
+ enum_timeout = timeout && (clock_timeout - Time.now.to_f)
176
+ raise Timeout::Error unless enum_timeout.nil? || enum_timeout.positive?
177
+
178
+ Timeout.timeout(enum_timeout) do
179
+ while (done_future = queue.pop)
180
+ yielder.yield done_future
181
+ end
182
+ end
183
+ ensure
184
+ mtx.synchronize { has_enumerated = true }
185
+ end
186
+ end
187
+ end
188
+
189
+ # Create a new Future instance in a pending state.
190
+ # Should generally only be called by Executor implementations.
191
+ def initialize
192
+ @mutex = Thread::Mutex.new
193
+ @condition = Thread::ConditionVariable.new
194
+ @state = PENDING
195
+ @result = nil
196
+ @exception = nil
197
+ @done_callbacks = []
198
+ @thread = nil
199
+ @fiber = nil
200
+ end
201
+
202
+ # The future can’t be frozen, so this method raises an exception:
203
+ #
204
+ # ```ruby
205
+ # AsyncFutures::Future.new.freeze # Raises TypeError (cannot freeze #<AsyncFutures::Future:0x...>)
206
+ # ```
207
+ def freeze
208
+ raise TypeError.new("cannot freeze #{self}")
209
+ end
210
+
211
+ # Convenience method to complete the future
212
+ # with the given block, args, and kwargs.
213
+ #
214
+ # This method will only run the given block
215
+ # if the future is *not* already running, canceled, or completed.
216
+ #
217
+ # It will return `true` if the block was run by this call
218
+ # and `false` if it was *not* run by this call.
219
+ def complete(*args, **kwargs, &block) # rubocop:disable Style/ArgumentsForwarding,Naming/PredicateMethod
220
+ raise ArgumentError.new('No block given') unless block
221
+
222
+ begin
223
+ return false unless set_running_or_notify_cancel(set_context: true)
224
+ rescue InvalidStateError
225
+ # RUNNING, CANCELLED_AND_NOTIFIED, or FINISHED states.
226
+ return false
227
+ end
228
+
229
+ begin
230
+ result = block.call(*args, **kwargs) # rubocop:disable Style/ArgumentsForwarding
231
+ rescue Exception => e # rubocop:disable Lint/RescueException
232
+ set_exception(e)
233
+ else
234
+ set_result(result)
235
+ end
236
+ true
237
+ end
238
+
239
+ # The Fiber that owns the work for this Future.
240
+ # Used to detect deadlocks.
241
+ # Not for direct use.
242
+ # Should only be used by Future and Executor implementations.
243
+ def fiber
244
+ @mutex.synchronize { @fiber }
245
+ end
246
+
247
+ # Set fiber attribute.
248
+ # Should only be used by Future and Executor implementations.
249
+ def fiber=(value)
250
+ @mutex.synchronize { @fiber = value }
251
+ end
252
+
253
+ # The Thread that owns the work for this Future.
254
+ # Used to detect deadlocks.
255
+ # Not for direct use.
256
+ # Should only be used by Future and Executor implementations.
257
+ def thread
258
+ @mutex.synchronize { @thread }
259
+ end
260
+
261
+ # Set thread attribute.
262
+ # Should only be used by Future and Executor implementations.
263
+ def thread=(value)
264
+ @mutex.synchronize { @thread = value }
265
+ end
266
+
267
+ # Attempt to cancel the call. If the call is currently being executed or
268
+ # finished running and cannot be cancelled then the method will return
269
+ # `False`, otherwise the call will be cancelled and the method will return
270
+ # `True`.
271
+ def cancel # rubocop:disable Naming/PredicateMethod
272
+ @mutex.synchronize do
273
+ return true if lockless_cancelled?
274
+ return false if lockless_running? || lockless_finished?
275
+
276
+ # The only other state left is PENDING, so we can safely cancel.
277
+ @state = CANCELLED
278
+ @condition.broadcast
279
+ end
280
+
281
+ invoke_callbacks
282
+ true
283
+ end
284
+
285
+ # Return `True` if the call has not yet started.
286
+ #
287
+ # Not present on Python `concurrent.futures.Future` class.
288
+ def pending?
289
+ @mutex.synchronize { lockless_pending? }
290
+ end
291
+
292
+ # Return `True` if the call finished running and was not cancelled.
293
+ #
294
+ # Not present on Python `concurrent.futures.Future` class.
295
+ def finished?
296
+ @mutex.synchronize { lockless_finished? }
297
+ end
298
+
299
+ # Return `True` if the call was successfully cancelled.
300
+ def cancelled?
301
+ @mutex.synchronize { lockless_cancelled? }
302
+ end
303
+
304
+ # Return `True` if the call is currently being executed and cannot be cancelled.
305
+ def running?
306
+ @mutex.synchronize { lockless_running? }
307
+ end
308
+
309
+ # Return `True` if the call was successfully cancelled or finished running.
310
+ def done?
311
+ @mutex.synchronize { lockless_done? }
312
+ end
313
+
314
+ # Return the value returned by the call. If the call hasn't yet completed
315
+ # then this method will wait up to `timeout` seconds. If the call
316
+ # hasn't completed in `timeout` seconds, then a `Timeout::Error` will
317
+ # be raised. `timeout` can be an int or float. If `timeout` is not
318
+ # specified or `nil`, there is no limit to the wait time.
319
+ #
320
+ # If the future is cancelled before completing then `CancelledError` will
321
+ # be raised.
322
+ #
323
+ # If the call raised an exception, this method will raise the same
324
+ # exception.
325
+ def result(timeout = nil)
326
+ private_join(timeout) do
327
+ raise CancelledError if lockless_cancelled?
328
+ raise @exception if @exception
329
+
330
+ @result
331
+ end
332
+ end
333
+
334
+ # Return the exception raised by the call. If the call hasn't yet completed
335
+ # then this method will wait up to `timeout` seconds. If the call
336
+ # hasn't completed in `timeout` seconds, then a `Timeout::Error` will
337
+ # be raised. `timeout` can be an int or float. If `timeout` is not
338
+ # specified or `nil`, there is no limit to the wait time.
339
+ #
340
+ # If the future is cancelled before completing then `CancelledError` will
341
+ # be raised.
342
+ #
343
+ # If the call completed without raising, `nil` is returned.
344
+ def exception(timeout = nil)
345
+ private_join(timeout) do
346
+ raise CancelledError if lockless_cancelled?
347
+
348
+ @exception
349
+ end
350
+ end
351
+
352
+ # Wait for future to be `done?`
353
+ # (through regular completion, exception, or cancellation),
354
+ # then return `self`.
355
+ # If the call hasn't yet completed
356
+ # then this method will wait up to `timeout` seconds.
357
+ # If the call hasn't completed in `timeout` seconds,
358
+ # then `nil` will be returned.
359
+ # `timeout` can be an int or float.
360
+ # If `timeout` is not specified or `nil`,
361
+ # there is no limit to the wait time.
362
+ #
363
+ # Calling `join` with a `timeout` value of zero
364
+ # will return immediately.
365
+ # This is effectively equivalent to calling `done?`.
366
+ #
367
+ # Not present on Python's `concurrent.futures.Future` class.
368
+ def join(timeout = nil)
369
+ return (done? && self) || nil if timeout&.zero?
370
+
371
+ private_join(timeout) do
372
+ self
373
+ end
374
+ rescue Timeout::Error
375
+ nil
376
+ end
377
+
378
+ # Attaches a block that will be called when the future finishes.
379
+ #
380
+ # The block will be called with this future as its only argument
381
+ # when the future completes or is cancelled.
382
+ # The block will always be called by a Thread in the same Ractor
383
+ # in which it was added.
384
+ # If the future has already completed
385
+ # or been cancelled then the block will be called immediately.
386
+ # These blocks are called in the order that they were added.
387
+ def add_done_callback(&block)
388
+ raise ArgumentError.new('No block given') unless block
389
+
390
+ @mutex.synchronize do
391
+ unless lockless_done?
392
+ @done_callbacks.append(block)
393
+ return
394
+ end
395
+ end
396
+
397
+ # If we reached here, the future already ended, just call the block immediately.
398
+ begin
399
+ block.call(self)
400
+ rescue Exception # rubocop:disable Lint/RescueException
401
+ logger&.error { "Exception calling callback for #{self}" }
402
+ end
403
+ end
404
+
405
+ # This method should only be called by `Executor` implementations
406
+ # before executing the work associated with the `Future`
407
+ # and by unit tests.
408
+ #
409
+ # If the method returns `false` then the `Future` was cancelled,
410
+ # i.e. `Future.cancel` was called and returned `true`.
411
+ # Any threads waiting on the `Future` completing
412
+ # (i.e. through `Future.as_completed()` or `Future.wait()`) will be woken up.
413
+ #
414
+ # If the method returns true
415
+ # then the `Future` was not cancelled
416
+ # and has been put in the running state,
417
+ # i.e. calls to `Future.running?` will return true.
418
+ #
419
+ # This method should only be called once.
420
+ # If it is called more than once,
421
+ # then it will raise an `InvalidStateError` exception.
422
+ # If it is called after `Future.set_result()`
423
+ # or `Future.set_exception()` have been called
424
+ # then it will raise an `InvalidStateError` exception.
425
+ # Thus, this is why it is more of an implementation detail
426
+ # for Executor implementations (or similar).
427
+ def set_running_or_notify_cancel(set_context: false)
428
+ @mutex.synchronize do
429
+ case @state
430
+ when CANCELLED
431
+ @state = CANCELLED_AND_NOTIFIED
432
+ @condition.broadcast
433
+ return false
434
+ when PENDING
435
+ @state = RUNNING
436
+ @condition.broadcast
437
+ if set_context
438
+ @thread = Thread.current
439
+ @fiber = Fiber.current
440
+ end
441
+ return true
442
+ else
443
+ # raised for RUNNING, CANCELLED_AND_NOTIFIED, and FINISHED states.
444
+ raise InvalidStateError.new(self, @state)
445
+ end
446
+ end
447
+ end
448
+
449
+ # Sets the result of the work associated with the `Future` to result.
450
+ #
451
+ # This method should only be used by `Executor` implementations and unit tests.
452
+ def set_result(result) # rubocop:disable Naming/AccessorMethodName
453
+ @mutex.synchronize do
454
+ raise InvalidStateError.new(self, @state) if lockless_done?
455
+
456
+ @result = result
457
+ @state = FINISHED
458
+ @condition.broadcast
459
+ end
460
+ invoke_callbacks
461
+ end
462
+
463
+ # Sets the result of the work associated with the `Future` to the Exception `exception`.
464
+ #
465
+ # This method should only be used by `Executor` implementations and unit tests.
466
+ def set_exception(exception) # rubocop:disable Naming/AccessorMethodName
467
+ @mutex.synchronize do
468
+ raise InvalidStateError.new(self, @state) if lockless_done?
469
+ raise ArgumentError.new("Not an Exception: #{exception.inspect}") unless exception.is_a?(Exception)
470
+
471
+ @exception = exception
472
+ @state = FINISHED
473
+ @condition.broadcast
474
+ end
475
+ invoke_callbacks
476
+ end
477
+
478
+ private
479
+
480
+ # Possible future states (for internal use by the futures package).
481
+
482
+ # Not yet started.
483
+ PENDING = :PENDING
484
+
485
+ # Has a worker doing work to complete it.
486
+ RUNNING = :RUNNING
487
+
488
+ # The future was cancelled.
489
+ CANCELLED = :CANCELLED
490
+
491
+ # Future has been cancelled
492
+ # **and** the worker assigned to complete the future has been notified.
493
+ # of the fact that the future has been cancelled.
494
+ # Only set by calling `set_running_or_notify_cancel` on a cancelled future.
495
+ # This prevents future from be set to running or cancelled more than once.
496
+ # Instead it raises an InvalidStateError if this is the state.
497
+ CANCELLED_AND_NOTIFIED = :CANCELLED_AND_NOTIFIED
498
+
499
+ # Finished running, via either success or exception.
500
+ FINISHED = :FINISHED
501
+
502
+ # Make all internal states private visibility
503
+ private_constant :PENDING, :RUNNING, :CANCELLED, :CANCELLED_AND_NOTIFIED, :FINISHED
504
+
505
+ def private_join(timeout, &block)
506
+ Timeout.timeout(timeout) do
507
+ @mutex.synchronize do
508
+ unless lockless_done?
509
+ raise DeadlockError.new(self) if Fiber.blocking? && Thread.current.equal?(@thread)
510
+ raise DeadlockError.new(self) if Fiber.current.equal?(@fiber)
511
+ end
512
+
513
+ @condition.wait(@mutex) until lockless_done?
514
+ block.call
515
+ end
516
+ end
517
+ end
518
+
519
+ def invoke_callbacks
520
+ @done_callbacks.each do |callback|
521
+ callback.call(self)
522
+ rescue Exception # rubocop:disable Lint/RescueException
523
+ logger&.error { "Exception calling callback for #{self}" }
524
+ end
525
+ end
526
+
527
+ def logger
528
+ AsyncFutures.logger
529
+ end
530
+
531
+ # Only safe to use these methods within the synchronized mutex.
532
+ # Thus why they are private.
533
+ def lockless_cancelled?
534
+ [CANCELLED, CANCELLED_AND_NOTIFIED].include? @state
535
+ end
536
+
537
+ def lockless_finished?
538
+ @state.equal? FINISHED
539
+ end
540
+
541
+ def lockless_pending?
542
+ @state.equal? PENDING
543
+ end
544
+
545
+ def lockless_running?
546
+ @state.equal? RUNNING
547
+ end
548
+
549
+ def lockless_done?
550
+ [CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED].include? @state
551
+ end
552
+ end
553
+ end