async 2.18.0 → 2.21.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a94e07aa32d09643fca9fcdb1abed3ea1cc266561331b42f840b39cdb391673
4
- data.tar.gz: ed364eb2cf2676401edc7a2d35eaab0f9f38e89aed1c66a1b03a0ae5e97a4441
3
+ metadata.gz: 6dda1de8f976c85bf6dcbd0156c27c30f85f88d5ffaf32f3397b9838aa920bab
4
+ data.tar.gz: 0f09cde08880db4456bc03e9a4351903b9a96ca7099e7305c76fdee7f8edc0ed
5
5
  SHA512:
6
- metadata.gz: 353f270a396696c8a689e5e48657a0c06efb21a6a5d6ade30c68c54b7bd09c7b123466b6a382237deffa6f15ce186ececcf49b52d814cafff7bd4c446db3910c
7
- data.tar.gz: 7390bd9fbef443a145dbbfd17f593cb0dffd91021ecb38cd3441a4a6ac7c04394197b512f177595739608a498f54c5619e14fd23ce0c0100a7b45eaa9b0bb3a6
6
+ metadata.gz: 980cd5e5832ddd71846785e10b23d18d11cb1f0eee33ccbdf33ed40feb4ac51dd99f7cf3d7f93055d0ad52de2abe0103c1b433d41f5bac1728c04435e35655db
7
+ data.tar.gz: d9c7c1c69e49acd89738f44900502bc83aea08341b84a9f8d9e02588261889a24a935dfa6509394514a575a4b17d8dcf0ca512518e8b0e089369a8fd37b44d52
checksums.yaml.gz.sig CHANGED
Binary file
data/lib/async/barrier.rb CHANGED
@@ -9,11 +9,11 @@ require_relative "task"
9
9
  module Async
10
10
  # A general purpose synchronisation primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}.
11
11
  #
12
- # @public Since `stable-v1`.
12
+ # @public Since *Async v1*.
13
13
  class Barrier
14
14
  # Initialize the barrier.
15
15
  # @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks.
16
- # @public Since `stable-v1`.
16
+ # @public Since *Async v1*.
17
17
  def initialize(parent: nil)
18
18
  @tasks = List.new
19
19
 
data/lib/async/clock.rb CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  module Async
7
7
  # A convenient wrapper around the internal monotonic clock.
8
- # @public Since `stable-v1`.
8
+ # @public Since *Async v1*.
9
9
  class Clock
10
10
  # Get the current elapsed monotonic time.
11
11
  def self.now
@@ -9,7 +9,7 @@ require_relative "list"
9
9
 
10
10
  module Async
11
11
  # A synchronization primitive, which allows fibers to wait until a particular condition is (edge) triggered.
12
- # @public Since `stable-v1`.
12
+ # @public Since *Async v1*.
13
13
  class Condition
14
14
  # Create a new condition.
15
15
  def initialize
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ module Async
7
+ # Shims for the console gem, redirecting warnings and above to `Kernel#warn`.
8
+ #
9
+ # If you require this file, the `async` library will not depend on the `console` gem.
10
+ #
11
+ # That includes any gems that sit within the `Async` namespace.
12
+ #
13
+ # This is an experimental feature.
14
+ module Console
15
+ # Log a message at the debug level. The shim is silent.
16
+ def self.debug(...)
17
+ end
18
+
19
+ # Log a message at the info level. The shim is silent.
20
+ def self.info(...)
21
+ end
22
+
23
+ # Log a message at the warn level. The shim redirects to `Kernel#warn`.
24
+ def self.warn(*arguments, exception: nil, **options)
25
+ if exception
26
+ super(*arguments, exception.full_message, **options)
27
+ else
28
+ super(*arguments, **options)
29
+ end
30
+ end
31
+
32
+ # Log a message at the error level. The shim redirects to `Kernel#warn`.
33
+ def self.error(...)
34
+ self.warn(...)
35
+ end
36
+
37
+ # Log a message at the fatal level. The shim redirects to `Kernel#warn`.
38
+ def self.fatal(...)
39
+ self.warn(...)
40
+ end
41
+ end
42
+ end
data/lib/async/idler.rb CHANGED
@@ -7,7 +7,8 @@ module Async
7
7
  # A load balancing mechanism that can be used process work when the system is idle.
8
8
  class Idler
9
9
  # Create a new idler.
10
- # @public Since `stable-v2`.
10
+ #
11
+ # @public Since *Async v2*.
11
12
  #
12
13
  # @parameter maximum_load [Numeric] The maximum load before we start shedding work.
13
14
  # @parameter backoff [Numeric] The initial backoff time, used for delaying work.
@@ -7,7 +7,7 @@ require_relative "condition"
7
7
 
8
8
  module Async
9
9
  # A synchronization primitive, which allows fibers to wait until a notification is received. Does not block the task which signals the notification. Waiting tasks are resumed on next iteration of the reactor.
10
- # @public Since `stable-v1`.
10
+ # @public Since *Async v1*.
11
11
  class Notification < Condition
12
12
  # Signal to a given task that it should resume operations.
13
13
  def signal(value = nil, task: Task.current)
data/lib/async/queue.rb CHANGED
@@ -12,7 +12,7 @@ module Async
12
12
  #
13
13
  # It has a compatible interface with {Notification} and {Condition}, except that it's multi-value.
14
14
  #
15
- # @public Since `stable-v1`.
15
+ # @public Since *Async v1*.
16
16
  class Queue
17
17
  # Create a new queue.
18
18
  #
@@ -99,7 +99,7 @@ module Async
99
99
  end
100
100
 
101
101
  # A queue which limits the number of items that can be enqueued.
102
- # @public Since `stable-v1`.
102
+ # @public Since *Async v1*.
103
103
  class LimitedQueue < Queue
104
104
  # Create a new limited queue.
105
105
  #
@@ -7,6 +7,7 @@
7
7
 
8
8
  require_relative "clock"
9
9
  require_relative "task"
10
+ require_relative "worker_pool"
10
11
 
11
12
  require "io/event"
12
13
 
@@ -16,6 +17,10 @@ require "resolv"
16
17
  module Async
17
18
  # Handles scheduling of fibers. Implements the fiber scheduler interface.
18
19
  class Scheduler < Node
20
+ DEFAULT_WORKER_POOL = ENV.fetch("ASYNC_SCHEDULER_DEFAULT_WORKER_POOL", nil).then do |value|
21
+ value == "true" ? true : nil
22
+ end
23
+
19
24
  # Raised when an operation is attempted on a closed scheduler.
20
25
  class ClosedError < RuntimeError
21
26
  # Create a new error.
@@ -27,17 +32,17 @@ module Async
27
32
  end
28
33
 
29
34
  # Whether the fiber scheduler is supported.
30
- # @public Since `stable-v1`.
35
+ # @public Since *Async v1*.
31
36
  def self.supported?
32
37
  true
33
38
  end
34
39
 
35
40
  # Create a new scheduler.
36
41
  #
37
- # @public Since `stable-v1`.
42
+ # @public Since *Async v1*.
38
43
  # @parameter parent [Node | Nil] The parent node to use for task hierarchy.
39
44
  # @parameter selector [IO::Event::Selector] The selector to use for event handling.
40
- def initialize(parent = nil, selector: nil)
45
+ def initialize(parent = nil, selector: nil, worker_pool: DEFAULT_WORKER_POOL)
41
46
  super(parent)
42
47
 
43
48
  @selector = selector || ::IO::Event::Selector.new(Fiber.current)
@@ -49,9 +54,19 @@ module Async
49
54
  @idle_time = 0.0
50
55
 
51
56
  @timers = ::IO::Event::Timers.new
57
+ if worker_pool == true
58
+ @worker_pool = WorkerPool.new
59
+ else
60
+ @worker_pool = worker_pool
61
+ end
62
+
63
+ if @worker_pool
64
+ self.singleton_class.prepend(WorkerPool::BlockingOperationWait)
65
+ end
52
66
  end
53
67
 
54
68
  # Compute the scheduler load according to the busy and idle times that are updated by the run loop.
69
+ #
55
70
  # @returns [Float] The load of the scheduler. 0.0 means no load, 1.0 means fully loaded or over-loaded.
56
71
  def load
57
72
  total_time = @busy_time + @idle_time
@@ -95,7 +110,7 @@ module Async
95
110
  end
96
111
 
97
112
  # Terminate all child tasks and close the scheduler.
98
- # @public Since `stable-v1`.
113
+ # @public Since *Async v1*.
99
114
  def close
100
115
  self.run_loop do
101
116
  until self.terminate
@@ -111,11 +126,16 @@ module Async
111
126
 
112
127
  selector&.close
113
128
 
129
+ worker_pool = @worker_pool
130
+ @worker_pool = nil
131
+
132
+ worker_pool&.close
133
+
114
134
  consume
115
135
  end
116
136
 
117
137
  # @returns [Boolean] Whether the scheduler has been closed.
118
- # @public Since `stable-v1`.
138
+ # @public Since *Async v1*.
119
139
  def closed?
120
140
  @selector.nil?
121
141
  end
@@ -167,7 +187,12 @@ module Async
167
187
  end
168
188
 
169
189
  # Invoked when a fiber tries to perform a blocking operation which cannot continue. A corresponding call {unblock} must be performed to allow this fiber to continue.
190
+ #
191
+ # @public Since *Async v2*.
170
192
  # @asynchronous May only be called on same thread as fiber scheduler.
193
+ #
194
+ # @parameter blocker [Object] The object that is blocking the fiber.
195
+ # @parameter timeout [Float | Nil] The maximum time to block, or if nil, indefinitely.
171
196
  def block(blocker, timeout)
172
197
  # $stderr.puts "block(#{blocker}, #{Fiber.current}, #{timeout})"
173
198
  fiber = Fiber.current
@@ -190,7 +215,13 @@ module Async
190
215
  timer&.cancel!
191
216
  end
192
217
 
218
+ # Unblock a fiber that was previously blocked.
219
+ #
220
+ # @public Since *Async v2* and *Ruby v3.1*.
193
221
  # @asynchronous May be called from any thread.
222
+ #
223
+ # @parameter blocker [Object] The object that was blocking the fiber.
224
+ # @parameter fiber [Fiber] The fiber to unblock.
194
225
  def unblock(blocker, fiber)
195
226
  # $stderr.puts "unblock(#{blocker}, #{fiber})"
196
227
 
@@ -201,7 +232,12 @@ module Async
201
232
  end
202
233
  end
203
234
 
204
- # @asynchronous May be non-blocking..
235
+ # Sleep for the specified duration.
236
+ #
237
+ # @public Since *Async v2* and *Ruby v3.1*.
238
+ # @asynchronous May be non-blocking.
239
+ #
240
+ # @parameter duration [Numeric | Nil] The time in seconds to sleep, or if nil, indefinitely.
205
241
  def kernel_sleep(duration = nil)
206
242
  if duration
207
243
  self.block(nil, duration)
@@ -210,7 +246,12 @@ module Async
210
246
  end
211
247
  end
212
248
 
213
- # @asynchronous May be non-blocking..
249
+ # Resolve the address of the given hostname.
250
+ #
251
+ # @public Since *Async v2*.
252
+ # @asynchronous May be non-blocking.
253
+ #
254
+ # @parameter hostname [String] The hostname to resolve.
214
255
  def address_resolve(hostname)
215
256
  # On some platforms, hostnames may contain a device-specific suffix (e.g. %en0). We need to strip this before resolving.
216
257
  # See <https://github.com/socketry/async/issues/180> for more details.
@@ -218,7 +259,6 @@ module Async
218
259
  ::Resolv.getaddresses(hostname)
219
260
  end
220
261
 
221
-
222
262
  if IO.method_defined?(:timeout)
223
263
  private def get_timeout(io)
224
264
  io.timeout
@@ -229,7 +269,14 @@ module Async
229
269
  end
230
270
  end
231
271
 
232
- # @asynchronous May be non-blocking..
272
+ # Wait for the specified IO to become ready for the specified events.
273
+ #
274
+ # @public Since *Async v2*.
275
+ # @asynchronous May be non-blocking.
276
+ #
277
+ # @parameter io [IO] The IO object to wait on.
278
+ # @parameter events [Integer] The events to wait for, e.g. `IO::READABLE`, `IO::WRITABLE`, etc.
279
+ # @parameter timeout [Float | Nil] The maximum time to wait, or if nil, indefinitely.
233
280
  def io_wait(io, events, timeout = nil)
234
281
  fiber = Fiber.current
235
282
 
@@ -251,6 +298,15 @@ module Async
251
298
  end
252
299
 
253
300
  if ::IO::Event::Support.buffer?
301
+ # Read from the specified IO into the buffer.
302
+ #
303
+ # @public Since *Async v2* and Ruby with `IO::Buffer` support.
304
+ # @asynchronous May be non-blocking.
305
+ #
306
+ # @parameter io [IO] The IO object to read from.
307
+ # @parameter buffer [IO::Buffer] The buffer to read into.
308
+ # @parameter length [Integer] The minimum number of bytes to read.
309
+ # @parameter offset [Integer] The offset within the buffer to read into.
254
310
  def io_read(io, buffer, length, offset = 0)
255
311
  fiber = Fiber.current
256
312
 
@@ -266,6 +322,15 @@ module Async
266
322
  end
267
323
 
268
324
  if RUBY_ENGINE != "ruby" || RUBY_VERSION >= "3.3.1"
325
+ # Write the specified buffer to the IO.
326
+ #
327
+ # @public Since *Async v2* and *Ruby v3.3.1* with `IO::Buffer` support.
328
+ # @asynchronous May be non-blocking.
329
+ #
330
+ # @parameter io [IO] The IO object to write to.
331
+ # @parameter buffer [IO::Buffer] The buffer to write from.
332
+ # @parameter length [Integer] The minimum number of bytes to write.
333
+ # @parameter offset [Integer] The offset within the buffer to write from.
269
334
  def io_write(io, buffer, length, offset = 0)
270
335
  fiber = Fiber.current
271
336
 
@@ -283,6 +348,10 @@ module Async
283
348
  end
284
349
 
285
350
  # Wait for the specified process ID to exit.
351
+ #
352
+ # @public Since *Async v2*.
353
+ # @asynchronous May be non-blocking.
354
+ #
286
355
  # @parameter pid [Integer] The process ID to wait for.
287
356
  # @parameter flags [Integer] A bit-mask of flags suitable for `Process::Status.wait`.
288
357
  # @returns [Process::Status] A process status instance.
@@ -335,7 +404,10 @@ module Async
335
404
  end
336
405
 
337
406
  # Run one iteration of the event loop.
338
- # Does not handle interrupts.
407
+ #
408
+ # @public Since *Async v1*.
409
+ # @asynchronous Must be invoked from blocking (root) fiber.
410
+ #
339
411
  # @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
340
412
  # @returns [Boolean] Whether there is more work to do.
341
413
  def run_once(timeout = nil)
@@ -354,6 +426,7 @@ module Async
354
426
  end
355
427
 
356
428
  # Checks and clears the interrupted state of the scheduler.
429
+ #
357
430
  # @returns [Boolean] Whether the reactor has been interrupted.
358
431
  private def interrupted?
359
432
  if @interrupted
@@ -368,7 +441,9 @@ module Async
368
441
  return false
369
442
  end
370
443
 
371
- # Stop all children, including transient children, ignoring any signals.
444
+ # Stop all children, including transient children.
445
+ #
446
+ # @public Since *Async v1*.
372
447
  def stop
373
448
  @children&.each do |child|
374
449
  child.stop
@@ -387,7 +462,13 @@ module Async
387
462
  end
388
463
  end
389
464
  rescue Interrupt => interrupt
465
+ # If an interrupt did occur during an iteration of the event loop, we need to handle it. More specifically, `self.stop` is not safe to interrupt without potentially corrupting the task tree.
390
466
  Thread.handle_interrupt(::SignalException => :never) do
467
+ Console.debug(self) do |buffer|
468
+ buffer.puts "Scheduler interrupted: #{interrupt.inspect}"
469
+ self.print_hierarchy(buffer)
470
+ end
471
+
391
472
  self.stop
392
473
  end
393
474
 
@@ -395,10 +476,19 @@ module Async
395
476
  end
396
477
 
397
478
  # If the event loop was interrupted, and we finished exiting normally (due to the interrupt), we need to re-raise the interrupt so that the caller can handle it too.
398
- Kernel.raise(interrupt) if interrupt
479
+ if interrupt
480
+ Kernel.raise(interrupt)
481
+ end
399
482
  end
400
483
 
401
484
  # Run the reactor until all tasks are finished. Proxies arguments to {#async} immediately before entering the loop, if a block is provided.
485
+ #
486
+ # Forwards all parameters to {#async} if a block is given.
487
+ #
488
+ # @public Since *Async v1*.
489
+ #
490
+ # @yields {|task| ...} The top level task, if a block is given.
491
+ # @returns [Task] The initial task that was scheduled into the reactor.
402
492
  def run(...)
403
493
  Kernel.raise ClosedError if @selector.nil?
404
494
 
@@ -411,30 +501,23 @@ module Async
411
501
  return initial_task
412
502
  end
413
503
 
414
- # Start an asynchronous task within the specified reactor. The task will be
415
- # executed until the first blocking call, at which point it will yield and
416
- # and this method will return.
504
+ # Start an asynchronous task within the specified reactor. The task will be executed until the first blocking call, at which point it will yield and and this method will return.
417
505
  #
418
- # This is the main entry point for scheduling asynchronus tasks.
506
+ # @public Since *Async v1*.
507
+ # @asynchronous May context switch immediately to new task.
508
+ # @deprecated Use {#run} or {Task#async} instead.
419
509
  #
420
510
  # @yields {|task| ...} Executed within the task.
421
511
  # @returns [Task] The task that was scheduled into the reactor.
422
- # @deprecated With no replacement.
423
512
  def async(*arguments, **options, &block)
513
+ # warn "Async::Scheduler#async is deprecated. Use `run` or `Task#async` instead.", uplevel: 1, category: :deprecated
514
+
424
515
  Kernel.raise ClosedError if @selector.nil?
425
516
 
426
517
  task = Task.new(Task.current? || self, **options, &block)
427
518
 
428
- # I want to take a moment to explain the logic of this.
429
- # When calling an async block, we deterministically execute it until the
430
- # first blocking operation. We don't *have* to do this - we could schedule
431
- # it for later execution, but it's useful to:
432
- # - Fail at the point of the method call where possible.
433
- # - Execute determinstically where possible.
434
- # - Avoid scheduler overhead if no blocking operation is performed.
435
519
  task.run(*arguments)
436
520
 
437
- # Console.debug "Initial execution of task #{fiber} complete (#{result} -> #{fiber.alive?})..."
438
521
  return task
439
522
  end
440
523
 
@@ -443,7 +526,14 @@ module Async
443
526
  end
444
527
 
445
528
  # Invoke the block, but after the specified timeout, raise {TimeoutError} in any currenly blocking operation. If the block runs to completion before the timeout occurs or there are no non-blocking operations after the timeout expires, the code will complete without any exception.
529
+ #
530
+ # @public Since *Async v1*.
531
+ # @asynchronous May raise an exception at any interruption point (e.g. blocking operations).
532
+ #
446
533
  # @parameter duration [Numeric] The time in seconds, in which the task should complete.
534
+ # @parameter exception [Class] The exception class to raise.
535
+ # @parameter message [String] The message to pass to the exception.
536
+ # @yields {|duration| ...} The block to execute with a timeout.
447
537
  def with_timeout(duration, exception = TimeoutError, message = "execution expired", &block)
448
538
  fiber = Fiber.current
449
539
 
@@ -458,6 +548,15 @@ module Async
458
548
  timer&.cancel!
459
549
  end
460
550
 
551
+ # Invoke the block, but after the specified timeout, raise the specified exception with the given message. If the block runs to completion before the timeout occurs or there are no non-blocking operations after the timeout expires, the code will complete without any exception.
552
+ #
553
+ # @public Since *Async v1* and *Ruby v3.1*. May be invoked from `Timeout.timeout`.
554
+ # @asynchronous May raise an exception at any interruption point (e.g. blocking operations).
555
+ #
556
+ # @parameter duration [Numeric] The time in seconds, in which the task should complete.
557
+ # @parameter exception [Class] The exception class to raise.
558
+ # @parameter message [String] The message to pass to the exception.
559
+ # @yields {|duration| ...} The block to execute with a timeout.
461
560
  def timeout_after(duration, exception, message, &block)
462
561
  with_timeout(duration, exception, message) do |timer|
463
562
  yield duration
@@ -7,7 +7,7 @@ require_relative "list"
7
7
 
8
8
  module Async
9
9
  # A synchronization primitive, which limits access to a given resource.
10
- # @public Since `stable-v1`.
10
+ # @public Since *Async v1*.
11
11
  class Semaphore
12
12
  # @parameter limit [Integer] The maximum number of times the semaphore can be acquired before it blocks.
13
13
  # @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks.
data/lib/async/task.rb CHANGED
@@ -8,7 +8,7 @@
8
8
  # Copyright, 2023, by Math Ieu.
9
9
 
10
10
  require "fiber"
11
- require "console/event/failure"
11
+ require "console"
12
12
 
13
13
  require_relative "node"
14
14
  require_relative "condition"
@@ -40,7 +40,7 @@ module Async
40
40
  end
41
41
 
42
42
  # Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
43
- # @public Since `stable-v1`.
43
+ # @public Since *Async v1*.
44
44
  class TimeoutError < StandardError
45
45
  # Create a new timeout error.
46
46
  #
@@ -50,7 +50,7 @@ module Async
50
50
  end
51
51
  end
52
52
 
53
- # @public Since `stable-v1`.
53
+ # @public Since *Async v1*.
54
54
  class Task < Node
55
55
  # Raised when a child task is created within a task that has finished execution.
56
56
  class FinishedError < RuntimeError
@@ -198,9 +198,7 @@ module Async
198
198
  rescue => error
199
199
  # I'm not completely happy with this overhead, but the alternative is to not log anything which makes debugging extremely difficult. Maybe we can introduce a debug wrapper which adds extra logging.
200
200
  if @finished.nil?
201
- Console::Event::Failure.for(error).emit(self, "Task may have ended with unhandled exception.", severity: :warn)
202
- else
203
- # Console::Event::Failure.for(error).emit(self, severity: :debug)
201
+ warn(self, "Task may have ended with unhandled exception.", exception: error)
204
202
  end
205
203
 
206
204
  raise
@@ -212,6 +210,10 @@ module Async
212
210
 
213
211
  # Run an asynchronous task as a child of the current task.
214
212
  #
213
+ # @public Since *Async v1*.
214
+ # @asynchronous May context switch immediately to the new task.
215
+ #
216
+ # @yields {|task| ...} in the context of the new task.
215
217
  # @raises [FinishedError] If the task has already finished.
216
218
  # @returns [Task] The child task.
217
219
  def async(*arguments, **options, &block)
@@ -219,6 +221,13 @@ module Async
219
221
 
220
222
  task = Task.new(self, **options, &block)
221
223
 
224
+ # When calling an async block, we deterministically execute it until the first blocking operation. We don't *have* to do this - we could schedule it for later execution, but it's useful to:
225
+ #
226
+ # - Fail at the point of the method call where possible.
227
+ # - Execute determinstically where possible.
228
+ # - Avoid scheduler overhead if no blocking operation is performed.
229
+ #
230
+ # There are different strategies (greedy vs non-greedy). We are currently using a greedy strategy.
222
231
  task.run(*arguments)
223
232
 
224
233
  return task
@@ -304,7 +313,7 @@ module Async
304
313
  # If stop is invoked a second time, it will be immediately executed.
305
314
  #
306
315
  # @yields {} The block of code to execute.
307
- # @public Since `stable-v1`.
316
+ # @public Since *Async v1*.
308
317
  def defer_stop
309
318
  # Tri-state variable for controlling stop:
310
319
  # - nil: defer_stop has not been called.
@@ -362,6 +371,10 @@ module Async
362
371
 
363
372
  private
364
373
 
374
+ def warn(...)
375
+ Console.warn(...)
376
+ end
377
+
365
378
  # Finish the current task, moving any children to the parent.
366
379
  def finish!
367
380
  # Don't hold references to the fiber or block after the task has finished:
data/lib/async/version.rb CHANGED
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2017-2024, by Samuel Williams.
5
5
 
6
6
  module Async
7
- VERSION = "2.18.0"
7
+ VERSION = "2.21.1"
8
8
  end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require "etc"
7
+
8
+ module Async
9
+ # A simple work pool that offloads work to a background thread.
10
+ #
11
+ # @private
12
+ class WorkerPool
13
+ module BlockingOperationWait
14
+ # Wait for the given work to be executed.
15
+ #
16
+ # @public Since *Async v2.19* and *Ruby v3.4*.
17
+ # @asynchronous May be non-blocking.
18
+ #
19
+ # @parameter work [Proc] The work to execute on a background thread.
20
+ # @returns [Object] The result of the work.
21
+ def blocking_operation_wait(work)
22
+ @worker_pool.call(work)
23
+ end
24
+ end
25
+
26
+ class Promise
27
+ def initialize(work)
28
+ @work = work
29
+ @state = :pending
30
+ @value = nil
31
+ @guard = ::Mutex.new
32
+ @condition = ::ConditionVariable.new
33
+ @thread = nil
34
+ end
35
+
36
+ def call
37
+ work = nil
38
+
39
+ @guard.synchronize do
40
+ @thread = ::Thread.current
41
+
42
+ return unless work = @work
43
+ end
44
+
45
+ resolve(work.call)
46
+ rescue Exception => error
47
+ reject(error)
48
+ end
49
+
50
+ private def resolve(value)
51
+ @guard.synchronize do
52
+ @work = nil
53
+ @thread = nil
54
+ @value = value
55
+ @state = :resolved
56
+ @condition.broadcast
57
+ end
58
+ end
59
+
60
+ private def reject(error)
61
+ @guard.synchronize do
62
+ @work = nil
63
+ @thread = nil
64
+ @value = error
65
+ @state = :failed
66
+ @condition.broadcast
67
+ end
68
+ end
69
+
70
+ def cancel
71
+ return unless @work
72
+
73
+ @guard.synchronize do
74
+ @work = nil
75
+ @state = :cancelled
76
+ @thread&.raise(Interrupt)
77
+ end
78
+ end
79
+
80
+ def wait
81
+ @guard.synchronize do
82
+ while @state == :pending
83
+ @condition.wait(@guard)
84
+ end
85
+
86
+ if @state == :failed
87
+ raise @value
88
+ else
89
+ return @value
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # A handle to the work being done.
96
+ class Worker
97
+ def initialize
98
+ @work = ::Thread::Queue.new
99
+ @thread = ::Thread.new(&method(:run))
100
+ end
101
+
102
+ def run
103
+ while work = @work.pop
104
+ work.call
105
+ end
106
+ end
107
+
108
+ def close
109
+ if thread = @thread
110
+ @thread = nil
111
+ thread.kill
112
+ end
113
+ end
114
+
115
+ # Call the work and notify the scheduler when it is done.
116
+ def call(work)
117
+ promise = Promise.new(work)
118
+
119
+ @work.push(promise)
120
+
121
+ begin
122
+ return promise.wait
123
+ ensure
124
+ promise.cancel
125
+ end
126
+ end
127
+ end
128
+
129
+ # Create a new work pool.
130
+ #
131
+ # @parameter size [Integer] The number of threads to use.
132
+ def initialize(size: Etc.nprocessors)
133
+ @ready = ::Thread::Queue.new
134
+
135
+ size.times do
136
+ @ready.push(Worker.new)
137
+ end
138
+ end
139
+
140
+ # Close the work pool. Kills all outstanding work.
141
+ def close
142
+ if ready = @ready
143
+ @ready = nil
144
+ ready.close
145
+
146
+ while worker = ready.pop
147
+ worker.close
148
+ end
149
+ end
150
+ end
151
+
152
+ # Offload work to a thread.
153
+ #
154
+ # @parameter work [Proc] The work to be done.
155
+ def call(work)
156
+ if ready = @ready
157
+ worker = ready.pop
158
+
159
+ begin
160
+ worker.call(work)
161
+ ensure
162
+ ready.push(worker)
163
+ end
164
+ else
165
+ raise RuntimeError, "No worker available!"
166
+ end
167
+ end
168
+ end
169
+ end
data/lib/async/wrapper.rb CHANGED
@@ -4,6 +4,8 @@
4
4
  # Copyright, 2017-2024, by Samuel Williams.
5
5
  # Copyright, 2017, by Kent Gruber.
6
6
 
7
+ warn "Async::Wrapper is deprecated and will be removed on 2025-03-31. Please use native interfaces instead.", uplevel: 1, category: :deprecated
8
+
7
9
  module Async
8
10
  # Represents an asynchronous IO within a reactor.
9
11
  # @deprecated With no replacement. Prefer native interfaces.
data/lib/kernel/async.rb CHANGED
@@ -19,7 +19,7 @@ module Kernel
19
19
  # @yields {|task| ...} The block that will execute asynchronously.
20
20
  # @parameter task [Async::Task] The task that is executing the given block.
21
21
  #
22
- # @public Since `stable-v1`.
22
+ # @public Since *Async v1*.
23
23
  # @asynchronous May block until given block completes executing.
24
24
  def Async(...)
25
25
  if current = ::Async::Task.current?
data/lib/kernel/sync.rb CHANGED
@@ -14,7 +14,7 @@ module Kernel
14
14
  # @yields {|task| ...} The block that will execute asynchronously.
15
15
  # @parameter task [Async::Task] The task that is executing the given block.
16
16
  #
17
- # @public Since `stable-v1`.
17
+ # @public Since *Async v1*.
18
18
  # @asynchronous Will block until given block completes executing.
19
19
  def Sync(annotation: nil, &block)
20
20
  if task = ::Async::Task.current?
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative "../../../async/task"
7
+ require "metrics/provider"
8
+
9
+ Metrics::Provider(Async::Task) do
10
+ ASYNC_TASK_SCHEDULED = Metrics.metric("async.task.scheduled", :counter, description: "The number of tasks scheduled.")
11
+
12
+ def schedule(&block)
13
+ ASYNC_TASK_SCHEDULED.emit(1)
14
+
15
+ super(&block)
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative "async/task"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022, by Samuel Williams.
5
+
6
+ require_relative "../../../async/barrier"
7
+ require "traces/provider"
8
+
9
+ Traces::Provider(Async::Barrier) do
10
+ def wait
11
+ attributes = {
12
+ "size" => self.size
13
+ }
14
+
15
+ Traces.trace("async.barrier.wait", attributes: attributes) {super}
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022, by Samuel Williams.
5
+
6
+ require_relative "../../../async/task"
7
+ require "traces/provider"
8
+
9
+ Traces::Provider(Async::Task) do
10
+ def schedule(&block)
11
+ unless self.transient?
12
+ trace_context = Traces.trace_context
13
+ end
14
+
15
+ super do
16
+ Traces.trace_context = trace_context
17
+
18
+ if annotation = self.annotation
19
+ attributes = {
20
+ "annotation" => annotation
21
+ }
22
+ end
23
+
24
+ Traces.trace("async.task", attributes: attributes) do
25
+ yield
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative "async/task"
7
+ require_relative "async/barrier"
data/readme.md CHANGED
@@ -35,6 +35,19 @@ Please see the [project documentation](https://socketry.github.io/async/) for mo
35
35
 
36
36
  Please see the [project releases](https://socketry.github.io/async/releases/index) for all releases.
37
37
 
38
+ ### v2.21.1
39
+
40
+ - [Worker Pool](https://socketry.github.io/async/releases/index#worker-pool)
41
+
42
+ ### v2.20.0
43
+
44
+ - [Traces and Metrics Providers](https://socketry.github.io/async/releases/index#traces-and-metrics-providers)
45
+
46
+ ### v2.19.0
47
+
48
+ - [Async::Scheduler Debugging](https://socketry.github.io/async/releases/index#async::scheduler-debugging)
49
+ - [Console Shims](https://socketry.github.io/async/releases/index#console-shims)
50
+
38
51
  ### v2.18.0
39
52
 
40
53
  - Add support for `Sync(annotation:)`, so that you can annotate the block with a description of what it does, even if it doesn't create a new task.
data/releases.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # Releases
2
2
 
3
+ ## v2.21.1
4
+
5
+ ### Worker Pool
6
+
7
+ Ruby 3.4 will feature a new fiber scheduler hook, `blocking_operation_wait` which allows the scheduler to redirect the work given to `rb_nogvl` to a worker pool.
8
+
9
+ The Async scheduler optionally supports this feature using a worker pool, by using the following environment variable:
10
+
11
+ ASYNC_SCHEDULER_DEFAULT_WORKER_POOL=true
12
+
13
+ This will cause the scheduler to use a worker pool for general blocking operations, rather than blocking the event loop.
14
+
15
+ It should be noted that this isn't a net win, as the overhead of using a worker pool can be significant compared to the `rb_nogvl` work. As such, it is recommended to benchmark your application with and without the worker pool to determine if it is beneficial.
16
+
17
+ ## v2.20.0
18
+
19
+ ### Traces and Metrics Providers
20
+
21
+ Async now has [traces](https://github.com/socketry/traces) and [metrics](https://github.com/socketry/metrics) providers for various core classes. This allows you to emit traces and metrics to a suitable backend (including DataDog, New Relic, OpenTelemetry, etc.) for monitoring and debugging purposes.
22
+
23
+ To take advantage of this feature, you will need to introduce your own `config/traces.rb` and `config/metrics.rb`. Async's own repository includes these files for testing purposes, you could copy them into your own project and modify them as needed.
24
+
25
+ ## v2.19.0
26
+
27
+ ### Async::Scheduler Debugging
28
+
29
+ Occasionally on issues, I encounter people asking for help and I need more information. Pressing Ctrl-C to exit a hung program is common, but it usually doesn't provide enough information to diagnose the problem. Setting the `CONSOLE_LEVEL=debug` environment variable will now print additional information about the scheduler when you interrupt it, including a backtrace of the current tasks.
30
+
31
+ > CONSOLE_LEVEL=debug bundle exec ruby ./test.rb
32
+ ^C 0.0s debug: Async::Reactor [oid=0x974] [ec=0x988] [pid=9116] [2024-11-08 14:12:03 +1300]
33
+ | Scheduler interrupted: Interrupt
34
+ | #<Async::Reactor:0x0000000000000974 1 children (running)>
35
+ | #<Async::Task:0x000000000000099c /Users/samuel/Developer/socketry/async/lib/async/scheduler.rb:185:in `transfer' (running)>
36
+ | → /Users/samuel/Developer/socketry/async/lib/async/scheduler.rb:185:in `transfer'
37
+ | /Users/samuel/Developer/socketry/async/lib/async/scheduler.rb:185:in `block'
38
+ | /Users/samuel/Developer/socketry/async/lib/async/scheduler.rb:207:in `kernel_sleep'
39
+ | /Users/samuel/Developer/socketry/async/test.rb:7:in `sleep'
40
+ | /Users/samuel/Developer/socketry/async/test.rb:7:in `sleepy'
41
+ | /Users/samuel/Developer/socketry/async/test.rb:12:in `block in <top (required)>'
42
+ | /Users/samuel/Developer/socketry/async/lib/async/task.rb:197:in `block in run'
43
+ | /Users/samuel/Developer/socketry/async/lib/async/task.rb:420:in `block in schedule'
44
+ /Users/samuel/Developer/socketry/async/lib/async/scheduler.rb:317:in `select': Interrupt
45
+ ... (backtrace continues) ...
46
+
47
+ This gives better visibility into what the scheduler is doing, and should help diagnose issues.
48
+
49
+ ### Console Shims
50
+
51
+ The `async` gem depends on `console` gem, because my goal was to have good logging by default without thinking about it too much. However, some users prefer to avoid using the `console` gem for logging, so I've added an experimental set of shims which should allow you to bypass the `console` gem entirely.
52
+
53
+ ``` ruby
54
+ require 'async/console'
55
+ require 'async'
56
+
57
+ Async{raise "Boom"}
58
+ ```
59
+
60
+ Will now use `Kernel#warn` to print the task failure warning:
61
+
62
+ #<Async::Task:0x00000000000012d4 /home/samuel/Developer/socketry/async/lib/async/task.rb:104:in `backtrace' (running)>
63
+ Task may have ended with unhandled exception.
64
+ (irb):4:in `block in <top (required)>': Boom (RuntimeError)
65
+ from /home/samuel/Developer/socketry/async/lib/async/task.rb:197:in `block in run'
66
+ from /home/samuel/Developer/socketry/async/lib/async/task.rb:420:in `block in schedule'
67
+
3
68
  ## v2.18.0
4
69
 
5
70
  - Add support for `Sync(annotation:)`, so that you can annotate the block with a description of what it does, even if it doesn't create a new task.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.18.0
4
+ version: 2.21.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -63,7 +63,7 @@ cert_chain:
63
63
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
64
64
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
65
65
  -----END CERTIFICATE-----
66
- date: 2024-10-29 00:00:00.000000000 Z
66
+ date: 2024-11-27 00:00:00.000000000 Z
67
67
  dependencies:
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: console
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '1.26'
74
+ version: '1.29'
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '1.26'
81
+ version: '1.29'
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: fiber-annotation
84
84
  requirement: !ruby/object:Gem::Requirement
@@ -125,6 +125,7 @@ files:
125
125
  - lib/async/clock.rb
126
126
  - lib/async/condition.md
127
127
  - lib/async/condition.rb
128
+ - lib/async/console.rb
128
129
  - lib/async/idler.rb
129
130
  - lib/async/list.rb
130
131
  - lib/async/node.rb
@@ -140,9 +141,15 @@ files:
140
141
  - lib/async/version.rb
141
142
  - lib/async/waiter.md
142
143
  - lib/async/waiter.rb
144
+ - lib/async/worker_pool.rb
143
145
  - lib/async/wrapper.rb
144
146
  - lib/kernel/async.rb
145
147
  - lib/kernel/sync.rb
148
+ - lib/metrics/provider/async.rb
149
+ - lib/metrics/provider/async/task.rb
150
+ - lib/traces/provider/async.rb
151
+ - lib/traces/provider/async/barrier.rb
152
+ - lib/traces/provider/async/task.rb
146
153
  - license.md
147
154
  - readme.md
148
155
  - releases.md
@@ -168,7 +175,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
168
175
  - !ruby/object:Gem::Version
169
176
  version: '0'
170
177
  requirements: []
171
- rubygems_version: 3.3.8
178
+ rubygems_version: 3.5.22
172
179
  signing_key:
173
180
  specification_version: 4
174
181
  summary: A concurrency framework for Ruby.
metadata.gz.sig CHANGED
Binary file