Bug in Rails 2.0 when overloading a find_by method?

I’ve got an interesting replecatable bug that has sprung up since I
migrated some code i’ve written from rails 1.2.3 to 2.0.2. I am using
a mysql backend database, in which I am storing IPv4 addresses as 32
bit unsigned integers. Of course, I don’t want my users to have to
enter the IP they are searching for in this fashion, nor do I want to
have to make the conversion myself every time I call the find_by_ip
function or find_or_create_by_ip function for the model, so I have
overloaded those two functions. find_by_ip now reads as follows:

def self.find_by_ip(ip)
super(NetAddr::CIDR.create(ip).to_i)
end

This works, the first time IP.find_by_ip(address) is called (this test
done in script/console):

ip = Ip.find_by_ip(“10.21.1.8”)
=> #<Ip id: 13, ip: 169148680>

However any subsequent calls to find_by_ip just return nil, even for
the same IP address, until the environment is reloaded:

reload!
Reloading…
=> true

ip = Ip.find_by_ip(“10.21.1.8”)
=> #<Ip id: 13, ip: 169148680>

ip = Ip.find_by_ip(“10.21.1.8”)
=> nil

If I add some puts statements in my overloaded find_by_ip, they never
get printed out after the first call to it has been done. Equally, if
I call find_by_ip with a 32 bit int form of an IPv4 address it works
reliably:

def self.find_by_ip(ip)
puts “Testing\n”
super(NetAddr::CIDR.create(ip).to_i)
end

?> reload!
Reloading…
=> true

ip = Ip.find_by_ip(“10.21.1.8”)
Testing
=> #<Ip id: 13, ip: 169148680>

ip = Ip.find_by_ip(“10.21.1.8”)
=> nil

ip = Ip.find_by_ip(169148680)
=> #<Ip id: 13, ip: 169148680>

It is as if, after the first call to my overloaded find_by_ip, rails
decides to ignore my overloaded function and go straight to the base
functionality it has for creating find_by functions. Can anyone else
test this to prove it’s not just me, and/or suggest who/where I should
report it as a bug?

Thanks

Dan Meyers
Network Support, Lancaster University

On 16 Jan 2008, at 14:57, Carr0t wrote:

def self.find_by_ip(ip)
super(NetAddr::CIDR.create(ip).to_i)
end

I expect what happens is as follows:
you call find_by_ip and go through to your method. You call super and
hit method_missing. In rails 1.2 this just called find for you with
the right arguments. In rails 2.x, rails actually creates a find_by_ip
method (so overwriting your one) and calls that.
On your next call to find_by_ip you go straight to the rails generate
one, which isn’t coercing the string to the appropriate ip.
You could create a ticket on dev.rubyonrails.org, but i’d first bring
up the issues on rubyonrails-core. I’m not sure what the best way of
resolving this would be.

Fred

2008/1/16, Carr0t [email protected]:

def self.find_by_ip(ip)
super(NetAddr::CIDR.create(ip).to_i)
end

[…]

It is as if, after the first call to my overloaded find_by_ip, rails
decides to ignore my overloaded function and go straight to the base
functionality it has for creating find_by functions. Can anyone else
test this to prove it’s not just me, and/or suggest who/where I should
report it as a bug?

  • You’ve defined find_by_ip in your AR::B subclass to overload
    a dynamic finder. And in your method, you call super.

  • In Rails < 2.* (like 1.2.* series), dynamic finders are handled
    by a method_missing mechanism.

So in 1.2.*, that’s how things work :

Ip.find_by_ip(“10.21.1.8”) calls your defined find_by_ip method.
With super, Ruby tries to see if there’s a find_by_ip in parent classes.
There is not, so method_missing is called, that makes your AR::B.find
on the fly and retrieves for you the right results.

  • In Rails 2.0.*, dynamic finders are handled by a method missing
    mechanism at the first call, a finder method is generated at the first
    call, this fresh method handles your query.

In the doc “Each dynamic finder or initializer/creator is also defined
in the class after it is first invoked, so that future attempts to use
it do not run through method_missing.”

So in 2.0.* your stuff works like that :

Ip.find_by_ip(“10.21.1.8”) calls your defined class method in Ip class.
your method invokes super : Ruby looks for a find_by_ip method
in parent classes. There aren’t. method_missing is called. First
time called, a find_by_ip is generated, overiding your defined method.
Rails call the new method, retrieving the right results. At the second
call, Rails use the new method that overrides yours, but can’t
deal string like “10.21.1.8”, so your query returns nil. If you pass
an integer as argument it works, using the one-the-fly generated
method, not yours.

3/ In a nutshell, in your defined find_by_ip, call AR::B.find directly
(or
another dynamic finder !), don’t use super, to bypass all this
method_missing mechanism.

HTH,

-- Jean-François.