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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/async/barrier.rb +2 -2
- data/lib/async/clock.rb +1 -1
- data/lib/async/condition.rb +1 -1
- data/lib/async/console.rb +42 -0
- data/lib/async/idler.rb +2 -1
- data/lib/async/notification.rb +1 -1
- data/lib/async/queue.rb +2 -2
- data/lib/async/scheduler.rb +124 -25
- data/lib/async/semaphore.rb +1 -1
- data/lib/async/task.rb +20 -7
- data/lib/async/version.rb +1 -1
- data/lib/async/worker_pool.rb +169 -0
- data/lib/async/wrapper.rb +2 -0
- data/lib/kernel/async.rb +1 -1
- data/lib/kernel/sync.rb +1 -1
- data/lib/metrics/provider/async/task.rb +17 -0
- data/lib/metrics/provider/async.rb +6 -0
- data/lib/traces/provider/async/barrier.rb +17 -0
- data/lib/traces/provider/async/task.rb +29 -0
- data/lib/traces/provider/async.rb +7 -0
- data/readme.md +13 -0
- data/releases.md +65 -0
- data.tar.gz.sig +0 -0
- metadata +12 -5
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6dda1de8f976c85bf6dcbd0156c27c30f85f88d5ffaf32f3397b9838aa920bab
|
4
|
+
data.tar.gz: 0f09cde08880db4456bc03e9a4351903b9a96ca7099e7305c76fdee7f8edc0ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
16
|
+
# @public Since *Async v1*.
|
17
17
|
def initialize(parent: nil)
|
18
18
|
@tasks = List.new
|
19
19
|
|
data/lib/async/clock.rb
CHANGED
data/lib/async/condition.rb
CHANGED
@@ -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
|
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
|
-
#
|
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.
|
data/lib/async/notification.rb
CHANGED
@@ -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
|
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
|
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
|
102
|
+
# @public Since *Async v1*.
|
103
103
|
class LimitedQueue < Queue
|
104
104
|
# Create a new limited queue.
|
105
105
|
#
|
data/lib/async/scheduler.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
-
|
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
|
-
#
|
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
|
data/lib/async/semaphore.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
-
|
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
|
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
@@ -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
|
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
|
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,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
|
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.
|
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-
|
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.
|
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.
|
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.
|
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
|