async 1.26.2 → 1.28.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72c4f7456b611f436a18f0d143ba5b4c9bb4c7882ad241f3ba251a139c020945
4
- data.tar.gz: 4270eef8e1de0d08b953eb373a665d3fb8c0b9348e13d99ac6c61e7aead63db5
3
+ metadata.gz: 77c63d6ea5723e686a2b72bd7bac1055aa293df4f4583915124a657cd39e6a74
4
+ data.tar.gz: 3f58014dc1430f1a03bc16a69f19020a8fc3aefb4280e9026fea74fe89118ee1
5
5
  SHA512:
6
- metadata.gz: 359f4bcd33b438eca0518a00b77d336c25ab49ed3346180013bc135743d9a5447c178416680e5289085cbd9d75718772d7a06e25d44a0b0303cd35086da0c8f5
7
- data.tar.gz: 420f280a2ed6d2cb4d363c6a4f80a695a1ad963ead1b040daa16ce9a18ceab9b8f3f6bcee707658a946acba3656f54578df9ed36e2bf8ffc0e6d151cdbbc9e9e
6
+ metadata.gz: ca3d285761dd73820c81a56e3bc55a11780e915652d07f611be3723934345f0204d0949b0b5f3a86480b231cdbf37515404eb38152d1771b564932798cb96e70
7
+ data.tar.gz: fa3cc87e0064c5a63c0f3b3683270c0e1ad9a2db283c131b139bbfd3c337399d8646824377c512b8e2245bc70f6ba213afda6e84391c4caaa4b8764c6587955d
@@ -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,6 +34,10 @@ module Async
34
34
 
35
35
  attr :items
36
36
 
37
+ def size
38
+ @items.size
39
+ end
40
+
37
41
  def empty?
38
42
  @items.empty?
39
43
  end
@@ -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'
@@ -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
@@ -226,6 +284,8 @@ module Async
226
284
  def run(*arguments, **options, &block)
227
285
  raise RuntimeError, 'Reactor has been closed' if @selector.nil?
228
286
 
287
+ @scheduler&.set!
288
+
229
289
  initial_task = self.async(*arguments, **options, &block) if block_given?
230
290
 
231
291
  while self.run_once
@@ -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,114 @@
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(@reactor, &block)
106
+
107
+ fiber = task.fiber
108
+
109
+ task.run
110
+
111
+ return fiber
112
+ end
113
+ end
114
+ 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
@@ -21,5 +21,5 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  module Async
24
- VERSION = "1.26.2"
24
+ VERSION = "1.28.3"
25
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.26.2
4
+ version: 1.28.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-11 00:00:00.000000000 Z
11
+ date: 2021-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: console
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.0'
19
+ version: '1.10'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.0'
26
+ version: '1.10'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: nio4r
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.1'
69
69
  - !ruby/object:Gem::Dependency
70
- name: bake-bundler
70
+ name: bake
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -81,7 +81,7 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: bake-modernize
84
+ name: benchmark-ips
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
@@ -122,20 +122,6 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0.10'
125
- - !ruby/object:Gem::Dependency
126
- name: benchmark-ips
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
125
  - !ruby/object:Gem::Dependency
140
126
  name: rspec
141
127
  requirement: !ruby/object:Gem::Requirement
@@ -167,6 +153,7 @@ files:
167
153
  - lib/async/notification.rb
168
154
  - lib/async/queue.rb
169
155
  - lib/async/reactor.rb
156
+ - lib/async/scheduler.rb
170
157
  - lib/async/semaphore.rb
171
158
  - lib/async/task.rb
172
159
  - lib/async/version.rb
@@ -192,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
192
179
  - !ruby/object:Gem::Version
193
180
  version: '0'
194
181
  requirements: []
195
- rubygems_version: 3.1.2
182
+ rubygems_version: 3.2.3
196
183
  signing_key:
197
184
  specification_version: 4
198
185
  summary: A concurrency framework for Ruby.