async icon indicating copy to clipboard operation
async copied to clipboard

Add support for `stop(cause:)`.

Open ioquatix opened this issue 6 months ago • 3 comments

Add cause: to Async::Task#stop(cause:) so that extra information about the reason for stopping can be provided. It must be an exception suitable for raise Async::Stop, cause: cause.

Types of Changes

  • New feature.

Contribution

ioquatix avatar May 16 '25 02:05 ioquatix

I found an issue, Fiber#raise does not seem to support cause: keyword argument correctly. I need to investigate.

ioquatix avatar May 16 '25 06:05 ioquatix

I test w/ a sleep, and sending SIGINT to the worker sleeping. Here's what I got:

Async::Stop: {cause: #<Async::Stop: {cause: #<Async::Stop: {cause: #<Async::Stop: {cause: #<Interrupt: Interrupt>}>}>}>}
/Users/ma/.gem/ruby/3.4.4/bundler/gems/async-53670d16cb43/lib/async/scheduler.rb:221:in 'IO::Event::Selector::KQueue#transfer'
	/Users/ma/.gem/ruby/3.4.4/bundler/gems/async-53670d16cb43/lib/async/scheduler.rb:221:in 'Async::Scheduler#block'
	/Users/ma/.gem/ruby/3.4.4/bundler/gems/async-53670d16cb43/lib/async/scheduler.rb:254:in 'Async::Scheduler#kernel_sleep'

Seems there is too much nesting.

macournoyer avatar May 16 '25 19:05 macournoyer

Two issues:

  • https://bugs.ruby-lang.org/issues/21359
  • https://bugs.ruby-lang.org/issues/21360

ioquatix avatar May 22 '25 09:05 ioquatix

@macournoyer I was able to understand the cause of the output you are seeing.

fiber = Fiber.new do
  while true
    begin
      Fiber.yield
    rescue Exception => error
      puts "error: #{error.inspect}", "error.cause: #{error.cause.inspect}"
    end
  end
end

fiber.resume

# Example of raising an exception with a cause:
fiber.raise StandardError.new("boom"), cause: StandardError.new("cause")

On 3.4, the cause is not correctly attributed to the error and instead ends up being a keyword argument.

> chruby 3.4
> ruby ./test.rb
error: #<StandardError: {cause: #<StandardError: cause>}>
error.cause: nil

On ruby-head, the cause is correctly attributed to the error (including this PR):

> chruby ruby-head
> ruby ./test.rb
error: #<StandardError: boom>
error.cause: #<StandardError: cause>

Deeply nested cause Async::Stop: {cause: #<Async::Stop: {cause: #<Async::Stop: {cause: #<Async::Stop: {cause: #<Interrupt: Interrupt>}>}>}>} is an artefact of several tasks stopping in sequence. The causality chain is not clearly printed but in 3.5+ it should be.

samuel-williams-shopify avatar Jul 22 '25 22:07 samuel-williams-shopify

Another before/after example:

require_relative "lib/async"

Async do
  sleep
ensure
  Console.error(self, "Task exiting", $!)
end

3.4.4 on main (without this PR)

> chruby 3.4.4
> ruby ./test.rb
^C  0.0s    error: Object [oid=0x20] [ec=0x28] [pid=199670] [2025-07-23 18:23:35 +1200]
               | Task exiting
               | Async::Stop
               |   Async::Stop: Async::Stop
               |   → lib/async/scheduler.rb:191 in 'IO::Event::Selector::URing#transfer'
               |     lib/async/scheduler.rb:191 in 'Async::Scheduler#transfer'
               |     lib/async/scheduler.rb:281 in 'Async::Scheduler#kernel_sleep'
               |     ./test.rb:4 in 'Kernel#sleep'
               |     ./test.rb:4 in 'block in <main>'
               |     lib/async/task.rb:205 in 'block in Async::Task#run'
               |     lib/async/task.rb:443 in 'block in Async::Task#schedule'
/home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453:in 'IO::Event::Selector::URing#select': Interrupt
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453:in 'Async::Scheduler#run_once!'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:492:in 'Async::Scheduler#run_once'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:568:in 'block in Async::Scheduler#run'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:528:in 'block in Async::Scheduler#run_loop'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525:in 'Thread.handle_interrupt'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525:in 'Async::Scheduler#run_loop'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:567:in 'Async::Scheduler#run'
	from /home/samuel/Developer/socketry/async/lib/kernel/async.rb:34:in 'Kernel#Async'
  • The error encountered from within the task (Async::Stop) has no cause attached.

3.4.4 + this PR

> chruby 3.4.4
> ruby ./test.rb
^C  0.0s    error: Object [oid=0x20] [ec=0x28] [pid=192112] [2025-07-23 18:12:29 +1200]
               | Task exiting
               | {cause: Interrupt}
               |   Async::Stop: {cause: Interrupt}
               |   → lib/async/scheduler.rb:191 in 'IO::Event::Selector::URing#transfer'
               |     lib/async/scheduler.rb:191 in 'Async::Scheduler#transfer'
               |     lib/async/scheduler.rb:281 in 'Async::Scheduler#kernel_sleep'
               |     ./test.rb:4 in 'Kernel#sleep'
               |     ./test.rb:4 in 'block in <main>'
               |     lib/async/task.rb:237 in 'block in Async::Task#run'
               |     lib/async/task.rb:481 in 'block in Async::Task#schedule'
/home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453:in 'IO::Event::Selector::URing#select': Interrupt
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453:in 'Async::Scheduler#run_once!'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:492:in 'Async::Scheduler#run_once'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:568:in 'block in Async::Scheduler#run'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:528:in 'block in Async::Scheduler#run_loop'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525:in 'Thread.handle_interrupt'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525:in 'Async::Scheduler#run_loop'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:567:in 'Async::Scheduler#run'
	from /home/samuel/Developer/socketry/async/lib/kernel/async.rb:34:in 'Kernel#Async'
	from ./test.rb:3:in '<main>'
  • The error encountered from within the task has an odd keyword argument attached.

HEAD + this PR

> chruby ruby-head
> ruby ./test.rb
^C  0.0s    error: Object [oid=0x20] [ec=0x28] [pid=192193] [2025-07-23 18:12:34 +1200]
               | Task exiting
               | Task was stopped
               |   Async::Stop: Task was stopped
               |   → lib/async/scheduler.rb:191 in 'IO::Event::Selector::URing#transfer'
               |     lib/async/scheduler.rb:191 in 'Async::Scheduler#transfer'
               |     lib/async/scheduler.rb:281 in 'Async::Scheduler#kernel_sleep'
               |     ./test.rb:4 in 'Kernel#sleep'
               |     ./test.rb:4 in 'block in <main>'
               |     lib/async/task.rb:237 in 'block in Async::Task#run'
               |     lib/async/task.rb:481 in 'block in Async::Task#schedule'
               |   Caused by Interrupt: Interrupt
               |   → /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453 in 'IO::Event::Selector::URing#select'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453 in 'Async::Scheduler#run_once!'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:492 in 'Async::Scheduler#run_once'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:568 in 'block in Async::Scheduler#run'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:528 in 'block in Async::Scheduler#run_loop'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525 in 'Thread.handle_interrupt'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525 in 'Async::Scheduler#run_loop'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:567 in 'Async::Scheduler#run'
               |     /home/samuel/Developer/socketry/async/lib/kernel/async.rb:34 in 'Kernel#Async'
               |     ./test.rb:3 in '<main>'
/home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453:in 'IO::Event::Selector::URing#select': Interrupt
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453:in 'Async::Scheduler#run_once!'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:492:in 'Async::Scheduler#run_once'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:568:in 'block in Async::Scheduler#run'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:528:in 'block in Async::Scheduler#run_loop'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525:in 'Thread.handle_interrupt'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525:in 'Async::Scheduler#run_loop'
	from /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:567:in 'Async::Scheduler#run'
	from /home/samuel/Developer/socketry/async/lib/kernel/async.rb:34:in 'Kernel#Async'
	from ./test.rb:3:in '<main>'
  • The cause is correctly attached.

HEAD (without this PR)

Interestingly enough, Fiber#raise automatically uses the current exception ($!) as cause, the same as Kernel#raise. Therefore, while this PR has some ergonomic advantages, it's not strictly needed:

> chruby ruby-head
> git checkout main
> ruby ./test.rb
^C  0.0s    error: Object [oid=0x20] [ec=0x28] [pid=201905] [2025-07-23 18:26:59 +1200]
               | Task exiting
               | Async::Stop
               |   Async::Stop: Async::Stop
               |   → lib/async/scheduler.rb:191 in 'IO::Event::Selector::URing#transfer'
               |     lib/async/scheduler.rb:191 in 'Async::Scheduler#transfer'
               |     lib/async/scheduler.rb:281 in 'Async::Scheduler#kernel_sleep'
               |     ./test.rb:4 in 'Kernel#sleep'
               |     ./test.rb:4 in 'block in <main>'
               |     lib/async/task.rb:205 in 'block in Async::Task#run'
               |     lib/async/task.rb:443 in 'block in Async::Task#schedule'
               |   Caused by Interrupt: Interrupt
               |   → /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453 in 'IO::Event::Selector::URing#select'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:453 in 'Async::Scheduler#run_once!'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:492 in 'Async::Scheduler#run_once'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:568 in 'block in Async::Scheduler#run'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:528 in 'block in Async::Scheduler#run_loop'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525 in 'Thread.handle_interrupt'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:525 in 'Async::Scheduler#run_loop'
               |     /home/samuel/Developer/socketry/async/lib/async/scheduler.rb:567 in 'Async::Scheduler#run'
               |     /home/samuel/Developer/socketry/async/lib/kernel/async.rb:34 in 'Kernel#Async'
               |     ./test.rb:3 in '<main>'
  • Cause is correctly attached in this case (but there are some cases where it won't be).

ioquatix avatar Jul 23 '25 06:07 ioquatix

Okay, the changes to CRuby were merged. We just need to make this work as best we can across different Ruby versions.

ioquatix avatar Jul 24 '25 03:07 ioquatix