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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/context/best-practices.md +188 -0
  4. data/context/debugging.md +63 -0
  5. data/context/getting-started.md +177 -0
  6. data/context/index.yaml +29 -0
  7. data/context/scheduler.md +109 -0
  8. data/context/tasks.md +448 -0
  9. data/context/thread-safety.md +651 -0
  10. data/lib/async/barrier.md +1 -2
  11. data/lib/async/barrier.rb +35 -12
  12. data/lib/async/clock.rb +11 -2
  13. data/lib/async/condition.md +1 -1
  14. data/lib/async/condition.rb +18 -34
  15. data/lib/async/console.rb +42 -0
  16. data/lib/async/deadline.rb +70 -0
  17. data/lib/async/idler.rb +2 -1
  18. data/lib/async/limited_queue.rb +13 -0
  19. data/lib/async/list.rb +16 -8
  20. data/lib/async/node.rb +5 -3
  21. data/lib/async/notification.rb +13 -9
  22. data/lib/async/priority_queue.rb +253 -0
  23. data/lib/async/promise.rb +188 -0
  24. data/lib/async/queue.rb +70 -82
  25. data/lib/async/reactor.rb +4 -2
  26. data/lib/async/scheduler.rb +233 -54
  27. data/lib/async/semaphore.rb +3 -3
  28. data/lib/async/stop.rb +82 -0
  29. data/lib/async/task.rb +111 -81
  30. data/lib/async/timeout.rb +88 -0
  31. data/lib/async/variable.rb +15 -4
  32. data/lib/async/version.rb +2 -2
  33. data/lib/async/waiter.rb +6 -1
  34. data/lib/kernel/async.rb +1 -1
  35. data/lib/kernel/sync.rb +14 -5
  36. data/lib/metrics/provider/async/task.rb +20 -0
  37. data/lib/metrics/provider/async.rb +6 -0
  38. data/lib/traces/provider/async/barrier.rb +17 -0
  39. data/lib/traces/provider/async/task.rb +40 -0
  40. data/lib/traces/provider/async.rb +7 -0
  41. data/license.md +8 -1
  42. data/readme.md +50 -7
  43. data/releases.md +357 -0
  44. data.tar.gz.sig +0 -0
  45. metadata +61 -20
  46. metadata.gz.sig +0 -0
  47. data/lib/async/waiter.md +0 -50
  48. data/lib/async/wrapper.rb +0 -65
@@ -1,21 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2024, by Samuel Williams.
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 'clock'
9
- require_relative 'task'
9
+ require_relative "clock"
10
+ require_relative "task"
11
+ require_relative "timeout"
10
12
 
11
- require 'io/event'
13
+ require "io/event"
12
14
 
13
- require 'console'
14
- require 'resolv'
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 `stable-v1`.
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 `stable-v1`.
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 `stable-v1`.
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 `stable-v1`.
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
- # @asynchronous May be non-blocking..
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
- # @asynchronous May be non-blocking..
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
- if IO.method_defined?(:timeout)
223
- private def get_timeout(io)
224
- io.timeout
225
- end
226
- else
227
- private def get_timeout(io)
228
- nil
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 = get_timeout(io)
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 = get_timeout(io)
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 = get_timeout(io)
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
- # Does not handle interrupts.
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, ignoring any signals.
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
- Kernel.raise(interrupt) if interrupt
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
- initial_task = self.async(...) if block_given?
406
-
407
- self.run_loop do
408
- run_once
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
- # This is the main entry point for scheduling asynchronus tasks.
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
- yield timer
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 |timer|
641
+ with_timeout(duration, exception, message) do
463
642
  yield duration
464
643
  end
465
644
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2023, by Samuel Williams.
4
+ # Copyright, 2018-2024, by Samuel Williams.
5
5
 
6
- require_relative 'list'
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 `stable-v1`.
10
+ # @public Since *Async v1*.
11
11
  class Semaphore
12
12
  # @parameter limit [Integer] The maximum number of times the semaphore can be acquired before it blocks.
13
13
  # @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks.
data/lib/async/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