Issue #4521 has been updated by mame (Yusuke Endoh).

Description updated
Target version set to next minor


----------------------------------------
Feature #4521: NoMethodError#message may take very long to execute
https://bugs.ruby-lang.org/issues/4521#change-33223

Author: adiel.mittmann (Adiel Mittmann)
Status: Assigned
Priority: Normal
Assignee: matz (Yukihiro Matsumoto)
Category: 
Target version: next minor


=begin
When a non-existing method is called on an object, NoMethodError is risen. If you call #message, however, your code may use up all CPU for a very long time (in my case, up to a few minutes).

I narrowed the problem down to this code in error.c (SVN snapshot) in the function name_err_mesg_to_str():

 d = rb_protect(rb_inspect, obj, 0);
 if (NIL_P(d) || RSTRING_LEN(d) > 65) {
     d = rb_any_to_s(obj);
 }
 
The problem is that, for a complex object, #inspect may take very long to execute, only to have its results thrown away because they will be larger than 65 characters.

Of course I can write a #to_s for all my objects, but the point is that I didn't call #to_s or #inspect, I called #message on an exception object, which then takes a few minutes just to return a short string.

Needless to say, this might be easy to spot in a simple example, but once you're writing a web application that suddenly freezes for one minute with no apparent reason, you're all but clueless as to what's going on. (The first time this happened, I didn't even know that something would eventually show up on the screen -- I thought it was an infinite loop).

Here's an example code that shows this behavior:

 require 'nokogiri'
 class A
   def x
     @xml = Nokogiri::XML(File.new('baz.xml', 'rb').read())
     foo()
   end
 end
 A.new().x()
 a.x


Here, the time it takes for Ruby to print out the message that #foo doesn't exist is proportional to the size of baz.xml.

As a comparison, Python doesn't seem to do this. Take the following code:

 class Test:
     def __str__(self):
         return "hello"
 a = Test()
 print a
 print a.x()

If you execute it, this is the result:

 hello
 Traceback (most recent call last):
   File "test.py", line 6, in <module>
     print a.x()
 AttributeError: Test instance has no attribute 'x'

It uses the method __str__ to convert the object to a string when necessary, but doesn't use it when printing out the message stating that the attribute doesn't exist.

One obvious way to fix this would be to always print out the simpler representation given by rb_any_to_s.
=end



-- 
http://bugs.ruby-lang.org/