socketry / async

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

NameError: uninitialized constant inside Async::Task #172

Closed gdonald closed 2 years ago

gdonald commented 2 years ago

I don't seem to have scope for my ActiveRecord model inside an Async::Task.

For my model named F I get:

NameError: uninitialized constant F

My implementation:

Async do
  # Queue of up to 10 items:
  items = Async::LimitedQueue.new(10)

  # Five producers:
  5.times do
    Async do |task|
      # while true
      while F.unchecked.count.positive?
        # t = rand
        f = F.unchecked.first
        f.do_checking!
        t = check_f(f)

        task.sleep(t)
        items.enqueue(t)
      end
    end
  end

  # A single consumer:
  Async do |task|
    while item = items.dequeue
      puts "dequeue -> #{item}"
    end
  end
end

I also tried using ::F but got the same error.

ioquatix commented 2 years ago

Are you using autoload? autoload is broken on Ruby < 3.2.0 unfortunately so this will only be fixed when Ruby 3.2.0 is released. Backporting the change to 3.1 was deemed too extensive. The solution is to ensure all files are loaded before executing your app. Maybe @fxn can help advise how to do this?

gdonald commented 2 years ago

I haven't made any calls to autoload, no.

My F model is in scope just before my Async do block, not sure why it would then not be in scope a couple of lines later. Does Async not follow normal Ruby scoping?

My code is fully loaded as far as I can tell. I'm running this using bundle exec rails console, then inside there I run my Async code I posted above.

fxn commented 2 years ago

Zeitwerk uses Module#autoload.

If you use fibers by hand you are in control, but custom fiber schedulers escape the synchronization of autoloads and constant references builtin in CRuby (for < 3.2 ?).

WIth current Ruby, the workaround is to load F before the Async call.

ioquatix commented 2 years ago

IIRC, any reference F makes to autoload can also break in the same way (NameError). So it's not just that F should be in scope but any class F references. @fxn isn't there a way to trigger all autoloads?

ioquatix commented 2 years ago

config.eager_load = true should do It (force all autoloads), can you try this @gdonald

fxn commented 2 years ago

IRC, any reference F makes to autoload can also break in the same way

Correct. If the reference happens at class-level, it is going to be resolved in cascade. No problem in this case. However, if the methods being executed trigger autoloads in turn, you'd have the same sync issues. Eager loading is the safest bet if that is the case.

gdonald commented 2 years ago

WIth current Ruby, the workaround is to load F before the Async call.

My ruby is:

> ruby -v
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [arm64-darwin21]

Adding a call to autoload doesn't seem to help, I get the same NameError as before:

autoload(:F, "#{Rails.root}/app/models/f")

Async do
  # Queue of up to 10 items:
  items = Async::LimitedQueue.new(10)

  # Five producers:
  5.times do
    Async do |task|
      # while true
      while F.unchecked.count.positive?
        # t = rand
        f = F.unchecked.first
        f.do_checking!
        t = check_f(f)

        task.sleep(t)
        items.enqueue(t)
      end
    end
  end

  # A single consumer:
  Async do |task|
    while item = items.dequeue
      puts "dequeue -> #{item}"
    end
  end
end

Also tried with ::F. Same.

ioquatix commented 2 years ago

autoload is what is causing the problem. You'll need to add config.eager_load = true.

gdonald commented 2 years ago

autoload is what is causing the problem. You'll need to add config.eager_load = true.

Ok, I guess I misunderstood. I thought you guys were suggesting to make a manual call to autoload.

config.eager_load = true gets me to the next error. Thanks.

My next error is TypeError: true can't be coerced into Float. I'm supposed to return a Float from my task?

ioquatix commented 2 years ago

Can you give me a line number or backtrace?

gdonald commented 2 years ago

Can you give me a line number or backtrace?

 0.13s     warn: Async::Task [oid=0x5c94] [ec=0x5ca8] [pid=7907] [2022-07-17 08:14:43 -0500]
               | Task may have ended with unhandled exception.
               |   TypeError: true can't be coerced into Float
               |   → /Users/gd/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/timers-4.3.3/lib/timers/timer.rb:103 in `+'
               |     /Users/gd/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/timers-4.3.3/lib/timers/timer.rb:103 in `reset'
               |     /Users/gd/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/timers-4.3.3/lib/timers/timer.rb:44 in `initialize'
               |     /Users/gd/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/timers-4.3.3/lib/timers/group.rb:60 in `new'
               |     /Users/gd/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/timers-4.3.3/lib/timers/group.rb:60 in `after'
               |     /Users/gd/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/async-2.0.3/lib/async/scheduler.rb:115 in `block'
               |     /Users/gd/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/async-2.0.3/lib/async/scheduler.rb:144 in `kernel_sleep'
               |     /Users/gd/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/async-2.0.3/lib/async/task.rb:92 in `sleep'
               |     /Users/gd/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/async-2.0.3/lib/async/task.rb:92 in `sleep'
ioquatix commented 2 years ago

You are probably calling task.sleep(t) with t = nil. I should add more explicit error checking for this.

gdonald commented 2 years ago

A boolean actually. I changed it to task.sleep(0.1) and got it working.

gdonald commented 2 years ago

Thanks for all your help :)

ioquatix commented 2 years ago

You're welcome.