async 2.17.0 → 2.32.0
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/context/best-practices.md +188 -0
- data/context/debugging.md +63 -0
- data/context/getting-started.md +177 -0
- data/context/index.yaml +29 -0
- data/context/scheduler.md +109 -0
- data/context/tasks.md +448 -0
- data/context/thread-safety.md +651 -0
- data/lib/async/barrier.md +1 -2
- data/lib/async/barrier.rb +35 -12
- data/lib/async/clock.rb +11 -2
- data/lib/async/condition.md +1 -1
- data/lib/async/condition.rb +18 -34
- data/lib/async/console.rb +42 -0
- data/lib/async/deadline.rb +70 -0
- data/lib/async/idler.rb +2 -1
- data/lib/async/limited_queue.rb +13 -0
- data/lib/async/list.rb +16 -8
- data/lib/async/node.rb +5 -3
- data/lib/async/notification.rb +13 -9
- data/lib/async/priority_queue.rb +253 -0
- data/lib/async/promise.rb +188 -0
- data/lib/async/queue.rb +70 -82
- data/lib/async/reactor.rb +4 -2
- data/lib/async/scheduler.rb +233 -54
- data/lib/async/semaphore.rb +3 -3
- data/lib/async/stop.rb +82 -0
- data/lib/async/task.rb +111 -81
- data/lib/async/timeout.rb +88 -0
- data/lib/async/variable.rb +15 -4
- data/lib/async/version.rb +2 -2
- data/lib/async/waiter.rb +6 -1
- data/lib/kernel/async.rb +1 -1
- data/lib/kernel/sync.rb +14 -5
- data/lib/metrics/provider/async/task.rb +20 -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 +40 -0
- data/lib/traces/provider/async.rb +7 -0
- data/license.md +8 -1
- data/readme.md +50 -7
- data/releases.md +357 -0
- data.tar.gz.sig +0 -0
- metadata +61 -20
- metadata.gz.sig +0 -0
- data/lib/async/waiter.md +0 -50
- data/lib/async/wrapper.rb +0 -65
data/lib/async/scheduler.rb
CHANGED
@@ -1,21 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2020-
|
4
|
+
# Copyright, 2020-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2020, by Jun Jiang.
|
6
6
|
# Copyright, 2021, by Julien Portalier.
|
7
|
+
# Copyright, 2025, by Shopify Inc.
|
7
8
|
|
8
|
-
require_relative
|
9
|
-
require_relative
|
9
|
+
require_relative "clock"
|
10
|
+
require_relative "task"
|
11
|
+
require_relative "timeout"
|
10
12
|
|
11
|
-
require
|
13
|
+
require "io/event"
|
12
14
|
|
13
|
-
require
|
14
|
-
require
|
15
|
+
require "console"
|
16
|
+
require "resolv"
|
15
17
|
|
16
18
|
module Async
|
19
|
+
begin
|
20
|
+
require "fiber/profiler"
|
21
|
+
Profiler = Fiber::Profiler
|
22
|
+
rescue LoadError
|
23
|
+
# Fiber::Profiler is not available.
|
24
|
+
Profiler = nil
|
25
|
+
end
|
26
|
+
|
17
27
|
# Handles scheduling of fibers. Implements the fiber scheduler interface.
|
18
28
|
class Scheduler < Node
|
29
|
+
WORKER_POOL = ENV.fetch("ASYNC_SCHEDULER_WORKER_POOL", nil).then do |value|
|
30
|
+
value == "true" ? true : nil
|
31
|
+
end
|
32
|
+
|
19
33
|
# Raised when an operation is attempted on a closed scheduler.
|
20
34
|
class ClosedError < RuntimeError
|
21
35
|
# Create a new error.
|
@@ -27,20 +41,44 @@ module Async
|
|
27
41
|
end
|
28
42
|
|
29
43
|
# Whether the fiber scheduler is supported.
|
30
|
-
# @public Since
|
44
|
+
# @public Since *Async v1*.
|
31
45
|
def self.supported?
|
32
46
|
true
|
33
47
|
end
|
34
48
|
|
49
|
+
# Used to augment the scheduler to add support for blocking operations.
|
50
|
+
module BlockingOperationWait
|
51
|
+
# Wait for the given work to be executed.
|
52
|
+
#
|
53
|
+
# @public Since *Async v2.21* and *Ruby v3.4*.
|
54
|
+
# @asynchronous May be non-blocking.
|
55
|
+
#
|
56
|
+
# @parameter work [Proc] The work to execute on a background thread.
|
57
|
+
# @returns [Object] The result of the work.
|
58
|
+
def blocking_operation_wait(work)
|
59
|
+
@worker_pool.call(work)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private_constant :BlockingOperationWait
|
64
|
+
|
65
|
+
if ::IO::Event.const_defined?(:WorkerPool)
|
66
|
+
WorkerPool = ::IO::Event::WorkerPool
|
67
|
+
else
|
68
|
+
WorkerPool = nil
|
69
|
+
end
|
70
|
+
|
35
71
|
# Create a new scheduler.
|
36
72
|
#
|
37
|
-
# @public Since
|
73
|
+
# @public Since *Async v1*.
|
38
74
|
# @parameter parent [Node | Nil] The parent node to use for task hierarchy.
|
39
75
|
# @parameter selector [IO::Event::Selector] The selector to use for event handling.
|
40
|
-
def initialize(parent = nil, selector: nil)
|
76
|
+
def initialize(parent = nil, selector: nil, profiler: Profiler&.default, worker_pool: WORKER_POOL)
|
41
77
|
super(parent)
|
42
78
|
|
43
79
|
@selector = selector || ::IO::Event::Selector.new(Fiber.current)
|
80
|
+
@profiler = profiler
|
81
|
+
|
44
82
|
@interrupted = false
|
45
83
|
|
46
84
|
@blocked = 0
|
@@ -49,9 +87,20 @@ module Async
|
|
49
87
|
@idle_time = 0.0
|
50
88
|
|
51
89
|
@timers = ::IO::Event::Timers.new
|
90
|
+
|
91
|
+
if worker_pool == true
|
92
|
+
@worker_pool = WorkerPool&.new
|
93
|
+
else
|
94
|
+
@worker_pool = worker_pool
|
95
|
+
end
|
96
|
+
|
97
|
+
if @worker_pool
|
98
|
+
self.singleton_class.prepend(BlockingOperationWait)
|
99
|
+
end
|
52
100
|
end
|
53
101
|
|
54
102
|
# Compute the scheduler load according to the busy and idle times that are updated by the run loop.
|
103
|
+
#
|
55
104
|
# @returns [Float] The load of the scheduler. 0.0 means no load, 1.0 means fully loaded or over-loaded.
|
56
105
|
def load
|
57
106
|
total_time = @busy_time + @idle_time
|
@@ -71,7 +120,7 @@ module Async
|
|
71
120
|
return @busy_time / total_time
|
72
121
|
end
|
73
122
|
end
|
74
|
-
|
123
|
+
|
75
124
|
# Invoked when the fiber scheduler is being closed.
|
76
125
|
#
|
77
126
|
# Executes the run loop until all tasks are finished, then closes the scheduler.
|
@@ -95,7 +144,7 @@ module Async
|
|
95
144
|
end
|
96
145
|
|
97
146
|
# Terminate all child tasks and close the scheduler.
|
98
|
-
# @public Since
|
147
|
+
# @public Since *Async v1*.
|
99
148
|
def close
|
100
149
|
self.run_loop do
|
101
150
|
until self.terminate
|
@@ -111,11 +160,16 @@ module Async
|
|
111
160
|
|
112
161
|
selector&.close
|
113
162
|
|
163
|
+
worker_pool = @worker_pool
|
164
|
+
@worker_pool = nil
|
165
|
+
|
166
|
+
worker_pool&.close
|
167
|
+
|
114
168
|
consume
|
115
169
|
end
|
116
170
|
|
117
171
|
# @returns [Boolean] Whether the scheduler has been closed.
|
118
|
-
# @public Since
|
172
|
+
# @public Since *Async v1*.
|
119
173
|
def closed?
|
120
174
|
@selector.nil?
|
121
175
|
end
|
@@ -167,7 +221,12 @@ module Async
|
|
167
221
|
end
|
168
222
|
|
169
223
|
# 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.
|
224
|
+
#
|
225
|
+
# @public Since *Async v2*.
|
170
226
|
# @asynchronous May only be called on same thread as fiber scheduler.
|
227
|
+
#
|
228
|
+
# @parameter blocker [Object] The object that is blocking the fiber.
|
229
|
+
# @parameter timeout [Float | Nil] The maximum time to block, or if nil, indefinitely.
|
171
230
|
def block(blocker, timeout)
|
172
231
|
# $stderr.puts "block(#{blocker}, #{Fiber.current}, #{timeout})"
|
173
232
|
fiber = Fiber.current
|
@@ -190,9 +249,15 @@ module Async
|
|
190
249
|
timer&.cancel!
|
191
250
|
end
|
192
251
|
|
252
|
+
# Unblock a fiber that was previously blocked.
|
253
|
+
#
|
254
|
+
# @public Since *Async v2* and *Ruby v3.1*.
|
193
255
|
# @asynchronous May be called from any thread.
|
256
|
+
#
|
257
|
+
# @parameter blocker [Object] The object that was blocking the fiber.
|
258
|
+
# @parameter fiber [Fiber] The fiber to unblock.
|
194
259
|
def unblock(blocker, fiber)
|
195
|
-
# $stderr.puts "unblock(#{blocker}, #{fiber})"
|
260
|
+
# Fiber.blocking{$stderr.puts "unblock(#{blocker}, #{fiber})"}
|
196
261
|
|
197
262
|
# This operation is protected by the GVL:
|
198
263
|
if selector = @selector
|
@@ -201,8 +266,15 @@ module Async
|
|
201
266
|
end
|
202
267
|
end
|
203
268
|
|
204
|
-
#
|
269
|
+
# Sleep for the specified duration.
|
270
|
+
#
|
271
|
+
# @public Since *Async v2* and *Ruby v3.1*.
|
272
|
+
# @asynchronous May be non-blocking.
|
273
|
+
#
|
274
|
+
# @parameter duration [Numeric | Nil] The time in seconds to sleep, or if nil, indefinitely.
|
205
275
|
def kernel_sleep(duration = nil)
|
276
|
+
# Fiber.blocking{$stderr.puts "kernel_sleep(#{duration}, #{Fiber.current})"}
|
277
|
+
|
206
278
|
if duration
|
207
279
|
self.block(nil, duration)
|
208
280
|
else
|
@@ -210,7 +282,12 @@ module Async
|
|
210
282
|
end
|
211
283
|
end
|
212
284
|
|
213
|
-
#
|
285
|
+
# Resolve the address of the given hostname.
|
286
|
+
#
|
287
|
+
# @public Since *Async v2*.
|
288
|
+
# @asynchronous May be non-blocking.
|
289
|
+
#
|
290
|
+
# @parameter hostname [String] The hostname to resolve.
|
214
291
|
def address_resolve(hostname)
|
215
292
|
# On some platforms, hostnames may contain a device-specific suffix (e.g. %en0). We need to strip this before resolving.
|
216
293
|
# See <https://github.com/socketry/async/issues/180> for more details.
|
@@ -218,18 +295,14 @@ module Async
|
|
218
295
|
::Resolv.getaddresses(hostname)
|
219
296
|
end
|
220
297
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
|
-
# @asynchronous May be non-blocking..
|
298
|
+
# Wait for the specified IO to become ready for the specified events.
|
299
|
+
#
|
300
|
+
# @public Since *Async v2*.
|
301
|
+
# @asynchronous May be non-blocking.
|
302
|
+
#
|
303
|
+
# @parameter io [IO] The IO object to wait on.
|
304
|
+
# @parameter events [Integer] The events to wait for, e.g. `IO::READABLE`, `IO::WRITABLE`, etc.
|
305
|
+
# @parameter timeout [Float | Nil] The maximum time to wait, or if nil, indefinitely.
|
233
306
|
def io_wait(io, events, timeout = nil)
|
234
307
|
fiber = Fiber.current
|
235
308
|
|
@@ -238,7 +311,7 @@ module Async
|
|
238
311
|
timer = @timers.after(timeout) do
|
239
312
|
fiber.transfer
|
240
313
|
end
|
241
|
-
elsif timeout =
|
314
|
+
elsif timeout = io.timeout
|
242
315
|
# Otherwise, if we default to the io's timeout, we raise an exception:
|
243
316
|
timer = @timers.after(timeout) do
|
244
317
|
fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become ready!")
|
@@ -251,10 +324,19 @@ module Async
|
|
251
324
|
end
|
252
325
|
|
253
326
|
if ::IO::Event::Support.buffer?
|
327
|
+
# Read from the specified IO into the buffer.
|
328
|
+
#
|
329
|
+
# @public Since *Async v2* and Ruby with `IO::Buffer` support.
|
330
|
+
# @asynchronous May be non-blocking.
|
331
|
+
#
|
332
|
+
# @parameter io [IO] The IO object to read from.
|
333
|
+
# @parameter buffer [IO::Buffer] The buffer to read into.
|
334
|
+
# @parameter length [Integer] The minimum number of bytes to read.
|
335
|
+
# @parameter offset [Integer] The offset within the buffer to read into.
|
254
336
|
def io_read(io, buffer, length, offset = 0)
|
255
337
|
fiber = Fiber.current
|
256
338
|
|
257
|
-
if timeout =
|
339
|
+
if timeout = io.timeout
|
258
340
|
timer = @timers.after(timeout) do
|
259
341
|
fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become readable!")
|
260
342
|
end
|
@@ -266,10 +348,19 @@ module Async
|
|
266
348
|
end
|
267
349
|
|
268
350
|
if RUBY_ENGINE != "ruby" || RUBY_VERSION >= "3.3.1"
|
351
|
+
# Write the specified buffer to the IO.
|
352
|
+
#
|
353
|
+
# @public Since *Async v2* and *Ruby v3.3.1* with `IO::Buffer` support.
|
354
|
+
# @asynchronous May be non-blocking.
|
355
|
+
#
|
356
|
+
# @parameter io [IO] The IO object to write to.
|
357
|
+
# @parameter buffer [IO::Buffer] The buffer to write from.
|
358
|
+
# @parameter length [Integer] The minimum number of bytes to write.
|
359
|
+
# @parameter offset [Integer] The offset within the buffer to write from.
|
269
360
|
def io_write(io, buffer, length, offset = 0)
|
270
361
|
fiber = Fiber.current
|
271
362
|
|
272
|
-
if timeout =
|
363
|
+
if timeout = io.timeout
|
273
364
|
timer = @timers.after(timeout) do
|
274
365
|
fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become writable!")
|
275
366
|
end
|
@@ -282,7 +373,39 @@ module Async
|
|
282
373
|
end
|
283
374
|
end
|
284
375
|
|
376
|
+
# Used to defer stopping the current task until later.
|
377
|
+
class FiberInterrupt
|
378
|
+
# Create a new stop later operation.
|
379
|
+
#
|
380
|
+
# @parameter task [Task] The task to stop later.
|
381
|
+
def initialize(fiber, exception)
|
382
|
+
@fiber = fiber
|
383
|
+
@exception = exception
|
384
|
+
end
|
385
|
+
|
386
|
+
# @returns [Boolean] Whether the task is alive.
|
387
|
+
def alive?
|
388
|
+
@fiber.alive?
|
389
|
+
end
|
390
|
+
|
391
|
+
# Transfer control to the operation - this will stop the task.
|
392
|
+
def transfer
|
393
|
+
# Fiber.blocking{$stderr.puts "FiberInterrupt#transfer(#{@fiber}, #{@exception})"}
|
394
|
+
@fiber.raise(@exception)
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# Raise an exception on the specified fiber, waking up the event loop if necessary.
|
399
|
+
def fiber_interrupt(fiber, exception)
|
400
|
+
# Fiber.blocking{$stderr.puts "fiber_interrupt(#{fiber}, #{exception})"}
|
401
|
+
unblock(nil, FiberInterrupt.new(fiber, exception))
|
402
|
+
end
|
403
|
+
|
285
404
|
# Wait for the specified process ID to exit.
|
405
|
+
#
|
406
|
+
# @public Since *Async v2*.
|
407
|
+
# @asynchronous May be non-blocking.
|
408
|
+
#
|
286
409
|
# @parameter pid [Integer] The process ID to wait for.
|
287
410
|
# @parameter flags [Integer] A bit-mask of flags suitable for `Process::Status.wait`.
|
288
411
|
# @returns [Process::Status] A process status instance.
|
@@ -291,6 +414,19 @@ module Async
|
|
291
414
|
return @selector.process_wait(Fiber.current, pid, flags)
|
292
415
|
end
|
293
416
|
|
417
|
+
# Wait for the specified IOs to become ready for the specified events.
|
418
|
+
#
|
419
|
+
# @public Since *Async v2.25*.
|
420
|
+
# @asynchronous May be non-blocking.
|
421
|
+
def io_select(...)
|
422
|
+
Thread.new do
|
423
|
+
# Don't make unnecessary output, since we will propagate the exception:
|
424
|
+
Thread.current.report_on_exception = false
|
425
|
+
|
426
|
+
::IO.select(...)
|
427
|
+
end.value
|
428
|
+
end
|
429
|
+
|
294
430
|
# Run one iteration of the event loop.
|
295
431
|
#
|
296
432
|
# When terminating the event loop, we already know we are finished. So we don't need to check the task tree. This is a logical requirement because `run_once` ignores transient tasks. For example, a single top level transient task is not enough to keep the reactor running, but during termination we must still process it in order to terminate child tasks.
|
@@ -335,7 +471,10 @@ module Async
|
|
335
471
|
end
|
336
472
|
|
337
473
|
# Run one iteration of the event loop.
|
338
|
-
#
|
474
|
+
#
|
475
|
+
# @public Since *Async v1*.
|
476
|
+
# @asynchronous Must be invoked from blocking (root) fiber.
|
477
|
+
#
|
339
478
|
# @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
|
340
479
|
# @returns [Boolean] Whether there is more work to do.
|
341
480
|
def run_once(timeout = nil)
|
@@ -354,6 +493,7 @@ module Async
|
|
354
493
|
end
|
355
494
|
|
356
495
|
# Checks and clears the interrupted state of the scheduler.
|
496
|
+
#
|
357
497
|
# @returns [Boolean] Whether the reactor has been interrupted.
|
358
498
|
private def interrupted?
|
359
499
|
if @interrupted
|
@@ -368,7 +508,9 @@ module Async
|
|
368
508
|
return false
|
369
509
|
end
|
370
510
|
|
371
|
-
# Stop all children, including transient children
|
511
|
+
# Stop all children, including transient children.
|
512
|
+
#
|
513
|
+
# @public Since *Async v1*.
|
372
514
|
def stop
|
373
515
|
@children&.each do |child|
|
374
516
|
child.stop
|
@@ -387,7 +529,13 @@ module Async
|
|
387
529
|
end
|
388
530
|
end
|
389
531
|
rescue Interrupt => interrupt
|
532
|
+
# 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
533
|
Thread.handle_interrupt(::SignalException => :never) do
|
534
|
+
Console.debug(self) do |buffer|
|
535
|
+
buffer.puts "Scheduler interrupted: #{interrupt.inspect}"
|
536
|
+
self.print_hierarchy(buffer)
|
537
|
+
end
|
538
|
+
|
391
539
|
self.stop
|
392
540
|
end
|
393
541
|
|
@@ -395,55 +543,73 @@ module Async
|
|
395
543
|
end
|
396
544
|
|
397
545
|
# 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
|
-
|
546
|
+
if interrupt
|
547
|
+
Kernel.raise(interrupt)
|
548
|
+
end
|
399
549
|
end
|
400
550
|
|
401
551
|
# Run the reactor until all tasks are finished. Proxies arguments to {#async} immediately before entering the loop, if a block is provided.
|
552
|
+
#
|
553
|
+
# Forwards all parameters to {#async} if a block is given.
|
554
|
+
#
|
555
|
+
# @public Since *Async v1*.
|
556
|
+
#
|
557
|
+
# @yields {|task| ...} The top level task, if a block is given.
|
558
|
+
# @returns [Task] The initial task that was scheduled into the reactor.
|
402
559
|
def run(...)
|
403
560
|
Kernel.raise ClosedError if @selector.nil?
|
404
561
|
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
562
|
+
begin
|
563
|
+
@profiler&.start
|
564
|
+
|
565
|
+
initial_task = self.async(...) if block_given?
|
566
|
+
|
567
|
+
self.run_loop do
|
568
|
+
run_once
|
569
|
+
end
|
570
|
+
|
571
|
+
return initial_task
|
572
|
+
ensure
|
573
|
+
@profiler&.stop
|
409
574
|
end
|
410
|
-
|
411
|
-
return initial_task
|
412
575
|
end
|
413
576
|
|
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.
|
577
|
+
# 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
578
|
#
|
418
|
-
#
|
579
|
+
# @public Since *Async v1*.
|
580
|
+
# @asynchronous May context switch immediately to new task.
|
581
|
+
# @deprecated Use {#run} or {Task#async} instead.
|
419
582
|
#
|
420
583
|
# @yields {|task| ...} Executed within the task.
|
421
584
|
# @returns [Task] The task that was scheduled into the reactor.
|
422
|
-
# @deprecated With no replacement.
|
423
585
|
def async(*arguments, **options, &block)
|
586
|
+
# Since this method is called by `run`, this warning is too excessive:
|
587
|
+
# warn("Async::Scheduler#async is deprecated. Use `run` or `Task#async` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
|
588
|
+
|
424
589
|
Kernel.raise ClosedError if @selector.nil?
|
425
590
|
|
426
591
|
task = Task.new(Task.current? || self, **options, &block)
|
427
592
|
|
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
593
|
task.run(*arguments)
|
436
594
|
|
437
|
-
# Console.debug "Initial execution of task #{fiber} complete (#{result} -> #{fiber.alive?})..."
|
438
595
|
return task
|
439
596
|
end
|
440
597
|
|
598
|
+
# Create a new fiber and return it without starting execution.
|
599
|
+
# @returns [Fiber] The fiber that was created.
|
441
600
|
def fiber(...)
|
442
601
|
return async(...).fiber
|
443
602
|
end
|
444
603
|
|
445
604
|
# 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.
|
605
|
+
#
|
606
|
+
# @public Since *Async v1*.
|
607
|
+
# @asynchronous May raise an exception at any interruption point (e.g. blocking operations).
|
608
|
+
#
|
446
609
|
# @parameter duration [Numeric] The time in seconds, in which the task should complete.
|
610
|
+
# @parameter exception [Class] The exception class to raise.
|
611
|
+
# @parameter message [String] The message to pass to the exception.
|
612
|
+
# @yields {|timeout| ...} The block to execute with a timeout.
|
447
613
|
def with_timeout(duration, exception = TimeoutError, message = "execution expired", &block)
|
448
614
|
fiber = Fiber.current
|
449
615
|
|
@@ -453,13 +619,26 @@ module Async
|
|
453
619
|
end
|
454
620
|
end
|
455
621
|
|
456
|
-
|
622
|
+
if block.arity.zero?
|
623
|
+
yield
|
624
|
+
else
|
625
|
+
yield Timeout.new(@timers, timer)
|
626
|
+
end
|
457
627
|
ensure
|
458
628
|
timer&.cancel!
|
459
629
|
end
|
460
630
|
|
631
|
+
# 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.
|
632
|
+
#
|
633
|
+
# @public Since *Async v1* and *Ruby v3.1*. May be invoked from `Timeout.timeout`.
|
634
|
+
# @asynchronous May raise an exception at any interruption point (e.g. blocking operations).
|
635
|
+
#
|
636
|
+
# @parameter duration [Numeric] The time in seconds, in which the task should complete.
|
637
|
+
# @parameter exception [Class] The exception class to raise.
|
638
|
+
# @parameter message [String] The message to pass to the exception.
|
639
|
+
# @yields {|duration| ...} The block to execute with a timeout.
|
461
640
|
def timeout_after(duration, exception, message, &block)
|
462
|
-
with_timeout(duration, exception, message) do
|
641
|
+
with_timeout(duration, exception, message) do
|
463
642
|
yield duration
|
464
643
|
end
|
465
644
|
end
|
data/lib/async/semaphore.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2024, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
6
|
+
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/stop.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
|
+
|
6
|
+
require "fiber"
|
7
|
+
require "console"
|
8
|
+
|
9
|
+
module Async
|
10
|
+
# Raised when a task is explicitly stopped.
|
11
|
+
class Stop < Exception
|
12
|
+
# Represents the source of the stop operation.
|
13
|
+
class Cause < Exception
|
14
|
+
if RUBY_VERSION >= "3.4"
|
15
|
+
# @returns [Array(Thread::Backtrace::Location)] The backtrace of the caller.
|
16
|
+
def self.backtrace
|
17
|
+
caller_locations(2..-1)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
# @returns [Array(String)] The backtrace of the caller.
|
21
|
+
def self.backtrace
|
22
|
+
caller(2..-1)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Create a new cause of the stop operation, with the given message.
|
27
|
+
#
|
28
|
+
# @parameter message [String] The error message.
|
29
|
+
# @returns [Cause] The cause of the stop operation.
|
30
|
+
def self.for(message = "Task was stopped")
|
31
|
+
instance = self.new(message)
|
32
|
+
instance.set_backtrace(self.backtrace)
|
33
|
+
return instance
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
if RUBY_VERSION < "3.5"
|
38
|
+
# Create a new stop operation.
|
39
|
+
#
|
40
|
+
# This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise}
|
41
|
+
#
|
42
|
+
# @parameter message [String | Hash] The error message or a hash containing the cause.
|
43
|
+
def initialize(message = "Task was stopped")
|
44
|
+
if message.is_a?(Hash)
|
45
|
+
@cause = message[:cause]
|
46
|
+
message = "Task was stopped"
|
47
|
+
end
|
48
|
+
|
49
|
+
super(message)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @returns [Exception] The cause of the stop operation.
|
53
|
+
#
|
54
|
+
# This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise}, we explicitly capture the cause here.
|
55
|
+
def cause
|
56
|
+
super || @cause
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Used to defer stopping the current task until later.
|
61
|
+
class Later
|
62
|
+
# Create a new stop later operation.
|
63
|
+
#
|
64
|
+
# @parameter task [Task] The task to stop later.
|
65
|
+
# @parameter cause [Exception] The cause of the stop operation.
|
66
|
+
def initialize(task, cause = nil)
|
67
|
+
@task = task
|
68
|
+
@cause = cause
|
69
|
+
end
|
70
|
+
|
71
|
+
# @returns [Boolean] Whether the task is alive.
|
72
|
+
def alive?
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
# Transfer control to the operation - this will stop the task.
|
77
|
+
def transfer
|
78
|
+
@task.stop(false, cause: @cause)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|