async 1.25.2 → 1.28.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/async/barrier.rb +1 -1
- data/lib/async/clock.rb +33 -1
- data/lib/async/logger.rb +1 -6
- data/lib/async/node.rb +20 -2
- data/lib/async/queue.rb +5 -1
- data/lib/async/reactor.rb +73 -12
- data/lib/async/scheduler.rb +112 -0
- data/lib/async/task.rb +11 -3
- data/lib/async/version.rb +1 -1
- metadata +46 -104
- data/.editorconfig +0 -6
- data/.github/workflows/development.yml +0 -55
- data/.gitignore +0 -14
- data/.rspec +0 -3
- data/.yardopts +0 -1
- data/Gemfile +0 -20
- data/Guardfile +0 -14
- data/README.md +0 -385
- data/Rakefile +0 -40
- data/async.gemspec +0 -34
- data/bake.rb +0 -33
- data/benchmark/async_vs_lightio.rb +0 -84
- data/benchmark/fiber_count.rb +0 -10
- data/benchmark/rubies/README.md +0 -51
- data/benchmark/rubies/benchmark.rb +0 -220
- data/benchmark/thread_count.rb +0 -9
- data/benchmark/thread_vs_fiber.rb +0 -45
- data/examples/async_method.rb +0 -60
- data/examples/callback/loop.rb +0 -44
- data/examples/capture/README.md +0 -59
- data/examples/capture/capture.rb +0 -116
- data/examples/fibers.rb +0 -178
- data/examples/queue/producer.rb +0 -28
- data/examples/sleep_sort.rb +0 -40
- data/examples/stop/condition.rb +0 -31
- data/examples/stop/sleep.rb +0 -42
- data/gems/event.gemfile +0 -4
- data/logo.png +0 -0
- data/logo.svg +0 -64
- data/papers/1982 Grossman.pdf +0 -0
- data/papers/1987 ODell.pdf +0 -0
- data/spec/async/barrier_spec.rb +0 -116
- data/spec/async/chainable_async_examples.rb +0 -13
- data/spec/async/clock_spec.rb +0 -37
- data/spec/async/condition_examples.rb +0 -105
- data/spec/async/condition_spec.rb +0 -72
- data/spec/async/logger_spec.rb +0 -65
- data/spec/async/node_spec.rb +0 -193
- data/spec/async/notification_spec.rb +0 -66
- data/spec/async/performance_spec.rb +0 -72
- data/spec/async/queue_spec.rb +0 -129
- data/spec/async/reactor/nested_spec.rb +0 -52
- data/spec/async/reactor_spec.rb +0 -253
- data/spec/async/semaphore_spec.rb +0 -169
- data/spec/async/task_spec.rb +0 -476
- data/spec/async/wrapper_spec.rb +0 -203
- data/spec/async_spec.rb +0 -33
- data/spec/enumerator_spec.rb +0 -83
- data/spec/kernel/async_spec.rb +0 -33
- data/spec/kernel/sync_spec.rb +0 -54
- data/spec/spec_helper.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e7dacbad60b89b492fe3741aa5f61240c8bc1f2d9f23e242c7eadd80cc7d9d38
|
4
|
+
data.tar.gz: a8193d5ec289670c5ca4a795e905ffd660aa57edaac8796e58c3db8b866fa340
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: de1a9942175a329082d388b280d63c80f614a99145d0df8b6b39e7a4b31d2cb7af4a91bad643f408eeebe6b72807d3f50093528aa5bac273daef67cbb25b3b97
|
7
|
+
data.tar.gz: 206be83fe80c466b12669ad847b041e60f27c21d32f82995e7350385f50d990abe94cfbd701a86717532936d7b6ad1e495df78f45a3c0972e2c70b6647824528
|
data/lib/async/barrier.rb
CHANGED
@@ -23,7 +23,7 @@
|
|
23
23
|
require_relative 'task'
|
24
24
|
|
25
25
|
module Async
|
26
|
-
# A
|
26
|
+
# A barrier is used to synchronize multiple tasks, waiting for them all to complete before continuing.
|
27
27
|
class Barrier
|
28
28
|
def initialize(parent: nil)
|
29
29
|
@tasks = []
|
data/lib/async/clock.rb
CHANGED
@@ -21,7 +21,7 @@
|
|
21
21
|
# THE SOFTWARE.
|
22
22
|
|
23
23
|
module Async
|
24
|
-
|
24
|
+
class Clock
|
25
25
|
# Get the current elapsed monotonic time.
|
26
26
|
def self.now
|
27
27
|
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
@@ -35,5 +35,37 @@ module Async
|
|
35
35
|
|
36
36
|
return self.now - start_time
|
37
37
|
end
|
38
|
+
|
39
|
+
def self.start
|
40
|
+
self.new.tap(&:start!)
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(total = 0)
|
44
|
+
@total = total
|
45
|
+
@started = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def start!
|
49
|
+
@started ||= Clock.now
|
50
|
+
end
|
51
|
+
|
52
|
+
def stop!
|
53
|
+
if @started
|
54
|
+
@total += (Clock.now - @started)
|
55
|
+
@started = nil
|
56
|
+
end
|
57
|
+
|
58
|
+
return @total
|
59
|
+
end
|
60
|
+
|
61
|
+
def total
|
62
|
+
total = @total
|
63
|
+
|
64
|
+
if @started
|
65
|
+
total += (Clock.now - @started)
|
66
|
+
end
|
67
|
+
|
68
|
+
return total
|
69
|
+
end
|
38
70
|
end
|
39
71
|
end
|
data/lib/async/logger.rb
CHANGED
@@ -24,10 +24,5 @@ require 'console'
|
|
24
24
|
require_relative 'task'
|
25
25
|
|
26
26
|
module Async
|
27
|
-
|
28
|
-
def self.logger
|
29
|
-
if task = Task.current?
|
30
|
-
task.logger
|
31
|
-
end || Console.logger
|
32
|
-
end
|
27
|
+
extend Console
|
33
28
|
end
|
data/lib/async/node.rb
CHANGED
@@ -217,6 +217,10 @@ module Async
|
|
217
217
|
end
|
218
218
|
end
|
219
219
|
|
220
|
+
def backtrace(*arguments)
|
221
|
+
nil
|
222
|
+
end
|
223
|
+
|
220
224
|
def to_s
|
221
225
|
"\#<#{description}>"
|
222
226
|
end
|
@@ -297,9 +301,23 @@ module Async
|
|
297
301
|
@children&.each(&:stop)
|
298
302
|
end
|
299
303
|
|
300
|
-
def print_hierarchy(out = $stdout)
|
304
|
+
def print_hierarchy(out = $stdout, backtrace: true)
|
301
305
|
self.traverse do |node, level|
|
302
|
-
|
306
|
+
indent = "\t" * level
|
307
|
+
|
308
|
+
out.puts "#{indent}#{node}"
|
309
|
+
|
310
|
+
print_backtrace(out, indent, node) if backtrace
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
private
|
315
|
+
|
316
|
+
def print_backtrace(out, indent, node)
|
317
|
+
if backtrace = node.backtrace
|
318
|
+
backtrace.each_with_index do |line, index|
|
319
|
+
out.puts "#{indent}#{index.zero? ? "→ " : " "}#{line}"
|
320
|
+
end
|
303
321
|
end
|
304
322
|
end
|
305
323
|
end
|
data/lib/async/queue.rb
CHANGED
data/lib/async/reactor.rb
CHANGED
@@ -23,6 +23,7 @@
|
|
23
23
|
require_relative 'logger'
|
24
24
|
require_relative 'task'
|
25
25
|
require_relative 'wrapper'
|
26
|
+
require_relative 'scheduler'
|
26
27
|
|
27
28
|
require 'nio'
|
28
29
|
require 'timers'
|
@@ -50,10 +51,10 @@ module Async
|
|
50
51
|
|
51
52
|
return reactor.async(*arguments, **options, &block)
|
52
53
|
else
|
53
|
-
reactor = self.new
|
54
|
+
reactor = self.new
|
54
55
|
|
55
56
|
begin
|
56
|
-
return reactor.run(*arguments, &block)
|
57
|
+
return reactor.run(*arguments, **options, &block)
|
57
58
|
ensure
|
58
59
|
reactor.close
|
59
60
|
end
|
@@ -82,12 +83,60 @@ module Async
|
|
82
83
|
@ready = []
|
83
84
|
@running = []
|
84
85
|
|
86
|
+
if Scheduler.supported?
|
87
|
+
@scheduler = Scheduler.new(self)
|
88
|
+
else
|
89
|
+
@scheduler = nil
|
90
|
+
end
|
91
|
+
|
85
92
|
@interrupted = false
|
86
93
|
@guard = Mutex.new
|
94
|
+
@blocked = 0
|
95
|
+
@unblocked = []
|
96
|
+
end
|
97
|
+
|
98
|
+
attr :scheduler
|
99
|
+
|
100
|
+
# @reentrant Not thread safe.
|
101
|
+
def block(blocker, timeout)
|
102
|
+
fiber = Fiber.current
|
103
|
+
|
104
|
+
if timeout
|
105
|
+
timer = self.after(timeout) do
|
106
|
+
if fiber.alive?
|
107
|
+
fiber.resume(false)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
begin
|
113
|
+
@blocked += 1
|
114
|
+
Fiber.yield
|
115
|
+
ensure
|
116
|
+
@blocked -= 1
|
117
|
+
end
|
118
|
+
ensure
|
119
|
+
timer&.cancel
|
120
|
+
end
|
121
|
+
|
122
|
+
# @reentrant Thread safe.
|
123
|
+
def unblock(blocker, fiber)
|
124
|
+
@guard.synchronize do
|
125
|
+
@unblocked << fiber
|
126
|
+
@selector.wakeup
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def fiber(&block)
|
131
|
+
if @scheduler
|
132
|
+
Fiber.new(blocking: false, &block)
|
133
|
+
else
|
134
|
+
Fiber.new(&block)
|
135
|
+
end
|
87
136
|
end
|
88
137
|
|
89
138
|
def logger
|
90
|
-
@logger
|
139
|
+
@logger || Console.logger
|
91
140
|
end
|
92
141
|
|
93
142
|
def to_s
|
@@ -98,9 +147,6 @@ module Async
|
|
98
147
|
@children.nil?
|
99
148
|
end
|
100
149
|
|
101
|
-
# TODO Remove these in next major release. They are too confusing to use correctly.
|
102
|
-
def_delegators :@timers, :every, :after
|
103
|
-
|
104
150
|
# Start an asynchronous task within the specified reactor. The task will be
|
105
151
|
# executed until the first blocking call, at which point it will yield and
|
106
152
|
# and this method will return.
|
@@ -157,7 +203,7 @@ module Async
|
|
157
203
|
|
158
204
|
def finished?
|
159
205
|
# TODO I'm not sure if checking `@running.empty?` is really required.
|
160
|
-
super && @ready.empty? && @running.empty?
|
206
|
+
super && @ready.empty? && @running.empty? && @blocked.zero?
|
161
207
|
end
|
162
208
|
|
163
209
|
# Run one iteration of the event loop.
|
@@ -177,6 +223,18 @@ module Async
|
|
177
223
|
@running.clear
|
178
224
|
end
|
179
225
|
|
226
|
+
unless @blocked.zero?
|
227
|
+
unblocked = Array.new
|
228
|
+
|
229
|
+
@guard.synchronize do
|
230
|
+
unblocked, @unblocked = @unblocked, unblocked
|
231
|
+
end
|
232
|
+
|
233
|
+
while fiber = unblocked.pop
|
234
|
+
fiber.resume if fiber.alive?
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
180
238
|
if @ready.empty?
|
181
239
|
interval = @timers.wait_interval
|
182
240
|
else
|
@@ -200,7 +258,7 @@ module Async
|
|
200
258
|
interval = timeout
|
201
259
|
end
|
202
260
|
|
203
|
-
# logger.
|
261
|
+
# logger.info(self) {"Selecting with #{@children&.size} children with interval = #{interval ? interval.round(2) : 'infinite'}..."}
|
204
262
|
if monitors = @selector.select(interval)
|
205
263
|
monitors.each do |monitor|
|
206
264
|
monitor.value.resume
|
@@ -223,10 +281,12 @@ module Async
|
|
223
281
|
end
|
224
282
|
|
225
283
|
# Run the reactor until all tasks are finished. Proxies arguments to {#async} immediately before entering the loop, if a block is provided.
|
226
|
-
def run(*arguments, &block)
|
284
|
+
def run(*arguments, **options, &block)
|
227
285
|
raise RuntimeError, 'Reactor has been closed' if @selector.nil?
|
228
286
|
|
229
|
-
|
287
|
+
@scheduler&.set!
|
288
|
+
|
289
|
+
initial_task = self.async(*arguments, **options, &block) if block_given?
|
230
290
|
|
231
291
|
while self.run_once
|
232
292
|
# Round and round we go!
|
@@ -234,6 +294,7 @@ module Async
|
|
234
294
|
|
235
295
|
return initial_task
|
236
296
|
ensure
|
297
|
+
@scheduler&.clear!
|
237
298
|
logger.debug(self) {"Exiting run-loop because #{$! ? $! : 'finished'}."}
|
238
299
|
end
|
239
300
|
|
@@ -266,7 +327,7 @@ module Async
|
|
266
327
|
def sleep(duration)
|
267
328
|
fiber = Fiber.current
|
268
329
|
|
269
|
-
timer =
|
330
|
+
timer = @timers.after(duration) do
|
270
331
|
if fiber.alive?
|
271
332
|
fiber.resume
|
272
333
|
end
|
@@ -283,7 +344,7 @@ module Async
|
|
283
344
|
def with_timeout(timeout, exception = TimeoutError)
|
284
345
|
fiber = Fiber.current
|
285
346
|
|
286
|
-
timer =
|
347
|
+
timer = @timers.after(timeout) do
|
287
348
|
if fiber.alive?
|
288
349
|
error = exception.new("execution expired")
|
289
350
|
fiber.resume error
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'clock'
|
22
|
+
|
23
|
+
module Async
|
24
|
+
class Scheduler
|
25
|
+
if Fiber.respond_to?(:set_scheduler)
|
26
|
+
def self.supported?
|
27
|
+
true
|
28
|
+
end
|
29
|
+
else
|
30
|
+
def self.supported?
|
31
|
+
false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(reactor)
|
36
|
+
@reactor = reactor
|
37
|
+
@wrappers = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def set!
|
41
|
+
@wrappers = {}
|
42
|
+
Fiber.set_scheduler(self)
|
43
|
+
end
|
44
|
+
|
45
|
+
def clear!
|
46
|
+
# Because these instances are created with `autoclose: false`, this does not close the underlying file descriptor:
|
47
|
+
# @ios&.each_value(&:close)
|
48
|
+
|
49
|
+
@wrappers = nil
|
50
|
+
Fiber.set_scheduler(nil)
|
51
|
+
end
|
52
|
+
|
53
|
+
private def from_io(io)
|
54
|
+
@wrappers[io] ||= Wrapper.new(io, @reactor)
|
55
|
+
end
|
56
|
+
|
57
|
+
def io_wait(io, events, timeout = nil)
|
58
|
+
wrapper = from_io(io)
|
59
|
+
|
60
|
+
if events == IO::READABLE
|
61
|
+
if wrapper.wait_readable(timeout)
|
62
|
+
return IO::READABLE
|
63
|
+
end
|
64
|
+
elsif events == IO::WRITABLE
|
65
|
+
if wrapper.wait_writable(timeout)
|
66
|
+
return IO::WRITABLE
|
67
|
+
end
|
68
|
+
else
|
69
|
+
if wrapper.wait_any(timeout)
|
70
|
+
return events
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
return false
|
75
|
+
ensure
|
76
|
+
wrapper.reactor = nil
|
77
|
+
end
|
78
|
+
|
79
|
+
# Wait for the specified process ID to exit.
|
80
|
+
# @parameter pid [Integer] The process ID to wait for.
|
81
|
+
# @parameter flags [Integer] A bit-mask of flags suitable for `Process::Status.wait`.
|
82
|
+
# @returns [Process::Status] A process status instance.
|
83
|
+
def process_wait(pid, flags)
|
84
|
+
Thread.new do
|
85
|
+
Process::Status.wait(pid, flags)
|
86
|
+
end.value
|
87
|
+
end
|
88
|
+
|
89
|
+
def kernel_sleep(duration)
|
90
|
+
@reactor.sleep(duration)
|
91
|
+
end
|
92
|
+
|
93
|
+
def block(blocker, timeout)
|
94
|
+
@reactor.block(blocker, timeout)
|
95
|
+
end
|
96
|
+
|
97
|
+
def unblock(blocker, fiber)
|
98
|
+
@reactor.unblock(blocker, fiber)
|
99
|
+
end
|
100
|
+
|
101
|
+
def close
|
102
|
+
end
|
103
|
+
|
104
|
+
def fiber(&block)
|
105
|
+
task = Task.new(&block)
|
106
|
+
|
107
|
+
task.resume
|
108
|
+
|
109
|
+
return task.fiber
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/async/task.rb
CHANGED
@@ -86,17 +86,24 @@ module Async
|
|
86
86
|
@fiber = make_fiber(&block)
|
87
87
|
end
|
88
88
|
|
89
|
+
if Fiber.current.respond_to?(:backtrace)
|
90
|
+
def backtrace(*arguments)
|
91
|
+
@fiber.backtrace(*arguments)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
89
95
|
def to_s
|
90
96
|
"\#<#{self.description} (#{@status})>"
|
91
97
|
end
|
92
98
|
|
93
99
|
def logger
|
94
|
-
@logger
|
100
|
+
@logger || Console.logger
|
95
101
|
end
|
96
102
|
|
97
103
|
# @attr ios [Reactor] The reactor the task was created within.
|
98
104
|
attr :reactor
|
99
|
-
|
105
|
+
|
106
|
+
def_delegators :@reactor, :with_timeout, :sleep
|
100
107
|
|
101
108
|
# Yield back to the reactor and allow other fibers to execute.
|
102
109
|
def yield
|
@@ -226,7 +233,7 @@ module Async
|
|
226
233
|
private
|
227
234
|
|
228
235
|
# This is a very tricky aspect of tasks to get right. I've modelled it after `Thread` but it's slightly different in that the exception can propagate back up through the reactor. If the user writes code which raises an exception, that exception should always be visible, i.e. cause a failure. If it's not visible, such code fails silently and can be very difficult to debug.
|
229
|
-
# As an explcit choice, the user can start a task which doesn't propagate exceptions. This only applies to `StandardError` and derived tasks. This allows tasks to internally capture their error state which is raised when invoking `Task#result` similar to how `Thread#join` works. This mode makes
|
236
|
+
# As an explcit choice, the user can start a task which doesn't propagate exceptions. This only applies to `StandardError` and derived tasks. This allows tasks to internally capture their error state which is raised when invoking `Task#result` similar to how `Thread#join` works. This mode makes {ruby Async::Task} behave more like a promise, and you would need to ensure that someone calls `Task#result` otherwise you might miss important errors.
|
230
237
|
def fail!(exception = nil, propagate = true)
|
231
238
|
@status = :failed
|
232
239
|
@result = exception
|
@@ -289,6 +296,7 @@ module Async
|
|
289
296
|
def set!
|
290
297
|
# This is actually fiber-local:
|
291
298
|
Thread.current[:async_task] = self
|
299
|
+
Console.logger = @logger if @logger
|
292
300
|
end
|
293
301
|
end
|
294
302
|
end
|