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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d1d673d78fe3b8bae70c67b12abf800e58cd79b95a7690c939b3d6d125d4224a
|
|
4
|
+
data.tar.gz: 4806d60aa3da8d213aa286874dbc8c7a90902832acb22e35ed00060595a8419f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9a3ee85e2d1d7a99feaae35124610c1d6265a43b27da902a3e32f683eb32d159a0b4cb7f6441e71de9e04da8827a30504058f9908976273abf11bec038c97fa8
|
|
7
|
+
data.tar.gz: 11cb824b92cd4dee667e477a0f67b0b8fce864ce5eef5dd3781fc5f345d27cfd0cdedb349c95695ab957506a46027f67bb6b278825f147f5f6a84045ff59e375
|
data/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby 4.0.5
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright (C) 2025 Ethan D. Estrada <ethan@misterfidget.com>
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose
|
|
4
|
+
with or without fee is hereby granted.
|
|
5
|
+
|
|
6
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
7
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
8
|
+
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
9
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
|
10
|
+
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
11
|
+
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
12
|
+
THIS SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# AsyncFutures
|
|
2
|
+
|
|
3
|
+
## Launch asynchronous tasks
|
|
4
|
+
|
|
5
|
+
[](https://github.com/eestrada/async_futures/actions/workflows/main.yml)
|
|
6
|
+
|
|
7
|
+
This library is heavily inspired by Python's `concurrent.futures` module.
|
|
8
|
+
It's API (mostly) follows the same one as the Python
|
|
9
|
+
[library](https://docs.python.org/3.13/library/concurrent.futures.html).
|
|
10
|
+
There are some differences to make it more Ruby-ish where it makes sense
|
|
11
|
+
(e.g. taking blocks instead of callable parameters, etc).
|
|
12
|
+
There are also separate submission methods
|
|
13
|
+
for concurrent versus non-concurrent tasks
|
|
14
|
+
for reasons explained later.
|
|
15
|
+
|
|
16
|
+
It has a different name for several reasons:
|
|
17
|
+
|
|
18
|
+
1. The [concurrent-ruby](https://rubygems.org/gems/concurrent-ruby) library
|
|
19
|
+
already exists and is very popular.
|
|
20
|
+
Naming this library `concurrent-futures`
|
|
21
|
+
would place it under the same `Concurrent` module namespace.
|
|
22
|
+
This would be confusing,
|
|
23
|
+
since this project isn't associated with that one.
|
|
24
|
+
2. This library does not _require_ that `Executor` implementations support concurrency,
|
|
25
|
+
only asynchrony.
|
|
26
|
+
(See Loris Cro's excellent article
|
|
27
|
+
[Asynchrony is not Concurrency](https://kristoff.it/blog/asynchrony-is-not-concurrency/)
|
|
28
|
+
to understand the way these terms are used in this project's documentation).
|
|
29
|
+
Consequently, this library implements (and supports) `Executor` implementations
|
|
30
|
+
that conform to an asynchronous interface,
|
|
31
|
+
but can in reality run immediately in synchronous modes.
|
|
32
|
+
This is still logically correct based on Loris Cro's definition of asynchrony:
|
|
33
|
+
the possibility for tasks to run out of order
|
|
34
|
+
and still be correct.
|
|
35
|
+
This means tasks run strictly in order
|
|
36
|
+
(i.e. synchronously)
|
|
37
|
+
are _also_ correct.
|
|
38
|
+
3. The more straightforward gem names [future](https://rubygems.org/gems/future)
|
|
39
|
+
and [futures](https://rubygems.org/gems/futures)
|
|
40
|
+
were already taken.
|
|
41
|
+
|
|
42
|
+
This Gem has multiple `Executor` implementations for creating `Future` instances
|
|
43
|
+
for tasks executed by `Ractor`, `Thread`, and `Fiber` concurrency primitives.
|
|
44
|
+
Users of the library can easily test out the performance differences
|
|
45
|
+
of primitives while only changing their code minimally.
|
|
46
|
+
|
|
47
|
+
Although the base `Executor` module is meant to be used as a mixin interface,
|
|
48
|
+
the module can also be run directly
|
|
49
|
+
to have a synchronous `Executor` implementation
|
|
50
|
+
that runs code immediately
|
|
51
|
+
and returns a completed future at the point of submission.
|
|
52
|
+
Although this may seem pointless, it has the benefit
|
|
53
|
+
that users of this library can trivially change their code execution
|
|
54
|
+
from serial to concurrent and back again
|
|
55
|
+
simply by using different `Executor` implementations.
|
|
56
|
+
In other words, their code does not require multiple complicated code paths
|
|
57
|
+
for correctness: it need only supply a different `Executor` instance
|
|
58
|
+
to get different performance
|
|
59
|
+
(assuming their code logic supports asynchrony
|
|
60
|
+
and doesn't require concurrency for correctness).
|
|
61
|
+
|
|
62
|
+
### Why wouldn't I just use `concurrent-ruby` for concurrency?
|
|
63
|
+
|
|
64
|
+
`concurrent-ruby` is a good library.
|
|
65
|
+
It is mainly focused on `Thread` primitives and thread safety.
|
|
66
|
+
If that is all you want/need, then you should use it.
|
|
67
|
+
|
|
68
|
+
The focus of this library is different.
|
|
69
|
+
This is meant to be a uniform
|
|
70
|
+
(albeit simple)
|
|
71
|
+
interface around _all_ concurrency/async primitives offered by Ruby.
|
|
72
|
+
You can indicate async versus concurrent intent
|
|
73
|
+
using the `submit` versus `submit_concurrent` methods
|
|
74
|
+
on `Executor` implementations.
|
|
75
|
+
It should also be possible to use the `Future` class
|
|
76
|
+
for things like event based libraries (i.e. async)
|
|
77
|
+
that were not intended to be used in this way.
|
|
78
|
+
Thus the `Executor` interface is not required
|
|
79
|
+
for the use of async futures.
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
bundle add async_futures
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
gem install async_futures
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Usage
|
|
96
|
+
|
|
97
|
+
The test suite and benchmark script
|
|
98
|
+
are probably the best place to see example usage.
|
|
99
|
+
|
|
100
|
+
However, here are some quick introductory examples:
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
require 'async_futures'
|
|
105
|
+
|
|
106
|
+
# Base `Executor` mixin module (inline execution):
|
|
107
|
+
bexec = AsyncFutures::Executor
|
|
108
|
+
ftr1 = bexec.submit('world') { |subject| 'hello ' + subject.to_s }
|
|
109
|
+
final_string = ftr1.result
|
|
110
|
+
|
|
111
|
+
begin
|
|
112
|
+
ftr1 = bexec.submit_concurrent('world') { |subject| 'hello ' + subject.to_s }
|
|
113
|
+
rescue AsyncFutures::NoConcurrencyError
|
|
114
|
+
# This will always trigger
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
subjects = ['Alice', 'Bob', 'Charlie']
|
|
118
|
+
final_strings = bexec.map(subjects) { |subject| 'hello ' + subject.to_s }.to_a
|
|
119
|
+
|
|
120
|
+
# `ThreadExecutor`:
|
|
121
|
+
texec = AsyncFutures::ThreadExecutor.new
|
|
122
|
+
ftr1 = texec.submit('world') { |subject| 'hello ' + subject.to_s }
|
|
123
|
+
final_string = ftr1.result
|
|
124
|
+
|
|
125
|
+
begin
|
|
126
|
+
ftr1 = texec.submit_concurrent('world') { |subject| 'hello ' + subject.to_s }
|
|
127
|
+
rescue AsyncFutures::NoConcurrencyError
|
|
128
|
+
# This will not trigger by default
|
|
129
|
+
# but may trigger based on some initialize arguments.
|
|
130
|
+
# See docs for details.
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
subjects = ['Alice', 'Bob', 'Charlie']
|
|
134
|
+
final_strings = texec.map(subjects) { |subject| 'hello ' + subject.to_s }.to_a
|
|
135
|
+
|
|
136
|
+
# `FiberExecutor`:
|
|
137
|
+
Fiber.set_scheduler(some_scheduler)
|
|
138
|
+
fexec = AsyncFutures::FiberExecutor.new
|
|
139
|
+
|
|
140
|
+
# It isn't necessary to submit within a scheduled fiber
|
|
141
|
+
# but if we want to join in the same thread
|
|
142
|
+
# we need to run within the scheduler.
|
|
143
|
+
# However, cross thread joining is safe.
|
|
144
|
+
Fiber.schedule do
|
|
145
|
+
ftr1 = fexec.submit('world') { |subject| 'hello ' + subject.to_s }
|
|
146
|
+
final_string = ftr1.result
|
|
147
|
+
|
|
148
|
+
begin
|
|
149
|
+
ftr1 = fexec.submit_concurrent('world') { |subject| 'hello ' + subject.to_s }
|
|
150
|
+
rescue AsyncFutures::NoConcurrencyError
|
|
151
|
+
# This will trigger by default
|
|
152
|
+
# but can be suppressed by an initialize argument.
|
|
153
|
+
# See docs for details.
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
subjects = ['Alice', 'Bob', 'Charlie']
|
|
157
|
+
final_strings = fexec.map(subjects) { |subject| 'hello ' + subject.to_s }.to_a
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Unsetting the scheduler causes it to run to completion.
|
|
161
|
+
# Otherwise, we may need to wait until process exit
|
|
162
|
+
# for it to fully finish running.
|
|
163
|
+
Fiber.set_scheduler(nil)
|
|
164
|
+
|
|
165
|
+
# `RactorExecutor`:
|
|
166
|
+
|
|
167
|
+
# This must be explicitly required
|
|
168
|
+
# since it doesn't run on anything prior to Ruby version 4.x
|
|
169
|
+
# (the API changed a lot between versions so it only targets 4.x for now).
|
|
170
|
+
# Also, the Java based VMs (JRuby and TruffleRuby)
|
|
171
|
+
# don't support Ractor primitives *at all* yet.
|
|
172
|
+
require 'async_futures/ractor_executor'
|
|
173
|
+
|
|
174
|
+
rexec = AsyncFutures::RactorExecutor.new
|
|
175
|
+
ftr1 = rexec.submit('world') { |subject| 'hello ' + subject.to_s }
|
|
176
|
+
final_string = ftr1.result
|
|
177
|
+
|
|
178
|
+
begin
|
|
179
|
+
ftr1 = rexec.submit_concurrent('world') { |subject| 'hello ' + subject.to_s }
|
|
180
|
+
rescue AsyncFutures::NoConcurrencyError
|
|
181
|
+
# This will not currently trigger.
|
|
182
|
+
# This may change in a future version to be more like ThreadExecutor.
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
subjects = ['Alice', 'Bob', 'Charlie']
|
|
186
|
+
final_strings = rexec.map(subjects) { |subject| 'hello ' + subject.to_s }.to_a
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
|
192
|
+
Then, run `rake test` to run the tests.
|
|
193
|
+
You can also run `bin/console` for an interactive prompt
|
|
194
|
+
that will allow you to experiment.
|
|
195
|
+
|
|
196
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
197
|
+
To release a new version, update the version number in `version.rb`,
|
|
198
|
+
and then run `bundle exec rake release`,
|
|
199
|
+
which will create a git tag for the version,
|
|
200
|
+
push git commits and the created tag,
|
|
201
|
+
and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
202
|
+
|
|
203
|
+
### Benchmarks
|
|
204
|
+
|
|
205
|
+
There is a simple benchmarking script in this repo at `bin/benchmark`.
|
|
206
|
+
It takes no command line arguments.
|
|
207
|
+
Just read it,
|
|
208
|
+
then run it
|
|
209
|
+
to see the difference in executor performance
|
|
210
|
+
on different types of workloads.
|
|
211
|
+
|
|
212
|
+
### Documentation
|
|
213
|
+
|
|
214
|
+
The documentation in this repo uses [Semantic Line breaks](https://sembr.org/).
|
|
215
|
+
If you contribute documentation changes, please follow the same convention.
|
|
216
|
+
|
|
217
|
+
## Contributing
|
|
218
|
+
|
|
219
|
+
Bug reports and pull requests are welcome on Codeberg at
|
|
220
|
+
<https://codeberg.org/eestrada/async_futures>.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'minitest/test_task'
|
|
5
|
+
require 'rubocop/rake_task'
|
|
6
|
+
require 'rdoc/task'
|
|
7
|
+
|
|
8
|
+
Minitest::TestTask.create
|
|
9
|
+
|
|
10
|
+
# https://github.com/simplecov-ruby/simplecov/issues/1032#issuecomment-2087973750
|
|
11
|
+
Minitest::TestTask.create(:coverage) do |t|
|
|
12
|
+
# simplecov can be configured inside the `test/minitest_helper.rb` file, but it
|
|
13
|
+
# needs to be required before minitest, otherwise it's at_exit hook won't be
|
|
14
|
+
# registered in the correct order, which will cause coverage to be missed.
|
|
15
|
+
#
|
|
16
|
+
# Run `rake test:cmd` and `rake coverage:cmd` to see the difference.
|
|
17
|
+
t.test_prelude = 'require "simplecov"'
|
|
18
|
+
t.verbose = true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
CLEAN << 'coverage'
|
|
22
|
+
|
|
23
|
+
RuboCop::RakeTask.new
|
|
24
|
+
|
|
25
|
+
namespace :rbs do
|
|
26
|
+
desc 'Generate prototypes for all library files'
|
|
27
|
+
task :prototype do
|
|
28
|
+
Dir.chdir('lib') do
|
|
29
|
+
Dir.glob('**/*.rb').each do |e|
|
|
30
|
+
FileUtils.mkdir_p("../sig/#{File.dirname(e)}")
|
|
31
|
+
sh "bundle exec rbs prototype rb '#{e}' > '../sig/#{e}s'"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc 'Validate RBS files'
|
|
37
|
+
task :validate do
|
|
38
|
+
sh 'bundle', 'exec', 'rbs', 'validate'
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# https://docs.ruby-lang.org/en/3.4/RDoc/Task.html
|
|
43
|
+
RDoc::Task.new do |rdoc|
|
|
44
|
+
rdoc.markup = 'markdown'
|
|
45
|
+
rdoc.main = 'README.md'
|
|
46
|
+
rdoc.rdoc_files.include('README.md', 'lib/**/*.rb')
|
|
47
|
+
|
|
48
|
+
# `docs` directory is what GitHub currently supports as pages source.
|
|
49
|
+
rdoc.rdoc_dir = 'docs'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
task default: %i[rubocop coverage]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsyncFutures
|
|
4
|
+
# Base error class.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Error that signals that concurrency is not available for the requested operation.
|
|
8
|
+
# This may be permanent or temporary.
|
|
9
|
+
class NoConcurrencyError < Error; end
|
|
10
|
+
|
|
11
|
+
# Error raised for all invalid Future states.
|
|
12
|
+
class InvalidStateError < Error
|
|
13
|
+
attr_reader :future, :state
|
|
14
|
+
|
|
15
|
+
def initialize(future, state)
|
|
16
|
+
@future = future
|
|
17
|
+
@state = state
|
|
18
|
+
|
|
19
|
+
super("Unexpected state '#{@state}' for Future: #{@future}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Error that is raised for invalid operations on a cancelled Future.
|
|
24
|
+
class CancelledError < Error; end
|
|
25
|
+
|
|
26
|
+
# Error for Executor errors related to Ractors.
|
|
27
|
+
class RactorError < Error; end
|
|
28
|
+
|
|
29
|
+
# Error for Future errors related to deadlocks.
|
|
30
|
+
class DeadlockError < Error
|
|
31
|
+
attr_reader :future
|
|
32
|
+
|
|
33
|
+
def initialize(future)
|
|
34
|
+
@future = future
|
|
35
|
+
|
|
36
|
+
super("Future would deadlock: #{@future}")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'error'
|
|
4
|
+
require_relative 'future'
|
|
5
|
+
|
|
6
|
+
require 'timeout'
|
|
7
|
+
|
|
8
|
+
module AsyncFutures
|
|
9
|
+
# `Executor` mixin module.
|
|
10
|
+
# Has a simple implementation
|
|
11
|
+
# that just runs submitted blocks immediately
|
|
12
|
+
# and returns a completed `Future`.
|
|
13
|
+
# Can be used standalone as a stateless `Executor`
|
|
14
|
+
# that runs submitted blocks immediately.
|
|
15
|
+
#
|
|
16
|
+
# Classes using this mixin should override the `submit` method.
|
|
17
|
+
#
|
|
18
|
+
# `shutdown` should be overridden if there is cleanup to be performed.
|
|
19
|
+
#
|
|
20
|
+
# If an implementation wants to signal that it supports true concurrency,
|
|
21
|
+
# it should override the `submit_concurrent` method;
|
|
22
|
+
# this can be as simple as aliasing it
|
|
23
|
+
# to the previously overridden `submit` method.
|
|
24
|
+
#
|
|
25
|
+
# The `map` method should *never* be overridden.
|
|
26
|
+
# This is already logically correct
|
|
27
|
+
# and should work with any `Executor` implementation.
|
|
28
|
+
module Executor
|
|
29
|
+
# Schedules the block
|
|
30
|
+
# to be executed as `block.call(*args, **kwargs)`
|
|
31
|
+
# and returns a `Future` object representing the execution of the block.
|
|
32
|
+
#
|
|
33
|
+
# Some Executor implementations may,
|
|
34
|
+
# under some or all circumstances,
|
|
35
|
+
# run the given block immediately and synchronously
|
|
36
|
+
# and return an already completed `Future` object.
|
|
37
|
+
def submit(...)
|
|
38
|
+
Future.new.tap { |future| future.complete(...) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Schedules the block, to be executed as `block.call(*args, **kwargs)` and
|
|
42
|
+
# returns a `Future` object representing the execution of the block.
|
|
43
|
+
#
|
|
44
|
+
# Executor must support concurrency otherwise this method will raise the
|
|
45
|
+
# exception `NoConcurrencyError`.
|
|
46
|
+
#
|
|
47
|
+
# This method should *never* run the block to completion before returning.
|
|
48
|
+
# This could cause a serious deadlock condition that cannot be overcome.
|
|
49
|
+
# If an implementation cannot schedule this to run concurrently
|
|
50
|
+
# it is better for it to raise an exception such as `NoConcurrencyError`.
|
|
51
|
+
# This at least allows the caller an opportunity to recover
|
|
52
|
+
# instead of potentially deadlocking.
|
|
53
|
+
def submit_concurrent(...)
|
|
54
|
+
raise NoConcurrencyError unless support_concurrency?
|
|
55
|
+
|
|
56
|
+
submit(...)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Return whether the current `Executor` implementation supports concurrency.
|
|
60
|
+
def support_concurrency?
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Similar to `enumerable.map(&block)` except:
|
|
65
|
+
#
|
|
66
|
+
# - `block` is executed asynchronously
|
|
67
|
+
# - several calls to block may be made concurrently
|
|
68
|
+
# - Instead of an `Array`, an `Enumerator::Lazy` is returned
|
|
69
|
+
#
|
|
70
|
+
# Just like `enumerable.map`,
|
|
71
|
+
# args are splatted for the block if there are multiple args.
|
|
72
|
+
# Thus you can do things like this:
|
|
73
|
+
#
|
|
74
|
+
# ```ruby
|
|
75
|
+
# ThreadExecutor.new.map(enum.each_with_index) do |e, i|
|
|
76
|
+
# [e, i]
|
|
77
|
+
# end
|
|
78
|
+
# ```
|
|
79
|
+
#
|
|
80
|
+
# `Future` instances are joined
|
|
81
|
+
# as the returned `Enumerator::Lazy` is enumerated over
|
|
82
|
+
# via a terminal method like `force`, `to_a`, or `each`.
|
|
83
|
+
# The `Future.result` values,
|
|
84
|
+
# and not the `Future` instances themselves,
|
|
85
|
+
# are what is returned.
|
|
86
|
+
#
|
|
87
|
+
# If a `block` call raises an exception,
|
|
88
|
+
# then that exception will be raised
|
|
89
|
+
# when its value is retrieved when enumerating
|
|
90
|
+
# over the `Enumerator::Lazy` instance.
|
|
91
|
+
# Any remaining `Future` instances will attempt to be cancelled
|
|
92
|
+
# in the case of a raised exception.
|
|
93
|
+
# However, because of possible concurrent execution
|
|
94
|
+
# there is no guarantee that they will be cancelled
|
|
95
|
+
# before being picked up, run, and completed.
|
|
96
|
+
#
|
|
97
|
+
# If `timeout` is given and not `nil`,
|
|
98
|
+
# then execution will raise `Timeout::Error`
|
|
99
|
+
# if more than `timeout` seconds elapses.
|
|
100
|
+
# The elapsed time includes both the initial submission of tasks
|
|
101
|
+
# *and* the enumeration of `Future` results from the returned `Enumerator::Lazy`.
|
|
102
|
+
#
|
|
103
|
+
# Keep in mind that an `Enumerator::Lazy` can be enumerated over more than once
|
|
104
|
+
# *and* that the `timeout` value will be evaluated each time it is enumerated
|
|
105
|
+
# *and* that the timeout value will be calculated from the time of first submission.
|
|
106
|
+
# Thus, enumeration could succeed on the first enumeration,
|
|
107
|
+
# but fail with a `Timeout::Error` on a subsequent enumeration.
|
|
108
|
+
# To avoid timing out on an enumeration after the first enumeration,
|
|
109
|
+
# you should save the result of the first enumeration in an `Array` (or similar)
|
|
110
|
+
# using something like `to_a` on the returned `Enumerator::Lazy` instance.
|
|
111
|
+
# If you immediately enumerate the returned `Enumerator::Lazy` only once
|
|
112
|
+
# or you have passed no `timeout` value,
|
|
113
|
+
# then none of this is a concern.
|
|
114
|
+
#
|
|
115
|
+
# Negative `timeout` values are allowed,
|
|
116
|
+
# but they just raise `Timeout::Error` immediately.
|
|
117
|
+
#
|
|
118
|
+
# Do ***not*** call this method with an infinite `Enumerable`
|
|
119
|
+
# and no `timeout` value:
|
|
120
|
+
# the first thing this method does is force it into a finite collection of futures.
|
|
121
|
+
# An infinite `Enumerable` forced into a finite collection
|
|
122
|
+
# will run forever and eventually eat up all memory.
|
|
123
|
+
def map(enumerable, timeout = nil, &block) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
124
|
+
timeout = nil if timeout&.zero?
|
|
125
|
+
|
|
126
|
+
clock_timeout = Time.now.to_f + timeout if timeout
|
|
127
|
+
|
|
128
|
+
futures = []
|
|
129
|
+
begin
|
|
130
|
+
local_timeout = timeout && (clock_timeout - Time.now.to_f)
|
|
131
|
+
raise Timeout::Error unless timeout.nil? || local_timeout.positive?
|
|
132
|
+
|
|
133
|
+
Timeout.timeout(local_timeout) do
|
|
134
|
+
enumerable.each { |*args| futures << submit(*args, &block) }
|
|
135
|
+
end
|
|
136
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
137
|
+
futures.each(&:cancel)
|
|
138
|
+
raise e
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
futures.each_with_index.lazy.map do |future, index|
|
|
142
|
+
local_timeout = timeout && (clock_timeout - Time.now.to_f)
|
|
143
|
+
raise Timeout::Error unless timeout.nil? || local_timeout.positive?
|
|
144
|
+
|
|
145
|
+
Timeout.timeout(local_timeout) { future.result }
|
|
146
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
147
|
+
# If *any* future raises an exception,
|
|
148
|
+
# we need to be sure to cancel the remaining ones.
|
|
149
|
+
# It's ok if we call cancel on already completed ones.
|
|
150
|
+
(index...futures.size).each { |i| futures[i].cancel }
|
|
151
|
+
raise e
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Signal the executor that it should free any resources that it is using
|
|
156
|
+
# when the currently pending futures are done executing. Calls to
|
|
157
|
+
# Executor.submit() and Executor.map() made after shutdown will raise
|
|
158
|
+
# RuntimeError.
|
|
159
|
+
#
|
|
160
|
+
# If `wait` is `true` then this method will not return until all the pending
|
|
161
|
+
# futures are done executing and the resources associated with the executor
|
|
162
|
+
# have been freed. If `wait` is `False` then this method will return immediately
|
|
163
|
+
# and the resources associated with the executor will be freed when all
|
|
164
|
+
# pending futures are done executing. Regardless of the value of `wait`, the
|
|
165
|
+
# entire Ruby program will not exit until all pending futures are done
|
|
166
|
+
# executing.
|
|
167
|
+
#
|
|
168
|
+
# If `cancel_futures` is `true`, this method will cancel all pending futures
|
|
169
|
+
# that the executor has not started running. Any futures that are completed
|
|
170
|
+
# or running won't be cancelled, regardless of the value of `cancel_futures`.
|
|
171
|
+
#
|
|
172
|
+
# If both `cancel_futures` and `wait` are `true`, all futures that the executor
|
|
173
|
+
# has started running will be completed prior to this method returning. The
|
|
174
|
+
# remaining futures are cancelled.
|
|
175
|
+
#
|
|
176
|
+
# You can ensure this gets called under all circumstances
|
|
177
|
+
# by calling this method with a block.
|
|
178
|
+
# The block will be called
|
|
179
|
+
# and then any shutdown cleanup logic will be run
|
|
180
|
+
# after the block completes.
|
|
181
|
+
# The block will be passed one parameter: the executor instance.
|
|
182
|
+
#
|
|
183
|
+
# ```ruby
|
|
184
|
+
# ThreadExecutor.new(max_workers: 4).shutdown do |executor|
|
|
185
|
+
# executor.submit('src1.txt', 'dest1.txt', &FileUtils.method(:cp))
|
|
186
|
+
# executor.submit('src2.txt', 'dest2.txt', &FileUtils.method(:cp))
|
|
187
|
+
# executor.submit('src3.txt', 'dest3.txt', &FileUtils.method(:cp))
|
|
188
|
+
# executor.submit('src4.txt', 'dest4.txt', &FileUtils.method(:cp))
|
|
189
|
+
# end
|
|
190
|
+
# ```
|
|
191
|
+
#
|
|
192
|
+
# `shutdown` can be called multiple times.
|
|
193
|
+
# The block given will always be run,
|
|
194
|
+
# but the actual procedure to shutdown afterward will only be called once,
|
|
195
|
+
# on the first time.
|
|
196
|
+
#
|
|
197
|
+
# It is the caller's responsibility
|
|
198
|
+
# to ensure that the passed block can deal with a shutdown executor
|
|
199
|
+
# if there is any possibility
|
|
200
|
+
# of `shutdown` being called more than once with a block.
|
|
201
|
+
# Unless the caller is doing something very out of the ordinary,
|
|
202
|
+
# this is unlikely to be an issue.
|
|
203
|
+
#
|
|
204
|
+
# This method returns the return value of the block,
|
|
205
|
+
# or `nil` if no block is given.
|
|
206
|
+
def shutdown(wait: true, cancel_futures: false, &block) # rubocop:disable Lint/UnusedMethodArgument
|
|
207
|
+
block&.call(self)
|
|
208
|
+
ensure # rubocop:disable Lint/EmptyEnsure
|
|
209
|
+
# Cleanup logic goes here.
|
|
210
|
+
#
|
|
211
|
+
# The mixin has no state,
|
|
212
|
+
# so it has nothing to cleanup.
|
|
213
|
+
#
|
|
214
|
+
# Also, this is the only implementation that will *not* raise
|
|
215
|
+
# an exception when new tasks are submitted after shutdown,
|
|
216
|
+
# precisely because it has no state
|
|
217
|
+
# to even keep track of whether shutdown has previously been called or not.
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
module_function :submit, :submit_concurrent, :support_concurrency?, :map, :shutdown
|
|
221
|
+
|
|
222
|
+
public :submit, :submit_concurrent, :support_concurrency?, :map, :shutdown
|
|
223
|
+
end
|
|
224
|
+
end
|