async 1.25.2 → 1.28.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/lib/async/barrier.rb +1 -1
  3. data/lib/async/clock.rb +33 -1
  4. data/lib/async/logger.rb +1 -6
  5. data/lib/async/node.rb +20 -2
  6. data/lib/async/queue.rb +5 -1
  7. data/lib/async/reactor.rb +73 -12
  8. data/lib/async/scheduler.rb +112 -0
  9. data/lib/async/task.rb +11 -3
  10. data/lib/async/version.rb +1 -1
  11. metadata +46 -104
  12. data/.editorconfig +0 -6
  13. data/.github/workflows/development.yml +0 -55
  14. data/.gitignore +0 -14
  15. data/.rspec +0 -3
  16. data/.yardopts +0 -1
  17. data/Gemfile +0 -20
  18. data/Guardfile +0 -14
  19. data/README.md +0 -385
  20. data/Rakefile +0 -40
  21. data/async.gemspec +0 -34
  22. data/bake.rb +0 -33
  23. data/benchmark/async_vs_lightio.rb +0 -84
  24. data/benchmark/fiber_count.rb +0 -10
  25. data/benchmark/rubies/README.md +0 -51
  26. data/benchmark/rubies/benchmark.rb +0 -220
  27. data/benchmark/thread_count.rb +0 -9
  28. data/benchmark/thread_vs_fiber.rb +0 -45
  29. data/examples/async_method.rb +0 -60
  30. data/examples/callback/loop.rb +0 -44
  31. data/examples/capture/README.md +0 -59
  32. data/examples/capture/capture.rb +0 -116
  33. data/examples/fibers.rb +0 -178
  34. data/examples/queue/producer.rb +0 -28
  35. data/examples/sleep_sort.rb +0 -40
  36. data/examples/stop/condition.rb +0 -31
  37. data/examples/stop/sleep.rb +0 -42
  38. data/gems/event.gemfile +0 -4
  39. data/logo.png +0 -0
  40. data/logo.svg +0 -64
  41. data/papers/1982 Grossman.pdf +0 -0
  42. data/papers/1987 ODell.pdf +0 -0
  43. data/spec/async/barrier_spec.rb +0 -116
  44. data/spec/async/chainable_async_examples.rb +0 -13
  45. data/spec/async/clock_spec.rb +0 -37
  46. data/spec/async/condition_examples.rb +0 -105
  47. data/spec/async/condition_spec.rb +0 -72
  48. data/spec/async/logger_spec.rb +0 -65
  49. data/spec/async/node_spec.rb +0 -193
  50. data/spec/async/notification_spec.rb +0 -66
  51. data/spec/async/performance_spec.rb +0 -72
  52. data/spec/async/queue_spec.rb +0 -129
  53. data/spec/async/reactor/nested_spec.rb +0 -52
  54. data/spec/async/reactor_spec.rb +0 -253
  55. data/spec/async/semaphore_spec.rb +0 -169
  56. data/spec/async/task_spec.rb +0 -476
  57. data/spec/async/wrapper_spec.rb +0 -203
  58. data/spec/async_spec.rb +0 -33
  59. data/spec/enumerator_spec.rb +0 -83
  60. data/spec/kernel/async_spec.rb +0 -33
  61. data/spec/kernel/sync_spec.rb +0 -54
  62. data/spec/spec_helper.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81b7571d3f6ac5ce2ade98ca74b0440581178c8cb74503553d5a6dc70956a6fb
4
- data.tar.gz: f5aeea0f0a482f52e98c5064bd9678aed1f9b8e4868ad2d5bf7a79c009f53275
3
+ metadata.gz: e7dacbad60b89b492fe3741aa5f61240c8bc1f2d9f23e242c7eadd80cc7d9d38
4
+ data.tar.gz: a8193d5ec289670c5ca4a795e905ffd660aa57edaac8796e58c3db8b866fa340
5
5
  SHA512:
6
- metadata.gz: 9a513e4b8f4efb35cd0438801c2e0dc6896b9f11f911e8cc547a15c22043d28adeb257d47de2d326513095cf813ab88fd9c1aec8c7c7daffef85a8c7238da7f9
7
- data.tar.gz: 84e46d9e07977de42c79297f0c1decf62957497ed6688bdc65b687fee21b26127c7e553c64d09d5e4dcf405dcc46d468b22e689bfb6e69dfac4c541b9608459f
6
+ metadata.gz: de1a9942175a329082d388b280d63c80f614a99145d0df8b6b39e7a4b31d2cb7af4a91bad643f408eeebe6b72807d3f50093528aa5bac273daef67cbb25b3b97
7
+ data.tar.gz: 206be83fe80c466b12669ad847b041e60f27c21d32f82995e7350385f50d990abe94cfbd701a86717532936d7b6ad1e495df78f45a3c0972e2c70b6647824528
@@ -23,7 +23,7 @@
23
23
  require_relative 'task'
24
24
 
25
25
  module Async
26
- # A semaphore is used to control access to a common resource in a concurrent system. A useful way to think of a semaphore as used in the real-world systems is as a record of how many units of a particular resource are available, coupled with operations to adjust that record safely (i.e. to avoid race conditions) as units are required or become free, and, if necessary, wait until a unit of the resource becomes available.
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 = []
@@ -21,7 +21,7 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  module Async
24
- module Clock
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
@@ -24,10 +24,5 @@ require 'console'
24
24
  require_relative 'task'
25
25
 
26
26
  module Async
27
- # @return the current logger, either the active tasks logger, or the global event console logger.
28
- def self.logger
29
- if task = Task.current?
30
- task.logger
31
- end || Console.logger
32
- end
27
+ extend Console
33
28
  end
@@ -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
- out.puts "#{"\t" * level}#{node}"
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
@@ -34,7 +34,11 @@ module Async
34
34
 
35
35
  attr :items
36
36
 
37
- def enqueue item
37
+ def empty?
38
+ @items.empty?
39
+ end
40
+
41
+ def enqueue(item)
38
42
  @items.push(item)
39
43
 
40
44
  self.signal unless self.empty?
@@ -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(**options)
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 ||= Console.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.debug(self) {"Selecting with #{@children&.size} children with interval = #{interval ? interval.round(2) : 'infinite'}..."}
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
- initial_task = self.async(*arguments, &block) if block_given?
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 = self.after(duration) do
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 = self.after(timeout) do
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
@@ -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 ||= @parent&.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
- def_delegators :@reactor, :with_timeout, :timeout, :sleep
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 `Async::Task` behave more like a promise, and you would need to ensure that someone calls `Task#result` otherwise you might miss important errors.
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