socketry / async

An awesome asynchronous event-driven reactor for Ruby.
MIT License
2.14k stars 88 forks source link

Task hangs during stop if there are more than two sub tasks (?) #158

Closed paddor closed 1 year ago

paddor commented 2 years ago

I'm on Ruby 3.1.1 and Async 2.0.1, Ubuntu 20.04. The following script hangs after the first Ctrl+C, but only if both sleeping tasks are active:

#!/usr/bin/env ruby

require 'async'

class Server
  def initialize
    @running = false
  end

  def stopping?
    !@running
  end

  def stop(&after_shutdown)
    @after_shutdown = after_shutdown if after_shutdown
    @running = false
  end

  def start(task: Async::Task.current)
    task.async do |task|
      @running = true

      while @running
        run_once
        sleep 0.1
      end

      @after_shutdown.call if @after_shutdown
    end
  end

  def run_once
    print '.'
  end
end

Async do |task|
  server = Server.new

  Signal.trap(:INT) do
    puts "Got INT"
    abort 'Aborting' if server.stopping? # abort on second ^C

    server.stop do
      puts "Server shut down. Stopping task."
      task.stop
    end
  end

  server.start

  task.async do
    puts "sleeping #1"
    sleep
  end

  # NOTE: Hangs only if this second task is added
  task.async do
    puts "sleeping #2"
    sleep
  end
end

puts 'Done'

Maybe related to #137 but it also hangs with all puts commented out.

ioquatix commented 2 years ago

Thanks, I'll check it.

paddor commented 2 years ago

I was hoping this was fixed together with #137 but it's still happening with Ruby 3.2-preview2:

$ ruby -v
ruby 3.2.0preview2 (2022-09-09 master 35cfc9a3bb) [x86_64-linux]
$ bundle exec ./async_test.rb
Could not load native event selector: cannot load such file -- IO_Event
.sleeping #1
sleeping #2
.....................^CGot INT
Server shut down. Stopping task.
^CGot INT
Aborting

The process should terminate after the first ˆC, but hangs instead. The second one calls Kernel#abort.

My Gemfile (repos checked out at current master/main):

gem 'async', path: '../async'
gem 'io-event', path: '../io-event'
gem 'timers', path: '../timers'
ioquatix commented 2 years ago

Thanks for the update, I'll investigate.

ioquatix commented 2 years ago

I could reproduce the issue. I'll investigate more.

ioquatix commented 2 years ago

Signal.trap behaves strangely in an asynchronous context. We might be able to make it more robust, but we've got a specific construct to handle it - Async::IO::Trap. Let me review this code a bit more and see if there is a better more straight forward solution.

paddor commented 1 year ago

Any updates?

ioquatix commented 1 year ago

For some odd reason, I could not reproduce the issue any more. Do you mind checking?

I'm sure it's something wrong on my end, I have a local branch with some work to deal with this, but have not pushed it yet.

paddor commented 1 year ago

@ioquatix Yes, it's still happening with v2.5.1 on Ruby 3.2.2:

$ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux]
$ ruby async_bug.rb
2.5.1
.sleeping #1
sleeping #2
...............^CGot INT
Server shut down. Stopping task.
^CGot INT
Aborting
ioquatix commented 1 year ago

Okay great, thanks, I'll check what's going on.

ioquatix commented 1 year ago

Here is a smaller repro:

#!/usr/bin/env ruby

require_relative 'lib/async'

Async(annotation: "Top") do |task|
  task.async(annotation: "Stopper") do
    sleep 0.1
    puts "Stopping parent..."
    task.stop
  end

  task.async(annotation: "Sleeper #1") do
    puts "sleeping #1"
    sleep
  end

  # NOTE: Hangs only if this second task is added
  task.async(annotation: "Sleeper #2") do
    puts "sleeping #2"
    sleep
  end
end

puts 'Done'

I understand the problem and am working on a fix.

paddor commented 1 year ago

@ioquatix Thank you for the fix.