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.
- checksums.yaml +7 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +12 -0
- data/README.md +220 -0
- data/Rakefile +52 -0
- data/lib/async_futures/error.rb +39 -0
- data/lib/async_futures/executor.rb +224 -0
- data/lib/async_futures/fiber_executor.rb +140 -0
- data/lib/async_futures/future.rb +553 -0
- data/lib/async_futures/io_async.rb +313 -0
- data/lib/async_futures/logger.rb +12 -0
- data/lib/async_futures/process_executor.rb +155 -0
- data/lib/async_futures/ractor_executor.rb +353 -0
- data/lib/async_futures/thread_executor.rb +286 -0
- data/lib/async_futures/version.rb +6 -0
- data/lib/async_futures.rb +20 -0
- data/sig/async_futures.rbs +4 -0
- metadata +62 -0
|
@@ -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
|