2

update_attribute call creates forever loop.

What could be the cause?
Maybe a variand of this:
Prevent infinite loop when updating attributes within after_commit, :on => :create

ruby --version

ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-linux]

commands

rails new loop
cd loop
bundle install
bundle exec rails g scaffold User
bundle exec rails db:migrate
bundle exec rails g migration add_name_to_users name:string
bundle exec rails db:migrate

add the following to app/models/user.rb

class User < ApplicationRecord
  before_create: create_temp_name

  def create_temp_name
    update_attribute :name, 'temp name'
  end
end

Run bundle exec rails s,access http://localhost:3000/users/new and press the create button.

Result:

SystemStackError (stack level too deep):
  
app/models/user.rb:5:in `create_temp_name'
app/models/user.rb:5:in `create_temp_name'

bundle list

  • actioncable (5.1.1)
  • actionmailer (5.1.1)
  • actionpack (5.1.1)
  • actionview (5.1.1)
  • activejob (5.1.1)
  • activemodel (5.1.1)
  • activerecord (5.1.1)
  • activesupport (5.1.1)
  • addressable (2.5.1)
  • arel (8.0.0)
  • bindex (0.5.0)
  • builder (3.2.3)
  • bundler (1.14.6)
  • byebug (9.0.6)
  • capybara (2.13.0)
  • childprocess (0.7.0)
  • coffee-rails (4.2.1)
  • coffee-script (2.4.1)
  • coffee-script-source (1.12.2)
  • concurrent-ruby (1.0.5)
  • erubi (1.6.0)
  • execjs (2.7.0)
  • ffi (1.9.18)
  • globalid (0.4.0)
  • i18n (0.8.1)
  • jbuilder (2.6.4)
  • listen (3.1.5)
  • loofah (2.0.3)
  • mail (2.6.5)
  • method_source (0.8.2)
  • mime-types (3.1)
  • mime-types-data (3.2016.0521)
  • mini_portile2 (2.1.0)
  • minitest (5.10.2)
  • multi_json (1.12.1)
  • nio4r (2.0.0)
  • nokogiri (1.7.2)
  • public_suffix (2.0.5)
  • puma (3.8.2)
  • rack (2.0.2)
  • rack-test (0.6.3)
  • rails (5.1.1)
  • rails-dom-testing (2.0.3)
  • rails-html-sanitizer (1.0.3)
  • railties (5.1.1)
  • rake (12.0.0)
  • rb-fsevent (0.9.8)
  • rb-inotify (0.9.8)
  • ruby_dep (1.5.0)
  • rubyzip (1.2.1)
  • sass (3.4.23)
  • sass-rails (5.0.6)
  • selenium-webdriver (3.4.0)
  • spring (2.0.1)
  • spring-watcher-listen (2.0.1)
  • sprockets (3.7.1)
  • sprockets-rails (3.2.0)
  • sqlite3 (1.3.13)
  • thor (0.19.4)
  • thread_safe (0.3.6)
  • tilt (2.0.7)
  • turbolinks (5.0.1)
  • turbolinks-source (5.0.3)
  • tzinfo (1.2.3)
  • uglifier (3.2.0)
  • web-console (3.5.1)
  • websocket (1.2.4)
  • websocket-driver (0.6.5)
  • websocket-extensions (0.1.2)
  • xpath (2.0.0)
Community
  • 1
  • 1
TastyCatFood
  • 1,632
  • 1
  • 15
  • 27
  • from where are you calling this method 'create_activation_digest' – rohan May 13 '17 at 13:17
  • It must be triggering some callback in the `User` model. – Eyeslandic May 13 '17 at 13:43
  • yea, probably before or after update. – rohan May 13 '17 at 16:06
  • Thanks for posting. The curious thing is pry's `next` command does not take me anywhere but keeps the execution point at `update_attribute` method call. So, it does not look like `create_activation_digest` is caught in callback loop. Well, I can simply call `@user.save` in controller's create action to avoid this, but gosh ruby's magic is too much for the uninitiated . – TastyCatFood May 13 '17 at 16:45
  • Could you include the relevant code in users_controller.rb? – kcdragon May 14 '17 at 03:38
  • hi kcdragon, updated. I have successfully recreated the problem in a new app;see the edit2. Looks like update_attribute call in call backs is not a good idea, which is fine but hmm what's causing this weirdness? – TastyCatFood May 14 '17 at 05:40
  • @TastyCatFood `update_attribute` is an alias of `save`, therefore you're recursively calling the `before_save` callback causing an infinite loop / stack overflow. see my answer below – nickcamillo Feb 22 '18 at 19:34

1 Answers1

2

You shouldn't need to call update_attribute in the callback, because the save method will be called after the before_create callback executes. You can just assign the value like name = 'temp_name' in the callback, and allow it to be saved during object creation. update_attribute is an alias of save, so you're in effect saving the initialized object before the save function is called again when the callback completes, causing an infinite loop / stack overflow.

To help understand this, if you instantiate a new blank object in the rails console, then run update_attribute on one of it's attributes, you'll notice that the object is saved/assigned an id:

# rails c

user = User.new
user.persisted?
  => false
user.update_attribute('name', 'Nick')
user.persisted?
  => true

So by calling update_attribute, you're invoking an additional instance of the before_create callback, because before the model allows the object to be saved to the database during the update_attribute call, it invokes the before_create callback.

nickcamillo
  • 340
  • 2
  • 9