Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infinite loop when using mutually-referential constants #2314

Closed
jeremyevans opened this Issue Dec 12, 2014 · 16 comments

Comments

Projects
None yet
5 participants
@jeremyevans
Copy link
Contributor

jeremyevans commented Dec 12, 2014

The following code appears to cause an infinite loop in JRuby 1.7.17 (OpenBSD) and 1.7.6 (Windows):

def a
  c = Class.new
  c2 = Class.new
  c.const_set(:A, c2)
  c2.const_set(:A, c)
end

Calling that a method causes the CPU to go to 100%, and requires a kill -9 to kill the process.

@headius headius added this to the JRuby 1.7.18 milestone Dec 15, 2014

@headius

This comment has been minimized.

Copy link
Member

headius commented Dec 15, 2014

Weird.

@headius

This comment has been minimized.

Copy link
Member

headius commented Dec 15, 2014

This one is a little tricky...there's some differences in constant-definition logic in JRuby. The problem is basically that when doing const_set, we do an additional step that sets the parent of a module/class that doesn't currently have a parent. MRI does not appear to have this logic, but if I remove it from setConstantCommon, a number of specs that do want to see parent updated properly no longer pass.

@headius

This comment has been minimized.

Copy link
Member

headius commented Dec 15, 2014

Here's my diff. This patch isn't right and this fix isn't going to make 1.7.18 because it's too risky to change this day before release.

diff --git a/core/src/main/java/org/jruby/RubyModule.java b/core/src/main/java/org/jruby/RubyModule.java
index c17f30e..19b2d20 100644
--- a/core/src/main/java/org/jruby/RubyModule.java
+++ b/core/src/main/java/org/jruby/RubyModule.java
@@ -3354,7 +3354,6 @@ public class RubyModule extends RubyObject {
             RubyModule module = (RubyModule)value;
             if (module != this && module.getBaseName() == null) {
                 module.setBaseName(name);
-                module.setParent(this);
             }
         }
         return value;

@headius headius modified the milestones: JRuby 1.7.19, JRuby 1.7.18 Dec 15, 2014

@headius

This comment has been minimized.

Copy link
Member

headius commented Dec 15, 2014

For anyone that attempts this bug... more facts.

  • Anonymous classes in Ruby normally do get the first name they're assigned to, but it appears that only happens for a direct constant assignment. In other words, const_set does not always set the name?
  • Parent assignment seems to be implicit in some operations, but not others. We need to sort out how and when MRI actually updates anonymous class/module name and parent.

@enebo enebo modified the milestones: JRuby 1.7.19, JRuby 1.7.20 Jan 28, 2015

@headius

This comment has been minimized.

Copy link
Member

headius commented Apr 30, 2015

I'm looking at this casually for 1.7.20 and wondering how synthetic this is. @jeremyevans Did you run into this with real code?

@headius

This comment has been minimized.

Copy link
Member

headius commented Apr 30, 2015

FWIW, you have to ^Z and kill because IRB traps SIGINT. If you run this at command line it can be interrupted with ^C like a normal JVM program.

@jeremyevans

This comment has been minimized.

Copy link
Contributor Author

jeremyevans commented Apr 30, 2015

I did run into this with real code. When building Roda, I originally wanted to use mutually referential constants. When you subclass Roda, Roda automatically creates a subclass of Roda::RodaRequest, which is used by the plugin system so that plugins can override request methods for specific application classes. So my original design was more or less:

roda_subclass = Class.new(Roda)
request_subclass = Class.new(Roda::RodaRequest)
roda_subclass::RodaRequest = request_subclass
request_subclass::RodaClass = roda_subclass

When that didn't work on JRuby, I switched it to use an instance variable/accessor to store a link from the request subclass to the roda subclass:

roda_subclass = Class.new(Roda)
request_subclass = Class.new(Roda::RodaRequest)
roda_subclass::RodaRequest = request_subclass
request_subclass.roda_class = roda_subclass

I would have preferred to use the mutually referential constant, as constant access is faster than a method call and the value really is constant (it won't ever change after being set).

@headius

This comment has been minimized.

Copy link
Member

headius commented Apr 30, 2015

@jeremyevans Ok thanks, good to know this wasn't a purely synthetic case.

I'm researching how MRI does this, having never actually looked into their code. Apparently they do the name discover when you request the module's name, rather than at assignment time like in JRuby and Rubinius. That could explain why they don't have the same issues.

@headius

This comment has been minimized.

Copy link
Member

headius commented Apr 30, 2015

With @evanphx's help, I've discovered that .name on detached namespaces is orders of magnitude slower than namespaces reachable from Object, due to the fact that they have to keep re-searching for the module every time.

require 'benchmark'

module B
  module X
  end
end

def a
  c = Class.new
  c2 = Class.new
  c.class_eval 'A = c2'
  c2.class_eval 'A = c'

  c
end

c = a
x = B::X

loop do
  puts 'named'
  puts Benchmark.measure { 1_000_000.times { x.name } }
  puts 'anon'
  puts Benchmark.measure { 1_000_000.times { c.name } }
end

And results:

named
  0.190000   0.000000   0.190000 (  0.188228)
anon
 12.810000   0.020000  12.830000 ( 12.846686)

We are going to fix this in some way, but if you have any namespaces unreachable from Object, you really shouldn't be calling #name on them.

@evanphx

This comment has been minimized.

Copy link

evanphx commented Apr 30, 2015

@headius can you do the benchmark and call inspect on an instance to see if that is also orders of magnitude slower?

@enebo

This comment has been minimized.

Copy link
Member

enebo commented Apr 30, 2015

What fast path use-cases could be affected by this? Exceptions inspect. Logging/IO will call inspect. Is this really not on top of something much slower in general?

@nirvdrum

This comment has been minimized.

Copy link
Contributor

nirvdrum commented Apr 30, 2015

I wonder if JRuby+Truffle is doing something wrong here, then. Even without Graal the two run roughly the same speed. But clearly it's much slower in MRI and JRuby.

@nirvdrum

This comment has been minimized.

Copy link
Contributor

nirvdrum commented Apr 30, 2015

And indeed, it looks like we get it wrong. I'll have to see about adding a RubySpec for that.

@headius

This comment has been minimized.

Copy link
Member

headius commented May 4, 2015

@nirvdrum I think everyone except MRI is going to get this wrong unless we intend to do full-system search to build the namespace lazily.

@headius

This comment has been minimized.

Copy link
Member

headius commented May 4, 2015

I filed https://bugs.ruby-lang.org/issues/11119 for the perf issue in MRI.

headius added a commit that referenced this issue May 4, 2015

@headius headius closed this May 4, 2015

@headius

This comment has been minimized.

Copy link
Member

headius commented May 4, 2015

This will get into 1.7.20, and 9k.rc1 (or whatever follows pre2).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.