attr_json icon indicating copy to clipboard operation
attr_json copied to clipboard

Defining attr_json attributes on the parent class results in "NoMethodError: super: no superclass method"

Open marclennox opened this issue 2 years ago • 20 comments

(note from maintainer: There are a variety of things that could cause a NoMethodError. If you have such an error but don't know why, please file a separate Issue or, even better, start at topic in Discussions, instead of adding a "me too" here. Thank you, and thanks for getting in touch!)


I'm trying to upgrade to the latest attr_json and running into an issue that was not happening with the previous version of attr_json (we've been using this gem for many years).

I have several cases where I define the attr_json attribute in the parent (abstract) class, then use it in the child class. With the new version, I can only get it to work if I define the attribute in each of the child classes.

Is this expected behaviour, and if so, can you suggest how to get around this?

marclennox avatar Jan 31 '23 15:01 marclennox

Hi @marclennox , thank you for getting in touch with an error report! And I'm glad to hear you've been using it for years!

What you describe does not sound like expected behavior, it sounds like a bug. If so, I'm sorry we didn't catch it before the 2.0.0 release!

But I'm having trouble reproducing. Subclasses inheriting the attr_json defined in the parent seem to work fine for me -- I even do it in my own app. If something broke for you upgrading, then probably there is some kind of bug, but I'm sorry, I haven't been able to reproduce it.

Can you give me a "failing test case", or the skeleton of one with some code that can reproduce the problem? Are your classes AttrJson::Record or AttrJson::Model?

jrochkind avatar Jan 31 '23 16:01 jrochkind

OK, I'll have to try and create a reproducible test case for you. I'm using AttrJson::Record.

marclennox avatar Jan 31 '23 17:01 marclennox

It should also be noted that it's just the setter method that is throwing this error. The getter method works fine.

marclennox avatar Jan 31 '23 17:01 marclennox

Thanks! I cannot reproduce, setter works fine for me too.

I suspect there is something specific in your code that triggers it where the simplest possible reproduction does not -- which doesn't mean it's not an attr_json bug.

Reproduction would be great, but in the meantime if you could send a whole stack trace for the error raised, that might be a clue.

jrochkind avatar Jan 31 '23 18:01 jrochkind

The super method I am guessing it is -- should be calling the "setter" method that was provided by the call to Rails attribute.

attr_json 2.0 always creates an underlying Rails attribute, for more natural/consistent Rails integration. And relies on it being there and being called by the setter, for some behaviors to work. Such as rails dirty tracking, and full casting.

So if it's not there.... things might break, guarding the call to super with a condition to check super exists may not be sufficient.

So I'm very curious under what conditions the super method ordinarily provided by rails attribute would not exist. I can't reproduce it. And might need to in order to fix things for those conditions. Which I am definitely interested in doing! So appreciate your help!

jrochkind avatar Jan 31 '23 18:01 jrochkind

Thanks @jrochkind! I am really glad that you have adopted the Rails attributes API for this gem. I will be honest and say that I was hoping this would happen for some time. Let me see if I can gather some more clues for you as to what is going on.

marclennox avatar Jan 31 '23 18:01 marclennox

Full stack trace, if it helps

/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/activemodel-7.0.4.2/lib/active_model/attribute_methods.rb:455:in `method_missing'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/attr_json-2.0.0/lib/attr_json/record.rb:205:in `block (2 levels) in attr_json'
(pry):6:in `__pry__'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:290:in `eval'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:290:in `evaluate_ruby'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:659:in `handle_line'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:261:in `block (2 levels) in eval'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:260:in `catch'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:260:in `block in eval'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:259:in `catch'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_instance.rb:259:in `eval'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/repl.rb:77:in `block in repl'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/repl.rb:67:in `loop'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/repl.rb:67:in `repl'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/repl.rb:38:in `block in start'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/input_lock.rb:61:in `__with_ownership'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/input_lock.rb:78:in `with_ownership'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/repl.rb:38:in `start'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/repl.rb:15:in `start'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-byebug-3.10.1/lib/pry-byebug/pry_ext.rb:15:in `start_with_pry_byebug'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/pry-0.14.2/lib/pry/pry_class.rb:194:in `start'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/railties-7.0.4.2/lib/rails/commands/console/console_command.rb:70:in `start'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/railties-7.0.4.2/lib/rails/commands/console/console_command.rb:19:in `start'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/railties-7.0.4.2/lib/rails/commands/console/console_command.rb:102:in `perform'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/thor-1.2.1/lib/thor/command.rb:27:in `run'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/thor-1.2.1/lib/thor/invocation.rb:127:in `invoke_command'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/thor-1.2.1/lib/thor.rb:392:in `dispatch'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/railties-7.0.4.2/lib/rails/command/base.rb:87:in `perform'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/railties-7.0.4.2/lib/rails/command.rb:48:in `invoke'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/railties-7.0.4.2/lib/rails/commands.rb:18:in `<main>'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/bootsnap-1.16.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
/usr/local/lib/ruby/bundler/ruby/3.2.0/gems/bootsnap-1.16.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
bin/rails:6:in `<main>'

marclennox avatar Jan 31 '23 18:01 marclennox

Interestingly, the Rails attribute shows up if I invoke the attribute_names method, so the attribute is defined at the AR level.

marclennox avatar Jan 31 '23 18:01 marclennox

An aside, but

I am really glad that you have adopted the Rails attributes API for this gem. I will be honest and say that I was hoping this would happen for some time.

Did you know about the feature in attr_json to pass rails_attribute: true to attr_json to get an accompanying rails attribute? Pretty much what happens now is you always get this. And I polished off a few other rough edges, that I noticed trying to look at things from all sides, but nobody had reported before (if they had, I would have tried to resolve them sooner!)

So it's not really too much different now than if you had just done rails_attribute: true, but either way I'm glad attr_json is working for you, and sorry for the regression!

jrochkind avatar Jan 31 '23 19:01 jrochkind

OK, I did not know that! Is this something that was added perhaps in the past year?

marclennox avatar Jan 31 '23 19:01 marclennox

I tried downgrading attr_json back to pre-2.0.0 and using the rails_attribute: true option. No errors in this case, and I can see the attribute in the list of model attributes.

marclennox avatar Jan 31 '23 19:01 marclennox

It was added in 1.2.0, released June 22, 2020. Actually that CHANGELOG may have been for an improvement to it, it may have been added even earlier, before I started maintaining a reliable CHANGELOG.

But anyway there's a lot to try and document/advertise in attr_json, I am not surprised if you missed it. One reason why I made it just the always-on default now -- plus it simplifies the code a bit. Several years later, it seems to me there's no reason not to want this.

The rails attribute syncing should work even better/more reliably in 2.0.0 .

Clearly some kind of regression has happened in 2.0.0 though. I'd love to fix it if we can figure out how to reproduce it.

jrochkind avatar Jan 31 '23 22:01 jrochkind

This is definitely a weird issue. I was playing around with injecting breakpoints in the underlying Rails attribute definition methods, and after returning to the main console, I was able to instantiate the object and use the setter method without error. I can only get it to work though if I place a breakpoint deep inside these underlying methods, so it almost seems like it's a timing issue.

marclennox avatar Feb 01 '23 17:02 marclennox

I've been able to get around this issue temporarily using a hack. Instead of directly calling attr_json in the abstract base class, I use the Ruby inherited callback to call it on all child classes. In order to prevent attr_json from being called multiple times, I manually prevent it from being called on any base classes.

class Tracking < ApplicationRecord
  self.abstract_class = true

  def self.inherited(child)
    super
    return if %w[Tracking Tracking::Location].include?(child.name)

    child.attr_json :sync_interval, :integer
  end

  ...

marclennox avatar Feb 02 '23 15:02 marclennox

Hi @marclennox , thanks for investigating this! I was unexpectedly away from work this last week with an unplanned situation.

If there is a bug in attr_json, I would really like to fix it! But I'm still confused about what's going on, and unable to reproduce it myself. (I definitely call attr_json in my own code in base classes that are then used by sub-classes! And if I make a simple test setup doing what you say, it also has no problems for me).

At first I thought you meant the problem only exhibited when you put a breakpoint in, but I don't think that's what you meant, you mean it's hard to investigate with a breakpoint because things seem to change in presence of breakpoint. (I have myself noticed some issues with trying to use debugger breakpoints nearby Rails attribute stuff, that I think are there with or without attr_json, and are annoying!)

No luck in making a reproduction case can you can share? If you can share your actual codebase with me, and instructions for reproducing from a console with that, I'd be willing to take a look at it too!

jrochkind avatar Feb 06 '23 20:02 jrochkind

@marclennox I still can't reproduce, but I wonder -- do you use the ActiveRecord becomes method in your code anywhere?

I wonder if the error is due to that, and the same thing as was also reported at #189 -- I was able to reproduce that one, with use of becomes.

jrochkind avatar Feb 28 '23 20:02 jrochkind

No we don't use that. I haven't done any investigation since, as we were able to get around it using the self.interited(child) method as described in my previous comment. At some point I'll see what I can find, just haven't had the time to spend on it. Thanks @jrochkind

marclennox avatar Feb 28 '23 20:02 marclennox

@garrywilliams I have deleted your comments and moved them to a separate issue at #198, because I do not believe it is the same thing.

jrochkind avatar Mar 07 '23 16:03 jrochkind

I am encountering similar situation, see follow case code:

Environments:

  • rails 6.0
  • ruby 2.7.7
  • attr_json 2.0.1
# application_record.rb
include ::AttrJson::Record
attr_json_config(default_container_attribute: :data)

# animal.rb
class Animal < ApplicationRecord

  class << self
    def base_class
      Animal
    end
  end

  attr_json :foots_count, :integer
end

# dog.rb
class Dog < Animal
  class << self
    def base_class
      Dog
    end
  end
end

dog = Dog.new
dog.foots_count # nil
dog.foots_count = 4 # NoMethodError: super

It only happen in test environment, I found three ways to avoid (or resolve) it.

  1. set confog.eager_load = true
    # config/environments/test.rb
    config.eager_load = true
    
  2. change Dog's .base_class to Animal
    # dog.rb
    class << self
      def base_class
        Animal
      end
    end
    
  3. run Animal.new before Dog.new
    Animal.new
    dog = Dog.new
    dog.foots_count = 4
    

I guess it caused by rails autoloading, in production, eager_load default value is true, so it would not happen. In my case, I have to set different base_class for other purpose such as polymorphic. In way 3, when superclass (Animal) call first, its rails attribute(foots_count) loaded to AR model, so call super will not raise NoMethodError.

These three ways is not best way for resolving, now I am trying to figure out how rails autoload to cause this problem :(

marsz avatar Apr 01 '23 21:04 marsz

@marsz Thanks for getting in touch! I'm not sure if this is the same case or not.

But is it possible your case (and maybe OP case) is this Rails-documented issue with Single-Table Inheritance in general, and not special to attr_json? https://edgeguides.rubyonrails.org/autoloading_and_reloading_constants.html#single-table-inheritance.

Your comments on it showing up in test environment (and maybe development too?) but not production reminded me of this. And the things you say solve it seem like they could be this. What do you think?

There is a bit of an annoying thing there in Rails with single-table inheritance. You get it with or without attr_json, it's not an attr_json issue. Perhaps I should mention it in the README, although there's already so much in the README I worry that adding more doesn't increase the chances of anyone seeing anything I add!

I'm not sure if it makes sense that @marclennox's original issue is this too or not, anyt houghts @marclennox ?

jrochkind avatar Apr 02 '23 13:04 jrochkind