views:

311

answers:

2

I have this code in my controller for a Rails app:

  def delete
    object = model.datamapper_class.first(:sourced_id => params[:sourced_id])
    if object.blank?
      render :xml => "No #{resource} with sourced_id #{params[:sourced_id]}", :status => :not_found and return
    end
    object.destroy
    render :xml => "", :status => :no_content
  rescue MysqlError => e
    puts "raised MysqlError #{e.message}"
    render :xml => e.message, :status => :unprocessable_entity and return
  rescue Mysql::Error => e
    puts "raised Mysql::Error #{e.message}"
    render :xml => e.message, :status => :unprocessable_entity and return
  rescue Exception => e
    puts "not a MysqlError, instead it was a #{e.class.name}"
    render :xml => e.message, :status => :unprocessable_entity and return
  end

When I run my spec to make sure my foreign key constraints work, I get this:

not a MysqlError, instead it was a MysqlError

What could be going on here?


Some ancestor information: When I change the rescue to give me this:

puts MysqlError.ancestors
puts "****"
puts Mysql::Error.ancestors
puts "****"
puts e.class.ancestors

This is what I get:

Mysql::Error
StandardError
Exception
ActiveSupport::Dependencies::Blamable   ...
****
Mysql::Error
StandardError
Exception
ActiveSupport::Dependencies::Blamable   ...
****
MysqlError
StandardError
Exception
ActiveSupport::Dependencies::Blamable   ...

Could there be an alias in the global namespace that makes the MysqlError class unreachable?

+3  A: 

Ruby classes are just objects, so comparison is based on object identity (ie, the same pointer under the hood).

Not sure what's happening in your case, but I'd try debugging in a few locations and seeing what object ids and ancestors you get for MysqlError. I suspect that there's two such objects in different modules and your catch clause is referencing the wrong one.

Edit:

That is quite strange. My guess now is that MysqlError or one of it's ancestors has been included at two different points along your controllers own class chain, and that's somehow tripping up the exception catching.

Theory #2 would be that since rails redefines const_missing to do auto-requires, where you'd expect to get an UndefinedConstant exception in the exception handling clauses is instead finding something by that name god knows where in the source tree. You should be able to see if that's the case by testing with the auto requiring off (ie do some debugs in both dev and prod mode).

There is a syntax for forcing your reference to start from the root which may be of some help if you can figure out the right one to be referencing:

::Foo::Bar

Rant:

This sort of thing is where I think some of the flaws of ruby show. Under the hood Ruby's object model and scoping is all object structures pointing to each other, in a way that's quite similar to javascript or other prototype based languages. But this is surfaced inconsistently in the class/module syntax you use in the language. It seems like with some careful refactoring you could make this stuff clearer as well as simplify the language, though this would of course be highly incompatible with existing code.

Tip:

When using puts for debugging, try doing puts foo.inspect as this will display it in the way you're used to from irb.

Jason Watkins
Yes, I think so too, and the ancestors idea was a good one, I put more info in the question. But how do I actually reference the one I want?
Michael Sofaer
+3  A: 

This was a simple class redefinition bug. Ruby lets you redefine a top-level constant, but it doesn't destroy the original constant when you do it. Objects that still hold references to that constant can still use it, so it could still be used to generate exceptions, like in the issue I was having.

Since my redefinition was happening in dependencies, I solved this by searching for the original class in the Object space, and hanging on to a reference to it to use when catching exceptions. I added this line to my controller:

ObjectSpace.each_object(Class){|k| @@mysql_error = k if k.name == 'MysqlError'}

That gets a reference to the original version of MysqlError. Then I was able to do this:

  rescue @@mysql_error => e
    render :xml => e.message, :status => :unprocessable_entity and return

This happens because the mysql gem is getting loaded after MysqlError has already been defined. Here is some test console joy:

Loading test environment (Rails 2.3.2)
>> MysqlError.object_id
=> 58446850
>> require 'mysql'
C:/Ruby/lib/ruby/gems/1.8/gems/mysql-2.7.3-x86-mswin32/ext/mysql.so: warning: already initialized constant MysqlError
=> true
>> MysqlError.object_id
=> 58886080
>> ObjectSpace._id2ref(MysqlError.object_id)
=> Mysql::Error

You can do this in IRB without a require pretty easily; here's a trick that works because irb doesn't look up Hash by name every time you declare a Hash literal:

irb(main):001:0> Hash = Class.new
(irb):1: warning: already initialized constant Hash
=> Hash
irb(main):002:0> hash = {:test => true}
=> {:test=>true}
irb(main):003:0> hash.class
=> Hash
irb(main):004:0> hash.is_a? Hash
=> false

I can see why you might want to do this, it could be used like alias_method_chain for the global namespace. You could add a mutex to a class that isn't threadsafe, for example, and not need to change old code to reference your threadsafe version. But I do wish RSpec hadn't silenced that warning.

Michael Sofaer
Wow, that's nasty. Great spot.
tomafro
+1 Fascinating stuff. I don't think you can replicate this in Ruby. I was just trying to define the class Blah in irb, and, strangely, even if you EXIT AND ENTER irb the object_id for a newly defined Blah class stays the same... However if you define Blah differently at the get-go (e.g., you add a method, but on the first definition) then the object_id changes. I have no idea what this means, but I think it might help...?
Yar
Is MysqlError being monkey-patched before the gem is loaded?
Scott
@Scott, monkey patching doesn't change the object_id. Try it in irb. When the class is first defined, it gets an object_id (which is somehow related to the contents of the class, including its name) and that sticks... except in the situation Michael describes.
Yar
Whoa, that is quite evil. Great job on tracking it down.
Jason Watkins