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,313 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'future'
|
|
4
|
+
|
|
5
|
+
require 'timeout'
|
|
6
|
+
require 'openssl'
|
|
7
|
+
require 'stringio'
|
|
8
|
+
require 'set' # rubocop:disable Lint/RedundantRequireStatement
|
|
9
|
+
|
|
10
|
+
module AsyncFutures
|
|
11
|
+
# This is a simple example mixin module
|
|
12
|
+
# to add async IO to the `IO` and `OpenSSL::SSLSocket` classes.
|
|
13
|
+
# You can also `include` this in other classes
|
|
14
|
+
# so long as they have the methods `write_nonblock` and `read_nonblock`
|
|
15
|
+
# and they behave the same way as `IO` and/or `OpenSSL::SSLSocket`.
|
|
16
|
+
#
|
|
17
|
+
# All reads and writes are done on (at least) one background worker thread.
|
|
18
|
+
#
|
|
19
|
+
# This is not the most efficient implementation.
|
|
20
|
+
# It is just meant to be an example
|
|
21
|
+
# of how one can use the `Future` class
|
|
22
|
+
# outside of an `Executor` implementation.
|
|
23
|
+
#
|
|
24
|
+
# It's only real efficiencies, so to speak,
|
|
25
|
+
# are that it doesn't use a thread per future
|
|
26
|
+
# (which would potentially use up a lot of memory),
|
|
27
|
+
# nor do futures need to wait until a thread opens up on a worker pool
|
|
28
|
+
# (which would block newer IO work until older IO work completely finished).
|
|
29
|
+
# Instead work is picked up immediately
|
|
30
|
+
# on at least one background worker thread,
|
|
31
|
+
# and attempts to read/write are started immediately
|
|
32
|
+
# via `read_nonblock` and `write_nonblock`.
|
|
33
|
+
# If `read_nonblock`/`write_nonblock` cannot proceed
|
|
34
|
+
# (because they would block)
|
|
35
|
+
# for any particular `IO` object
|
|
36
|
+
# then the next operation for the next `IO` object is attempted,
|
|
37
|
+
# and so forth,
|
|
38
|
+
# until all the work is completed (or a timeout happens).
|
|
39
|
+
#
|
|
40
|
+
# However, all of this is accomplished via a simplistic, unoptimized busy loop.
|
|
41
|
+
# This is less than ideal.
|
|
42
|
+
# There are some simple sleeps and timeouts added
|
|
43
|
+
# to avoid completely eating up the CPU,
|
|
44
|
+
# but this is still a very naive approach.
|
|
45
|
+
#
|
|
46
|
+
# A better implementation would utilize higher performance OS specific features
|
|
47
|
+
# like FreeBSD's kqueue/aio or Linux's epoll/io_uring.
|
|
48
|
+
# However, the logic for integrating these
|
|
49
|
+
# is beyond the scope of this example code.
|
|
50
|
+
#
|
|
51
|
+
# This could probably be done using the FFI library fiddle,
|
|
52
|
+
# which is bundled with ruby,
|
|
53
|
+
# so it wouldn't need to reach outside the standard library.
|
|
54
|
+
# However I have a very good reason for not doing that right now:
|
|
55
|
+
# I don't want to.
|
|
56
|
+
module IOAsync
|
|
57
|
+
# Return an incomplete future
|
|
58
|
+
# that will eventually contain an integer with the number of bytes written
|
|
59
|
+
# or an exception if the string could not be written for some reason.
|
|
60
|
+
#
|
|
61
|
+
# The `string` argument is written in a nonblocking fashion
|
|
62
|
+
# on a background worker thread.
|
|
63
|
+
#
|
|
64
|
+
# The optional `timeout` argument
|
|
65
|
+
# causes the work to finish the future exceptionally with `Timeout::Error`
|
|
66
|
+
# if it takes longer than `timeout` seconds to complete.
|
|
67
|
+
# This is used to avoid having background work that spins forever
|
|
68
|
+
# on IO that may never complete.
|
|
69
|
+
# If `nil` or no value is given, this means no timeout
|
|
70
|
+
# (i.e. potentially spin indefinitely).
|
|
71
|
+
#
|
|
72
|
+
# This should *not* be confused with the `Timeout::Error` raised via the
|
|
73
|
+
# `timeout` argument on `Future.result` and `Future.exception`.
|
|
74
|
+
# If it matters for your purposes to differentiate between the two,
|
|
75
|
+
# you can do something like the following:
|
|
76
|
+
#
|
|
77
|
+
# ```ruby
|
|
78
|
+
# # `join` returns `nil` on timeout
|
|
79
|
+
# result = if future.join(1.0)
|
|
80
|
+
# # If `Timeout::Error` is raised here,
|
|
81
|
+
# # it is from the `timeout` parameter to the `*_async` method.
|
|
82
|
+
# future.result
|
|
83
|
+
# else
|
|
84
|
+
# raise Timeout::Error.new('Timed out on `join`')
|
|
85
|
+
# end
|
|
86
|
+
# ```
|
|
87
|
+
#
|
|
88
|
+
# The optional `sleep_timeout` keyword argument
|
|
89
|
+
# is used to determine how quickly the worker thread
|
|
90
|
+
# stops polling the input work queue
|
|
91
|
+
# and how much sleep time happens between failed nonblocking IO attempts.
|
|
92
|
+
# It defaults to 1ms.
|
|
93
|
+
# If existing worker(s) have already been spawned,
|
|
94
|
+
# then this argument isn't used.
|
|
95
|
+
#
|
|
96
|
+
# If the process shuts down before the future can be fully completed,
|
|
97
|
+
# the work may be abandoned even if it partially completed.
|
|
98
|
+
def write_async(string, timeout = nil, sleep_timeout: 0.001) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
99
|
+
Future.new.tap do |ftr|
|
|
100
|
+
clock_timeout = timeout && (Time.now.to_f + timeout)
|
|
101
|
+
|
|
102
|
+
work_proc = proc do
|
|
103
|
+
# :nocov:
|
|
104
|
+
break ftr unless ftr.set_running_or_notify_cancel(set_context: true)
|
|
105
|
+
# :nocov:
|
|
106
|
+
|
|
107
|
+
to_err = Timeout::Error.new('execution expired')
|
|
108
|
+
all_written = 0
|
|
109
|
+
|
|
110
|
+
loop do
|
|
111
|
+
cur_to = timeout && (Time.now.to_f - clock_timeout)
|
|
112
|
+
break ftr.tap { ftr.set_exception(to_err) } unless timeout.nil? || cur_to.positive?
|
|
113
|
+
|
|
114
|
+
bytes_written = write_nonblock(string)
|
|
115
|
+
string = string[bytes_written..nil]
|
|
116
|
+
all_written += bytes_written
|
|
117
|
+
break ftr.tap { ftr.set_result(all_written) } if string.empty?
|
|
118
|
+
rescue IO::WaitReadable, IO::WaitWritable, Errno::EINTR
|
|
119
|
+
Fiber.yield nil
|
|
120
|
+
retry
|
|
121
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
122
|
+
break ftr.tap { ftr.set_exception(e) }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
io_async_queue.push(work_proc)
|
|
127
|
+
maybe_spawn_worker(sleep_timeout)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Return an incomplete future
|
|
132
|
+
# that will eventually contain the string value read from the IO object
|
|
133
|
+
# or an exception if the IO object could not be read from for some reason.
|
|
134
|
+
#
|
|
135
|
+
# A string up to `maxlen` in length is read in a nonblocking fashion
|
|
136
|
+
# on a background worker thread.
|
|
137
|
+
#
|
|
138
|
+
# The optional `timeout` argument
|
|
139
|
+
# causes the work to finish the future exceptionally with `Timeout::Error`
|
|
140
|
+
# if it takes longer than `timeout` seconds to complete.
|
|
141
|
+
# This is used to avoid having background work that spins forever
|
|
142
|
+
# on IO that may never complete.
|
|
143
|
+
# If `nil` or no value is given, this means no timeout
|
|
144
|
+
# (i.e. potentially spin indefinitely).
|
|
145
|
+
#
|
|
146
|
+
# This should *not* be confused with the `Timeout::Error` raised via the
|
|
147
|
+
# `timeout` argument on `Future.result` and `Future.exception`.
|
|
148
|
+
# If it matters for your purposes to differentiate between the two,
|
|
149
|
+
# you can do something like the following:
|
|
150
|
+
#
|
|
151
|
+
# ```ruby
|
|
152
|
+
# # `join` returns `nil` on timeout
|
|
153
|
+
# result = if future.join(1.0)
|
|
154
|
+
# # If `Timeout::Error` is raised here,
|
|
155
|
+
# # it is from the `timeout` parameter to the `*_async` method.
|
|
156
|
+
# future.result
|
|
157
|
+
# else
|
|
158
|
+
# raise Timeout::Error.new('Timed out on `join`')
|
|
159
|
+
# end
|
|
160
|
+
# ```
|
|
161
|
+
#
|
|
162
|
+
# The optional `sleep_timeout` keyword argument
|
|
163
|
+
# is used to determine how quickly the worker thread
|
|
164
|
+
# stops polling the input work queue
|
|
165
|
+
# and how much sleep time happens between failed nonblocking IO attempts.
|
|
166
|
+
# It defaults to 1ms.
|
|
167
|
+
# If existing worker(s) have already been spawned,
|
|
168
|
+
# then this argument isn't used.
|
|
169
|
+
#
|
|
170
|
+
# If the process shuts down before the future can be fully completed,
|
|
171
|
+
# the work may be abandoned even if it partially completed.
|
|
172
|
+
def read_async(maxlen, timeout = nil, sleep_timeout: 0.001) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
173
|
+
Future.new.tap do |ftr|
|
|
174
|
+
clock_timeout = timeout && (Time.now.to_f + timeout)
|
|
175
|
+
|
|
176
|
+
work_proc = proc do
|
|
177
|
+
# :nocov:
|
|
178
|
+
break ftr unless ftr.set_running_or_notify_cancel(set_context: true)
|
|
179
|
+
# :nocov:
|
|
180
|
+
|
|
181
|
+
to_err = Timeout::Error.new('execution expired')
|
|
182
|
+
to_read_length = maxlen
|
|
183
|
+
retrieved_str = String.new
|
|
184
|
+
|
|
185
|
+
loop do
|
|
186
|
+
cur_to = timeout && (Time.now.to_f - clock_timeout)
|
|
187
|
+
break ftr.tap { ftr.set_exception(to_err) } unless timeout.nil? || cur_to.positive?
|
|
188
|
+
|
|
189
|
+
retrieved_str << read_nonblock(to_read_length)
|
|
190
|
+
to_read_length = maxlen - retrieved_str.size
|
|
191
|
+
break ftr.tap { ftr.set_result(retrieved_str) } if to_read_length.zero?
|
|
192
|
+
rescue IO::WaitReadable, IO::WaitWritable, Errno::EINTR
|
|
193
|
+
Fiber.yield nil
|
|
194
|
+
retry
|
|
195
|
+
rescue EOFError
|
|
196
|
+
break ftr.tap { ftr.set_result(retrieved_str) }
|
|
197
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
198
|
+
break ftr.tap { ftr.set_exception(e) }
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
io_async_queue.push(work_proc)
|
|
203
|
+
maybe_spawn_worker(sleep_timeout)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
# This method will spawn *at least* one worker thread.
|
|
210
|
+
# It *may* spawn more than one worker thread
|
|
211
|
+
# based on submission thread timing,
|
|
212
|
+
# but that is ok,
|
|
213
|
+
# because they will all eventually reap once they run out of work
|
|
214
|
+
# and the individual work is grabbed exclusively per thread
|
|
215
|
+
# via the thread safe queue.
|
|
216
|
+
#
|
|
217
|
+
# If work starts up again after reaping all threads,
|
|
218
|
+
# then new worker thread(s) will be spawned again.
|
|
219
|
+
def maybe_spawn_worker(sleep_timeout)
|
|
220
|
+
Ractor[:io_async_worker] ||= Thread.new do
|
|
221
|
+
worker_fibers = Set.new
|
|
222
|
+
|
|
223
|
+
while (fproc = io_async_queue.pop(timeout: sleep_timeout))
|
|
224
|
+
worker_fibers.add(Fiber.new(&fproc))
|
|
225
|
+
worker_fibers.reject!(&:resume)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# We need to unset the worker thread
|
|
229
|
+
# since we're no longer polling the input queue.
|
|
230
|
+
#
|
|
231
|
+
# If more work comes in,
|
|
232
|
+
# new worker thread(s) need to be spawned.
|
|
233
|
+
Ractor[:io_async_worker] = nil
|
|
234
|
+
|
|
235
|
+
# FIXME: if we have IO that never finishes,
|
|
236
|
+
# this will create zombie threads that busy loop and never complete.
|
|
237
|
+
until worker_fibers.empty?
|
|
238
|
+
# Sleep when no fiber in the set completes
|
|
239
|
+
sleep(sleep_timeout) unless worker_fibers.reject!(&:resume)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
GLOBAL_MUTEX = Thread::Mutex.new
|
|
245
|
+
|
|
246
|
+
private_constant :GLOBAL_MUTEX
|
|
247
|
+
|
|
248
|
+
def io_async_queue
|
|
249
|
+
GLOBAL_MUTEX.synchronize do
|
|
250
|
+
Ractor[:io_async_queue] ||= Thread::Queue.new
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Simple mixin for sync IO with an async interface.
|
|
256
|
+
module IOSync
|
|
257
|
+
# Return a completed future
|
|
258
|
+
# containing an integer with the number of bytes written.
|
|
259
|
+
#
|
|
260
|
+
# This exists for classes such as `StringIO` to maintain compatibility
|
|
261
|
+
# with classes with true nonblocking methods (such as `IO`).
|
|
262
|
+
#
|
|
263
|
+
# There is no performance benefit
|
|
264
|
+
# to calling this instead of directly calling `write`.
|
|
265
|
+
# In fact,
|
|
266
|
+
# there may be a slight performance degradation
|
|
267
|
+
# because of the added overhead of instantiating
|
|
268
|
+
# and completing a `Future` object.
|
|
269
|
+
#
|
|
270
|
+
# You should only use this method if you are dealing with a mix
|
|
271
|
+
# of `IO`, `OpenSSL::SSLSocket`, and/or `StringIO` objects
|
|
272
|
+
# and want to interact with them identically in a nonblocking manner.
|
|
273
|
+
# Or you may want to use this method with `StringIO`
|
|
274
|
+
# as a type of mock object for testing
|
|
275
|
+
# in place of real `IO` or `OpenSSL::SSLSocket` objects.
|
|
276
|
+
def write_async(string, *args, **kwargs) # rubocop:disable Lint/UnusedMethodArgument
|
|
277
|
+
Future.new.tap do |ftr|
|
|
278
|
+
ftr.complete(string, &method(:write))
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Return a completed future
|
|
283
|
+
# containing a string up to `maxlen` bytes long.
|
|
284
|
+
#
|
|
285
|
+
# This exists for classes such as `StringIO` to maintain compatibility
|
|
286
|
+
# with classes with true nonblocking methods (such as `IO`).
|
|
287
|
+
#
|
|
288
|
+
# There is no performance benefit
|
|
289
|
+
# to calling this instead of directly calling `read`.
|
|
290
|
+
# In fact,
|
|
291
|
+
# there may be a slight performance degradation
|
|
292
|
+
# because of the added overhead of instantiating
|
|
293
|
+
# and completing a `Future` object.
|
|
294
|
+
#
|
|
295
|
+
# You should only use this method if you are dealing with a mix
|
|
296
|
+
# of `IO`, `OpenSSL::SSLSocket`, and/or `StringIO` objects
|
|
297
|
+
# and want to interact with them identically in a nonblocking manner.
|
|
298
|
+
# Or you may want to use this method with `StringIO`
|
|
299
|
+
# as a type of mock object for testing
|
|
300
|
+
# in place of real `IO` or `OpenSSL::SSLSocket` objects.
|
|
301
|
+
def read_async(maxlen, *args, **kwargs) # rubocop:disable Lint/UnusedMethodArgument
|
|
302
|
+
Future.new.tap do |ftr|
|
|
303
|
+
ftr.complete(maxlen, &method(:read))
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
IO.include AsyncFutures::IOAsync
|
|
310
|
+
|
|
311
|
+
OpenSSL::SSL::SSLSocket.include AsyncFutures::IOAsync
|
|
312
|
+
|
|
313
|
+
StringIO.include AsyncFutures::IOSync
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
module AsyncFutures # rubocop:disable Style/Documentation
|
|
6
|
+
class << self
|
|
7
|
+
# Configurable logger for the library. All calls assume the standard logger interface.
|
|
8
|
+
#
|
|
9
|
+
# Defaults to being unset (i.e. `nil`).
|
|
10
|
+
attr_accessor :logger
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'executor'
|
|
4
|
+
|
|
5
|
+
require 'etc'
|
|
6
|
+
require 'set' # rubocop:disable Lint/RedundantRequireStatement
|
|
7
|
+
|
|
8
|
+
module AsyncFutures
|
|
9
|
+
# `Executor` implementation based on Process forking
|
|
10
|
+
# that uses up to `max_workers` to execute calls concurrently.
|
|
11
|
+
#
|
|
12
|
+
# `ProcessExecutor` specific submission considerations:
|
|
13
|
+
#
|
|
14
|
+
# For `ProcessExecutor` the tasks are never run immediately upon submission.
|
|
15
|
+
# They are placed into a work queue
|
|
16
|
+
# to be picked up later.
|
|
17
|
+
#
|
|
18
|
+
# Process workers are not reused for work.
|
|
19
|
+
# Each task gets a freshly forked process.
|
|
20
|
+
# This is because marshalling anonymous blocks is not trivial;
|
|
21
|
+
# it is simpler to just fork after the block closure has been defined.
|
|
22
|
+
# Use `ThreadExecutor` or `RactorExecutor`
|
|
23
|
+
# for `Executor` implementations that support worker reuse.
|
|
24
|
+
#
|
|
25
|
+
# Consequently, this executor is only really useful for expensive calculations
|
|
26
|
+
# where the startup time for a process
|
|
27
|
+
# is dwarfed by the time needed for the actual work.
|
|
28
|
+
# If RactorExecutor is available on your Ruby version
|
|
29
|
+
# it is almost certainly a better choice than this.
|
|
30
|
+
#
|
|
31
|
+
# This does _not_ guarantee
|
|
32
|
+
# that any particular task will be run concurrently
|
|
33
|
+
# with any other particular task;
|
|
34
|
+
# that is dependent on how many worker threads and tasks there are
|
|
35
|
+
# at any given point in time.
|
|
36
|
+
class ProcessExecutor
|
|
37
|
+
include Executor
|
|
38
|
+
|
|
39
|
+
# Create a new `ProcessExecutor`.
|
|
40
|
+
#
|
|
41
|
+
# Uses a pool of up to `max_workers`
|
|
42
|
+
# to execute tasks concurrently.
|
|
43
|
+
# If no value is given for `max_workers`
|
|
44
|
+
# it will default to `[32, Etc.nprocessors + 4].min`.
|
|
45
|
+
# Workers are spawned lazily as needed
|
|
46
|
+
# when tasks are added to the work queue.
|
|
47
|
+
#
|
|
48
|
+
# The parameter `worker_name_prefix` can be used
|
|
49
|
+
# to optionally add a prefix to generated `Thread` names.
|
|
50
|
+
#
|
|
51
|
+
# If the `reap_after` keyword argument is given,
|
|
52
|
+
# worker threads will be shut down
|
|
53
|
+
# if they haven't received any work after this amount of seconds.
|
|
54
|
+
# If it is `nil` or not given,
|
|
55
|
+
# they will not be reaped until the `ProcessExecutor` instance is `shutdown`.
|
|
56
|
+
def initialize(max_workers: nil, worker_name_prefix: '', reap_after: nil)
|
|
57
|
+
@max_workers = (max_workers || [32, Etc.nprocessors + 4].min).to_i
|
|
58
|
+
@worker_name_prefix = worker_name_prefix.to_s
|
|
59
|
+
@reap_after = reap_after
|
|
60
|
+
@mutex = Thread::Mutex.new
|
|
61
|
+
@tasks = Thread::Queue.new
|
|
62
|
+
@pool = Set.new
|
|
63
|
+
|
|
64
|
+
at_exit { shutdown(wait: false) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Asynchronously submit a task for execution.
|
|
68
|
+
#
|
|
69
|
+
# See `AsyncFutures::Executor.submit` method for full documentation.
|
|
70
|
+
def submit(*args, **kwargs, &block)
|
|
71
|
+
raise ArgumentError.new('No block given') unless block
|
|
72
|
+
raise 'ProcessExecutor instance is shutdown' if @tasks.closed?
|
|
73
|
+
|
|
74
|
+
Future.new.tap do |future|
|
|
75
|
+
@tasks.push([future, block, args, kwargs])
|
|
76
|
+
maybe_spawn_worker
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
alias submit_concurrent submit
|
|
81
|
+
|
|
82
|
+
public :map
|
|
83
|
+
|
|
84
|
+
# Shutdown `ProcessExecutor` instance.
|
|
85
|
+
#
|
|
86
|
+
# See `AsyncFutures::Executor.shutdown` for full documentation.
|
|
87
|
+
def shutdown(wait: true, cancel_futures: false, &block)
|
|
88
|
+
block&.call(self)
|
|
89
|
+
ensure
|
|
90
|
+
unless check_and_set_shutdown!
|
|
91
|
+
if cancel_futures
|
|
92
|
+
while (task = @tasks.pop)
|
|
93
|
+
future = task[0]
|
|
94
|
+
future.cancel
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if wait
|
|
99
|
+
synchronize { @pool.dup }.each do |thread|
|
|
100
|
+
thread.join
|
|
101
|
+
synchronize { @pool.delete(thread) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def synchronize(&)
|
|
110
|
+
@mutex.synchronize(&)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns the current shutdown state,
|
|
114
|
+
# then sets internal shutdown state to `true`.
|
|
115
|
+
# This is all done atomically to avoid race conditions.
|
|
116
|
+
def check_and_set_shutdown!
|
|
117
|
+
synchronize do
|
|
118
|
+
return true if @tasks.closed?
|
|
119
|
+
|
|
120
|
+
@tasks.close
|
|
121
|
+
return false
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Only spawn a worker if one is needed.
|
|
126
|
+
def maybe_spawn_worker
|
|
127
|
+
# synchronize when interacting directly with @pool
|
|
128
|
+
spawn_worker if !@tasks.empty? && synchronize { @pool.size } < @max_workers
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Always spawn a worker
|
|
132
|
+
def spawn_worker # rubocop:disable Metrics/AbcSize
|
|
133
|
+
thread = Thread.new do
|
|
134
|
+
Thread.current.name = "#{@worker_name_prefix}_#{Thread.current.object_id}" unless @worker_name_prefix.empty?
|
|
135
|
+
|
|
136
|
+
while (task = @tasks.pop(timeout: @reap_after))
|
|
137
|
+
tfuture, tblock, targs, tkwargs = task
|
|
138
|
+
|
|
139
|
+
next unless tfuture.set_running_or_notify_cancel
|
|
140
|
+
|
|
141
|
+
begin
|
|
142
|
+
result = tblock.call(*targs, **tkwargs)
|
|
143
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
144
|
+
tfuture.set_exception(e)
|
|
145
|
+
else
|
|
146
|
+
tfuture.set_result(result)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
ensure
|
|
150
|
+
synchronize { @pool.delete Thread.current }
|
|
151
|
+
end
|
|
152
|
+
synchronize { @pool.add thread }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|