async 2.14.2 → 2.23.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.md +1 -1
- data/lib/async/barrier.rb +5 -5
- data/lib/async/clock.rb +10 -1
- data/lib/async/condition.md +1 -1
- data/lib/async/condition.rb +4 -4
- data/lib/async/console.rb +42 -0
- data/lib/async/idler.rb +4 -2
- data/lib/async/limited_queue.rb +7 -0
- data/lib/async/list.rb +1 -1
- data/lib/async/node.rb +29 -3
- data/lib/async/notification.rb +3 -3
- data/lib/async/queue.rb +17 -7
- data/lib/async/reactor.rb +2 -2
- data/lib/async/scheduler.rb +202 -75
- data/lib/async/semaphore.rb +3 -3
- data/lib/async/task.rb +59 -24
- data/lib/async/variable.rb +11 -6
- data/lib/async/version.rb +1 -1
- data/lib/async/waiter.rb +2 -1
- data/lib/async/worker_pool.rb +182 -0
- data/lib/async/wrapper.rb +3 -1
- data/lib/async.rb +1 -1
- data/lib/kernel/async.rb +4 -2
- data/lib/kernel/sync.rb +12 -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 +2 -1
- data/readme.md +46 -12
- data/releases.md +121 -0
- data.tar.gz.sig +0 -0
- metadata +45 -24
- metadata.gz.sig +0 -0
data/lib/async/scheduler.rb
CHANGED
@@ -5,17 +5,30 @@
|
|
5
5
|
# Copyright, 2020, by Jun Jiang.
|
6
6
|
# Copyright, 2021, by Julien Portalier.
|
7
7
|
|
8
|
-
require_relative
|
9
|
-
require_relative
|
8
|
+
require_relative "clock"
|
9
|
+
require_relative "task"
|
10
|
+
require_relative "worker_pool"
|
10
11
|
|
11
|
-
require
|
12
|
+
require "io/event"
|
12
13
|
|
13
|
-
require
|
14
|
-
require
|
14
|
+
require "console"
|
15
|
+
require "resolv"
|
15
16
|
|
16
17
|
module Async
|
18
|
+
begin
|
19
|
+
require "fiber/profiler"
|
20
|
+
Profiler = Fiber::Profiler
|
21
|
+
rescue LoadError
|
22
|
+
# Fiber::Profiler is not available.
|
23
|
+
Profiler = nil
|
24
|
+
end
|
25
|
+
|
17
26
|
# Handles scheduling of fibers. Implements the fiber scheduler interface.
|
18
27
|
class Scheduler < Node
|
28
|
+
WORKER_POOL = ENV.fetch("ASYNC_SCHEDULER_WORKER_POOL", nil).then do |value|
|
29
|
+
value == "true" ? true : nil
|
30
|
+
end
|
31
|
+
|
19
32
|
# Raised when an operation is attempted on a closed scheduler.
|
20
33
|
class ClosedError < RuntimeError
|
21
34
|
# Create a new error.
|
@@ -27,20 +40,22 @@ module Async
|
|
27
40
|
end
|
28
41
|
|
29
42
|
# Whether the fiber scheduler is supported.
|
30
|
-
# @public Since
|
43
|
+
# @public Since *Async v1*.
|
31
44
|
def self.supported?
|
32
45
|
true
|
33
46
|
end
|
34
47
|
|
35
48
|
# Create a new scheduler.
|
36
49
|
#
|
37
|
-
# @public Since
|
50
|
+
# @public Since *Async v1*.
|
38
51
|
# @parameter parent [Node | Nil] The parent node to use for task hierarchy.
|
39
52
|
# @parameter selector [IO::Event::Selector] The selector to use for event handling.
|
40
|
-
def initialize(parent = nil, selector: nil)
|
53
|
+
def initialize(parent = nil, selector: nil, profiler: Profiler&.default, worker_pool: WORKER_POOL)
|
41
54
|
super(parent)
|
42
55
|
|
43
56
|
@selector = selector || ::IO::Event::Selector.new(Fiber.current)
|
57
|
+
@profiler = profiler
|
58
|
+
|
44
59
|
@interrupted = false
|
45
60
|
|
46
61
|
@blocked = 0
|
@@ -49,9 +64,19 @@ module Async
|
|
49
64
|
@idle_time = 0.0
|
50
65
|
|
51
66
|
@timers = ::IO::Event::Timers.new
|
67
|
+
if worker_pool == true
|
68
|
+
@worker_pool = WorkerPool.new
|
69
|
+
else
|
70
|
+
@worker_pool = worker_pool
|
71
|
+
end
|
72
|
+
|
73
|
+
if @worker_pool
|
74
|
+
self.singleton_class.prepend(WorkerPool::BlockingOperationWait)
|
75
|
+
end
|
52
76
|
end
|
53
77
|
|
54
78
|
# Compute the scheduler load according to the busy and idle times that are updated by the run loop.
|
79
|
+
#
|
55
80
|
# @returns [Float] The load of the scheduler. 0.0 means no load, 1.0 means fully loaded or over-loaded.
|
56
81
|
def load
|
57
82
|
total_time = @busy_time + @idle_time
|
@@ -71,52 +96,56 @@ module Async
|
|
71
96
|
return @busy_time / total_time
|
72
97
|
end
|
73
98
|
end
|
74
|
-
|
99
|
+
|
75
100
|
# Invoked when the fiber scheduler is being closed.
|
76
101
|
#
|
77
102
|
# Executes the run loop until all tasks are finished, then closes the scheduler.
|
78
|
-
def scheduler_close
|
103
|
+
def scheduler_close(error = $!)
|
79
104
|
# If the execution context (thread) was handling an exception, we want to exit as quickly as possible:
|
80
|
-
unless
|
105
|
+
unless error
|
81
106
|
self.run
|
82
107
|
end
|
83
108
|
ensure
|
84
109
|
self.close
|
85
110
|
end
|
86
111
|
|
87
|
-
# Terminate
|
112
|
+
# Terminate all child tasks.
|
88
113
|
def terminate
|
89
|
-
|
90
|
-
|
114
|
+
# If that doesn't work, take more serious action:
|
115
|
+
@children&.each do |child|
|
116
|
+
child.terminate
|
91
117
|
end
|
118
|
+
|
119
|
+
return @children.nil?
|
92
120
|
end
|
93
121
|
|
94
122
|
# Terminate all child tasks and close the scheduler.
|
95
|
-
# @public Since
|
123
|
+
# @public Since *Async v1*.
|
96
124
|
def close
|
97
|
-
|
98
|
-
|
99
|
-
|
125
|
+
self.run_loop do
|
126
|
+
until self.terminate
|
127
|
+
self.run_once!
|
128
|
+
end
|
100
129
|
end
|
101
130
|
|
102
131
|
Kernel.raise "Closing scheduler with blocked operations!" if @blocked > 0
|
103
|
-
|
104
|
-
# We depend on GVL for consistency:
|
105
|
-
# @guard.synchronize do
|
106
|
-
|
132
|
+
ensure
|
107
133
|
# We want `@selector = nil` to be a visible side effect from this point forward, specifically in `#interrupt` and `#unblock`. If the selector is closed, then we don't want to push any fibers to it.
|
108
134
|
selector = @selector
|
109
135
|
@selector = nil
|
110
136
|
|
111
137
|
selector&.close
|
112
138
|
|
113
|
-
|
139
|
+
worker_pool = @worker_pool
|
140
|
+
@worker_pool = nil
|
141
|
+
|
142
|
+
worker_pool&.close
|
114
143
|
|
115
144
|
consume
|
116
145
|
end
|
117
146
|
|
118
147
|
# @returns [Boolean] Whether the scheduler has been closed.
|
119
|
-
# @public Since
|
148
|
+
# @public Since *Async v1*.
|
120
149
|
def closed?
|
121
150
|
@selector.nil?
|
122
151
|
end
|
@@ -168,7 +197,12 @@ module Async
|
|
168
197
|
end
|
169
198
|
|
170
199
|
# 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.
|
200
|
+
#
|
201
|
+
# @public Since *Async v2*.
|
171
202
|
# @asynchronous May only be called on same thread as fiber scheduler.
|
203
|
+
#
|
204
|
+
# @parameter blocker [Object] The object that is blocking the fiber.
|
205
|
+
# @parameter timeout [Float | Nil] The maximum time to block, or if nil, indefinitely.
|
172
206
|
def block(blocker, timeout)
|
173
207
|
# $stderr.puts "block(#{blocker}, #{Fiber.current}, #{timeout})"
|
174
208
|
fiber = Fiber.current
|
@@ -191,7 +225,13 @@ module Async
|
|
191
225
|
timer&.cancel!
|
192
226
|
end
|
193
227
|
|
228
|
+
# Unblock a fiber that was previously blocked.
|
229
|
+
#
|
230
|
+
# @public Since *Async v2* and *Ruby v3.1*.
|
194
231
|
# @asynchronous May be called from any thread.
|
232
|
+
#
|
233
|
+
# @parameter blocker [Object] The object that was blocking the fiber.
|
234
|
+
# @parameter fiber [Fiber] The fiber to unblock.
|
195
235
|
def unblock(blocker, fiber)
|
196
236
|
# $stderr.puts "unblock(#{blocker}, #{fiber})"
|
197
237
|
|
@@ -202,7 +242,12 @@ module Async
|
|
202
242
|
end
|
203
243
|
end
|
204
244
|
|
205
|
-
#
|
245
|
+
# Sleep for the specified duration.
|
246
|
+
#
|
247
|
+
# @public Since *Async v2* and *Ruby v3.1*.
|
248
|
+
# @asynchronous May be non-blocking.
|
249
|
+
#
|
250
|
+
# @parameter duration [Numeric | Nil] The time in seconds to sleep, or if nil, indefinitely.
|
206
251
|
def kernel_sleep(duration = nil)
|
207
252
|
if duration
|
208
253
|
self.block(nil, duration)
|
@@ -211,7 +256,12 @@ module Async
|
|
211
256
|
end
|
212
257
|
end
|
213
258
|
|
214
|
-
#
|
259
|
+
# Resolve the address of the given hostname.
|
260
|
+
#
|
261
|
+
# @public Since *Async v2*.
|
262
|
+
# @asynchronous May be non-blocking.
|
263
|
+
#
|
264
|
+
# @parameter hostname [String] The hostname to resolve.
|
215
265
|
def address_resolve(hostname)
|
216
266
|
# On some platforms, hostnames may contain a device-specific suffix (e.g. %en0). We need to strip this before resolving.
|
217
267
|
# See <https://github.com/socketry/async/issues/180> for more details.
|
@@ -219,7 +269,6 @@ module Async
|
|
219
269
|
::Resolv.getaddresses(hostname)
|
220
270
|
end
|
221
271
|
|
222
|
-
|
223
272
|
if IO.method_defined?(:timeout)
|
224
273
|
private def get_timeout(io)
|
225
274
|
io.timeout
|
@@ -230,7 +279,14 @@ module Async
|
|
230
279
|
end
|
231
280
|
end
|
232
281
|
|
233
|
-
#
|
282
|
+
# Wait for the specified IO to become ready for the specified events.
|
283
|
+
#
|
284
|
+
# @public Since *Async v2*.
|
285
|
+
# @asynchronous May be non-blocking.
|
286
|
+
#
|
287
|
+
# @parameter io [IO] The IO object to wait on.
|
288
|
+
# @parameter events [Integer] The events to wait for, e.g. `IO::READABLE`, `IO::WRITABLE`, etc.
|
289
|
+
# @parameter timeout [Float | Nil] The maximum time to wait, or if nil, indefinitely.
|
234
290
|
def io_wait(io, events, timeout = nil)
|
235
291
|
fiber = Fiber.current
|
236
292
|
|
@@ -242,7 +298,7 @@ module Async
|
|
242
298
|
elsif timeout = get_timeout(io)
|
243
299
|
# Otherwise, if we default to the io's timeout, we raise an exception:
|
244
300
|
timer = @timers.after(timeout) do
|
245
|
-
fiber.raise(::IO::TimeoutError, "Timeout while waiting for IO to become ready!")
|
301
|
+
fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become ready!")
|
246
302
|
end
|
247
303
|
end
|
248
304
|
|
@@ -252,12 +308,21 @@ module Async
|
|
252
308
|
end
|
253
309
|
|
254
310
|
if ::IO::Event::Support.buffer?
|
311
|
+
# Read from the specified IO into the buffer.
|
312
|
+
#
|
313
|
+
# @public Since *Async v2* and Ruby with `IO::Buffer` support.
|
314
|
+
# @asynchronous May be non-blocking.
|
315
|
+
#
|
316
|
+
# @parameter io [IO] The IO object to read from.
|
317
|
+
# @parameter buffer [IO::Buffer] The buffer to read into.
|
318
|
+
# @parameter length [Integer] The minimum number of bytes to read.
|
319
|
+
# @parameter offset [Integer] The offset within the buffer to read into.
|
255
320
|
def io_read(io, buffer, length, offset = 0)
|
256
321
|
fiber = Fiber.current
|
257
322
|
|
258
323
|
if timeout = get_timeout(io)
|
259
324
|
timer = @timers.after(timeout) do
|
260
|
-
fiber.raise(::IO::TimeoutError, "Timeout while waiting for IO to become readable!")
|
325
|
+
fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become readable!")
|
261
326
|
end
|
262
327
|
end
|
263
328
|
|
@@ -267,12 +332,21 @@ module Async
|
|
267
332
|
end
|
268
333
|
|
269
334
|
if RUBY_ENGINE != "ruby" || RUBY_VERSION >= "3.3.1"
|
335
|
+
# Write the specified buffer to the IO.
|
336
|
+
#
|
337
|
+
# @public Since *Async v2* and *Ruby v3.3.1* with `IO::Buffer` support.
|
338
|
+
# @asynchronous May be non-blocking.
|
339
|
+
#
|
340
|
+
# @parameter io [IO] The IO object to write to.
|
341
|
+
# @parameter buffer [IO::Buffer] The buffer to write from.
|
342
|
+
# @parameter length [Integer] The minimum number of bytes to write.
|
343
|
+
# @parameter offset [Integer] The offset within the buffer to write from.
|
270
344
|
def io_write(io, buffer, length, offset = 0)
|
271
345
|
fiber = Fiber.current
|
272
346
|
|
273
347
|
if timeout = get_timeout(io)
|
274
348
|
timer = @timers.after(timeout) do
|
275
|
-
fiber.raise(::IO::TimeoutError, "Timeout while waiting for IO to become writable!")
|
349
|
+
fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become writable!")
|
276
350
|
end
|
277
351
|
end
|
278
352
|
|
@@ -284,6 +358,10 @@ module Async
|
|
284
358
|
end
|
285
359
|
|
286
360
|
# Wait for the specified process ID to exit.
|
361
|
+
#
|
362
|
+
# @public Since *Async v2*.
|
363
|
+
# @asynchronous May be non-blocking.
|
364
|
+
#
|
287
365
|
# @parameter pid [Integer] The process ID to wait for.
|
288
366
|
# @parameter flags [Integer] A bit-mask of flags suitable for `Process::Status.wait`.
|
289
367
|
# @returns [Process::Status] A process status instance.
|
@@ -292,28 +370,13 @@ module Async
|
|
292
370
|
return @selector.process_wait(Fiber.current, pid, flags)
|
293
371
|
end
|
294
372
|
|
295
|
-
# Run one iteration of the event loop.
|
296
|
-
# Does not handle interrupts.
|
297
|
-
# @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
|
298
|
-
# @returns [Boolean] Whether there is more work to do.
|
299
|
-
def run_once(timeout = nil)
|
300
|
-
Kernel::raise "Running scheduler on non-blocking fiber!" unless Fiber.blocking?
|
301
|
-
|
302
|
-
# If we are finished, we stop the task tree and exit:
|
303
|
-
if self.finished?
|
304
|
-
return false
|
305
|
-
end
|
306
|
-
|
307
|
-
return run_once!(timeout)
|
308
|
-
end
|
309
|
-
|
310
373
|
# Run one iteration of the event loop.
|
311
374
|
#
|
312
375
|
# 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.
|
313
376
|
#
|
314
377
|
# @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
|
315
378
|
# @returns [Boolean] Whether there is more work to do.
|
316
|
-
private def run_once!(timeout =
|
379
|
+
private def run_once!(timeout = nil)
|
317
380
|
start_time = Async::Clock.now
|
318
381
|
|
319
382
|
interval = @timers.wait_interval
|
@@ -350,7 +413,30 @@ module Async
|
|
350
413
|
return true
|
351
414
|
end
|
352
415
|
|
416
|
+
# Run one iteration of the event loop.
|
417
|
+
#
|
418
|
+
# @public Since *Async v1*.
|
419
|
+
# @asynchronous Must be invoked from blocking (root) fiber.
|
420
|
+
#
|
421
|
+
# @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
|
422
|
+
# @returns [Boolean] Whether there is more work to do.
|
423
|
+
def run_once(timeout = nil)
|
424
|
+
Kernel.raise "Running scheduler on non-blocking fiber!" unless Fiber.blocking?
|
425
|
+
|
426
|
+
if self.finished?
|
427
|
+
self.stop
|
428
|
+
end
|
429
|
+
|
430
|
+
# If we are finished, we stop the task tree and exit:
|
431
|
+
if @children.nil?
|
432
|
+
return false
|
433
|
+
end
|
434
|
+
|
435
|
+
return run_once!(timeout)
|
436
|
+
end
|
437
|
+
|
353
438
|
# Checks and clears the interrupted state of the scheduler.
|
439
|
+
#
|
354
440
|
# @returns [Boolean] Whether the reactor has been interrupted.
|
355
441
|
private def interrupted?
|
356
442
|
if @interrupted
|
@@ -365,26 +451,34 @@ module Async
|
|
365
451
|
return false
|
366
452
|
end
|
367
453
|
|
368
|
-
#
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
454
|
+
# Stop all children, including transient children.
|
455
|
+
#
|
456
|
+
# @public Since *Async v1*.
|
457
|
+
def stop
|
458
|
+
@children&.each do |child|
|
459
|
+
child.stop
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
private def run_loop(&block)
|
373
464
|
interrupt = nil
|
374
465
|
|
375
466
|
begin
|
376
467
|
# In theory, we could use Exception here to be a little bit safer, but we've only shown the case for SignalException to be a problem, so let's not over-engineer this.
|
377
468
|
Thread.handle_interrupt(::SignalException => :never) do
|
378
|
-
|
379
|
-
# If we are interrupted, we need to exit:
|
380
|
-
break if self.interrupted?
|
381
|
-
|
469
|
+
until self.interrupted?
|
382
470
|
# If we are finished, we need to exit:
|
383
|
-
break unless
|
471
|
+
break unless yield
|
384
472
|
end
|
385
473
|
end
|
386
474
|
rescue Interrupt => interrupt
|
475
|
+
# 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.
|
387
476
|
Thread.handle_interrupt(::SignalException => :never) do
|
477
|
+
Console.debug(self) do |buffer|
|
478
|
+
buffer.puts "Scheduler interrupted: #{interrupt.inspect}"
|
479
|
+
self.print_hierarchy(buffer)
|
480
|
+
end
|
481
|
+
|
388
482
|
self.stop
|
389
483
|
end
|
390
484
|
|
@@ -392,37 +486,54 @@ module Async
|
|
392
486
|
end
|
393
487
|
|
394
488
|
# 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.
|
395
|
-
|
489
|
+
if interrupt
|
490
|
+
Kernel.raise(interrupt)
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
# Run the reactor until all tasks are finished. Proxies arguments to {#async} immediately before entering the loop, if a block is provided.
|
495
|
+
#
|
496
|
+
# Forwards all parameters to {#async} if a block is given.
|
497
|
+
#
|
498
|
+
# @public Since *Async v1*.
|
499
|
+
#
|
500
|
+
# @yields {|task| ...} The top level task, if a block is given.
|
501
|
+
# @returns [Task] The initial task that was scheduled into the reactor.
|
502
|
+
def run(...)
|
503
|
+
Kernel.raise ClosedError if @selector.nil?
|
396
504
|
|
397
|
-
|
398
|
-
|
399
|
-
|
505
|
+
begin
|
506
|
+
@profiler&.start
|
507
|
+
|
508
|
+
initial_task = self.async(...) if block_given?
|
509
|
+
|
510
|
+
self.run_loop do
|
511
|
+
run_once
|
512
|
+
end
|
513
|
+
|
514
|
+
return initial_task
|
515
|
+
ensure
|
516
|
+
@profiler&.stop
|
517
|
+
end
|
400
518
|
end
|
401
519
|
|
402
|
-
# Start an asynchronous task within the specified reactor. The task will be
|
403
|
-
# executed until the first blocking call, at which point it will yield and
|
404
|
-
# and this method will return.
|
520
|
+
# 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.
|
405
521
|
#
|
406
|
-
#
|
522
|
+
# @public Since *Async v1*.
|
523
|
+
# @asynchronous May context switch immediately to new task.
|
524
|
+
# @deprecated Use {#run} or {Task#async} instead.
|
407
525
|
#
|
408
526
|
# @yields {|task| ...} Executed within the task.
|
409
527
|
# @returns [Task] The task that was scheduled into the reactor.
|
410
|
-
# @deprecated With no replacement.
|
411
528
|
def async(*arguments, **options, &block)
|
412
|
-
|
529
|
+
# warn "Async::Scheduler#async is deprecated. Use `run` or `Task#async` instead.", uplevel: 1, category: :deprecated
|
530
|
+
|
531
|
+
Kernel.raise ClosedError if @selector.nil?
|
413
532
|
|
414
533
|
task = Task.new(Task.current? || self, **options, &block)
|
415
534
|
|
416
|
-
# I want to take a moment to explain the logic of this.
|
417
|
-
# When calling an async block, we deterministically execute it until the
|
418
|
-
# first blocking operation. We don't *have* to do this - we could schedule
|
419
|
-
# it for later execution, but it's useful to:
|
420
|
-
# - Fail at the point of the method call where possible.
|
421
|
-
# - Execute determinstically where possible.
|
422
|
-
# - Avoid scheduler overhead if no blocking operation is performed.
|
423
535
|
task.run(*arguments)
|
424
536
|
|
425
|
-
# Console.debug "Initial execution of task #{fiber} complete (#{result} -> #{fiber.alive?})..."
|
426
537
|
return task
|
427
538
|
end
|
428
539
|
|
@@ -431,7 +542,14 @@ module Async
|
|
431
542
|
end
|
432
543
|
|
433
544
|
# 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.
|
545
|
+
#
|
546
|
+
# @public Since *Async v1*.
|
547
|
+
# @asynchronous May raise an exception at any interruption point (e.g. blocking operations).
|
548
|
+
#
|
434
549
|
# @parameter duration [Numeric] The time in seconds, in which the task should complete.
|
550
|
+
# @parameter exception [Class] The exception class to raise.
|
551
|
+
# @parameter message [String] The message to pass to the exception.
|
552
|
+
# @yields {|duration| ...} The block to execute with a timeout.
|
435
553
|
def with_timeout(duration, exception = TimeoutError, message = "execution expired", &block)
|
436
554
|
fiber = Fiber.current
|
437
555
|
|
@@ -446,6 +564,15 @@ module Async
|
|
446
564
|
timer&.cancel!
|
447
565
|
end
|
448
566
|
|
567
|
+
# 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.
|
568
|
+
#
|
569
|
+
# @public Since *Async v1* and *Ruby v3.1*. May be invoked from `Timeout.timeout`.
|
570
|
+
# @asynchronous May raise an exception at any interruption point (e.g. blocking operations).
|
571
|
+
#
|
572
|
+
# @parameter duration [Numeric] The time in seconds, in which the task should complete.
|
573
|
+
# @parameter exception [Class] The exception class to raise.
|
574
|
+
# @parameter message [String] The message to pass to the exception.
|
575
|
+
# @yields {|duration| ...} The block to execute with a timeout.
|
449
576
|
def timeout_after(duration, exception, message, &block)
|
450
577
|
with_timeout(duration, exception, message) do |timer|
|
451
578
|
yield duration
|
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/task.rb
CHANGED
@@ -7,11 +7,11 @@
|
|
7
7
|
# Copyright, 2020, by Patrik Wenger.
|
8
8
|
# Copyright, 2023, by Math Ieu.
|
9
9
|
|
10
|
-
require
|
11
|
-
require
|
10
|
+
require "fiber"
|
11
|
+
require "console"
|
12
12
|
|
13
|
-
require_relative
|
14
|
-
require_relative
|
13
|
+
require_relative "node"
|
14
|
+
require_relative "condition"
|
15
15
|
|
16
16
|
Fiber.attr_accessor :async_task
|
17
17
|
|
@@ -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
|
@@ -67,6 +67,13 @@ module Async
|
|
67
67
|
Fiber.scheduler.transfer
|
68
68
|
end
|
69
69
|
|
70
|
+
# Run the given block of code in a task, asynchronously, in the given scheduler.
|
71
|
+
def self.run(scheduler, *arguments, **options, &block)
|
72
|
+
self.new(scheduler, **options, &block).tap do |task|
|
73
|
+
task.run(*arguments)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
70
77
|
# Create a new task.
|
71
78
|
# @parameter reactor [Reactor] the reactor this task will run within.
|
72
79
|
# @parameter parent [Task] the parent task.
|
@@ -174,9 +181,12 @@ module Async
|
|
174
181
|
@status == :completed
|
175
182
|
end
|
176
183
|
|
177
|
-
|
184
|
+
# Alias for {#completed?}.
|
185
|
+
def complete?
|
186
|
+
self.completed?
|
187
|
+
end
|
178
188
|
|
179
|
-
# @attribute [Symbol] The status of the execution of the
|
189
|
+
# @attribute [Symbol] The status of the execution of the task, one of `:initialized`, `:running`, `:complete`, `:stopped` or `:failed`.
|
180
190
|
attr :status
|
181
191
|
|
182
192
|
# Begin the execution of the task.
|
@@ -191,9 +201,7 @@ module Async
|
|
191
201
|
rescue => error
|
192
202
|
# 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.
|
193
203
|
if @finished.nil?
|
194
|
-
|
195
|
-
# else
|
196
|
-
# Console::Event::Failure.for(error).emit(self, severity: :debug)
|
204
|
+
warn(self, "Task may have ended with unhandled exception.", exception: error)
|
197
205
|
end
|
198
206
|
|
199
207
|
raise
|
@@ -205,6 +213,10 @@ module Async
|
|
205
213
|
|
206
214
|
# Run an asynchronous task as a child of the current task.
|
207
215
|
#
|
216
|
+
# @public Since *Async v1*.
|
217
|
+
# @asynchronous May context switch immediately to the new task.
|
218
|
+
#
|
219
|
+
# @yields {|task| ...} in the context of the new task.
|
208
220
|
# @raises [FinishedError] If the task has already finished.
|
209
221
|
# @returns [Task] The child task.
|
210
222
|
def async(*arguments, **options, &block)
|
@@ -212,6 +224,13 @@ module Async
|
|
212
224
|
|
213
225
|
task = Task.new(self, **options, &block)
|
214
226
|
|
227
|
+
# 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:
|
228
|
+
#
|
229
|
+
# - Fail at the point of the method call where possible.
|
230
|
+
# - Execute determinstically where possible.
|
231
|
+
# - Avoid scheduler overhead if no blocking operation is performed.
|
232
|
+
#
|
233
|
+
# There are different strategies (greedy vs non-greedy). We are currently using a greedy strategy.
|
215
234
|
task.run(*arguments)
|
216
235
|
|
217
236
|
return task
|
@@ -253,15 +272,18 @@ module Async
|
|
253
272
|
return stopped!
|
254
273
|
end
|
255
274
|
|
256
|
-
# If we are deferring stop...
|
257
|
-
if @defer_stop == false
|
258
|
-
# Don't stop now... but update the state so we know we need to stop later.
|
259
|
-
@defer_stop = true
|
260
|
-
return false
|
261
|
-
end
|
262
|
-
|
263
275
|
# If the fiber is alive, we need to stop it:
|
264
276
|
if @fiber&.alive?
|
277
|
+
# As the task is now exiting, we want to ensure the event loop continues to execute until the task finishes.
|
278
|
+
self.transient = false
|
279
|
+
|
280
|
+
# If we are deferring stop...
|
281
|
+
if @defer_stop == false
|
282
|
+
# Don't stop now... but update the state so we know we need to stop later.
|
283
|
+
@defer_stop = true
|
284
|
+
return false
|
285
|
+
end
|
286
|
+
|
265
287
|
if self.current?
|
266
288
|
# If the fiber is current, and later is `true`, we need to schedule the fiber to be stopped later, as it's currently invoking `stop`:
|
267
289
|
if later
|
@@ -294,26 +316,30 @@ module Async
|
|
294
316
|
# If stop is invoked a second time, it will be immediately executed.
|
295
317
|
#
|
296
318
|
# @yields {} The block of code to execute.
|
297
|
-
# @public Since
|
319
|
+
# @public Since *Async v1*.
|
298
320
|
def defer_stop
|
299
321
|
# Tri-state variable for controlling stop:
|
300
322
|
# - nil: defer_stop has not been called.
|
301
323
|
# - false: defer_stop has been called and we are not stopping.
|
302
324
|
# - true: defer_stop has been called and we will stop when exiting the block.
|
303
325
|
if @defer_stop.nil?
|
304
|
-
# If we are not deferring stop already, we can defer it now:
|
305
|
-
@defer_stop = false
|
306
|
-
|
307
326
|
begin
|
327
|
+
# If we are not deferring stop already, we can defer it now:
|
328
|
+
@defer_stop = false
|
329
|
+
|
308
330
|
yield
|
309
331
|
rescue Stop
|
310
332
|
# If we are exiting due to a stop, we shouldn't try to invoke stop again:
|
311
333
|
@defer_stop = nil
|
312
334
|
raise
|
313
335
|
ensure
|
336
|
+
defer_stop = @defer_stop
|
337
|
+
|
338
|
+
# We need to ensure the state is reset before we exit the block:
|
339
|
+
@defer_stop = nil
|
340
|
+
|
314
341
|
# If we were asked to stop, we should do so now:
|
315
|
-
if
|
316
|
-
@defer_stop = nil
|
342
|
+
if defer_stop
|
317
343
|
raise Stop, "Stopping current task (was deferred)!"
|
318
344
|
end
|
319
345
|
end
|
@@ -323,6 +349,11 @@ module Async
|
|
323
349
|
end
|
324
350
|
end
|
325
351
|
|
352
|
+
# @returns [Boolean] Whether stop has been deferred.
|
353
|
+
def stop_deferred?
|
354
|
+
@defer_stop
|
355
|
+
end
|
356
|
+
|
326
357
|
# Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
|
327
358
|
# @returns [Task]
|
328
359
|
# @raises[RuntimeError] If task was not {set!} for the current fiber.
|
@@ -343,6 +374,10 @@ module Async
|
|
343
374
|
|
344
375
|
private
|
345
376
|
|
377
|
+
def warn(...)
|
378
|
+
Console.warn(...)
|
379
|
+
end
|
380
|
+
|
346
381
|
# Finish the current task, moving any children to the parent.
|
347
382
|
def finish!
|
348
383
|
# Don't hold references to the fiber or block after the task has finished:
|