algoliasearch-rails icon indicating copy to clipboard operation
algoliasearch-rails copied to clipboard

System Stack Error when indexing objects.

Open v4n opened this issue 9 years ago • 5 comments

I placed a binding before the stack too deep error occurs when indexing an object.

From: /Users/Foo/.rvm/gems/ruby-2.2.3/gems/algoliasearch-rails-1.14.1/lib/algoliasearch-rails.rb @ line 114 AlgoliaSearch::IndexSettings#get_attributes:

    109:         @additional_attributes.each { |name, value| res[name.to_s] = value.call(object) } if @additional_attributes
    110:         res
    111:       else
    112:         object.class.unscoped do
    113:           binding.pry
 => 114:           res = @attributes.nil? || @attributes.length == 0 ? object.attributes :
    115:             Hash[@attributes.map { |name, value| [name.to_s, value.call(object) ] }]
    116:           @additional_attributes.each { |name, value| res[name.to_s] = value.call(object) } if @additional_attributes
    117:           res
    118:         end
    119:       end

We are indexing an Admin object, here is the rails model code for that object:

class Admin < User
  def avatar
    # avatar method uses `self.class` and we need that to be `User`
    # not ideal as it creates an extra query + object... but ignore this
    # as the ticket exposes an issue that could affect valid implementations
    User.find(id).avatar  
  end
end 

Admin model inherits from the User model. The avatar method instantiates a User object and calls the avatar method on it.

In the binding I run:

[14] pry(<AlgoliaSearch::IndexSettings>)> User == Admin
=> false
[15] pry(<AlgoliaSearch::IndexSettings>)> User.first == Admin.first
  Admin Load (50.7ms)  SELECT  "users".* FROM "users"  ORDER BY "users"."id" ASC LIMIT 1
  Admin Load (46.7ms)  SELECT  "users".* FROM "users"  ORDER BY "users"."id" ASC LIMIT 1
=> true

The User & Admin model are identified as structures but when I instantiate a User and Admin object they are identified as equals.

The correct behaviour would be the following (output from outside the binding):

2.2.3 :003 > User.first == Admin.first
  User Load (52.8ms)  SELECT  "users".* FROM "users"  ORDER BY "users"."id" ASC LIMIT 1
  Admin Load (45.4ms)  SELECT  "users".* FROM "users"  ORDER BY "users"."id" ASC LIMIT 1
 => false

Due to this weird behaviour the Admin avatar method is the equivalent of def avatar; Admin.find(id).avatar; end which is obviously an infinite loop.

v4n avatar Feb 05 '16 06:02 v4n

Hi @v4n ,

It could be a bug related to STI. You are using STI, isn't it ?

I'm trying to understand your models' setup to replicate, could you help me with the following questions?

  • Why is the Admin#avatar method needed since this method should be inherited from the User model ?
  • What happens when you run Admin.last.avatar, does it lead to a stack too deep error as well ?

I do not understand how the Admin#avatar method is not infinitely recursive since with STI User.find(id) should return an Admin instance (same as self)

Thanks

alexandremeunier avatar Feb 05 '16 09:02 alexandremeunier

@AlexanderEkdahl I did mentioned why in the comment above:

    # avatar method uses `self.class` and we need that to be `User`
    # not ideal as as it creates an extra query + object... but ignore this
    # as the ticket exposes an issue that could affect valid implementations

If a run Admin.last.avatar in the binding (algoliasearch-rails.rb @ line 114 AlgoliaSearch::IndexSettings#get_attributes) this will lead to a stack to deep error. If I run the same method call on the Admin object outside the binding, e.g. in a controller of my app, it works perfectly fine.

Let me propose another example:

class User < ActiveRecord::Base
   def type
       "I am a #{self.class}"
   end
end

class Admin < User
   def non_admin_friends
       User.where(:friend => id)
   end
end

# Inside the binding (algoliasearch-rails.rb @ line 114 AlgoliaSearch::IndexSettings#get_attributes)
> non_admin_friends = Admin.non_admin_friends
> non_admin_friends.first.type 
=> I am a Admin # this should be I am a User. Works on the rails console.

v4n avatar Feb 05 '16 17:02 v4n

Hi @v4n

So it seems that this unscoped block messes with the STI internals.

While we are working on finding a more generic fix, can you try to change the Admin#avatar definition to the following (it'll recast the admin object into an instance of User instead of Admin) :

class Admin < User
  def avatar
    self.becomes(User).avatar  
  end
end 

alexandremeunier avatar Feb 08 '16 08:02 alexandremeunier

Thanks @AlexanderEkdahl using becomes solves the issue for us.

v4n avatar Feb 15 '16 22:02 v4n

@alexandremeunier do you have an idea how to improve that?

redox avatar Apr 24 '16 08:04 redox