Umang Raghuvanshi
Umang Raghuvanshi

Reputation: 1258

Rails infinite loop while updating other record's value during `before_save`

I have this model in Rails (trimmed to the relevant parts)

class Session < ActiveRecord::Base
  belongs_to :user
  before_save :invalidate_existing_sessions


  def invalidate_existing_sessions
    Session.where(user_id: user.id, current: true).each { |sess| sess.update_attributes(current: false) }
  end
end

However, when a record is created and about to be saved, the server goes into an infinite loop.

Here are the server logs

Processing by V1::SessionsController#create as */*
  Parameters: {"email"=>"[email protected]", "password"=>"[FILTERED]", "session"=>{}}
  User Load (0.7ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = $1 LIMIT 1  [["email", "[email protected]"]]
   (0.2ms)  BEGIN
  Session Load (0.7ms)  SELECT "sessions".* FROM "sessions" WHERE "sessions"."user_id" = $1 AND "sessions"."current" = $2  [["user_id", 1
], ["current", true]]
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
  CACHE (0.0ms)  SELECT "sessions".* FROM "sessions" WHERE "sessions"."user_id" = $1 AND "sessions"."current" = $2  [["user_id", 1], ["cu
rrent", true]]
  CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
  CACHE (0.0ms)  SELECT "sessions".* FROM "sessions" WHERE "sessions"."user_id" = $1 AND "sessions"."current" = $2  [["user_id", 1], ["cu
rrent", true]]
  CACHE (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
  CACHE (0.0ms)  SELECT "sessions".* FROM "sessions" WHERE "sessions"."user_id" = $1 AND "sessions"."current" = $2  [["user_id", 1], ["cu
rrent", true]]

A bit later, this is what the log turns into

  app/models/session.rb:12:in `invalidate_existing_sessions'
  app/models/session.rb:12:in `block in invalidate_existing_sessions'
  app/models/session.rb:12:in `invalidate_existing_sessions'
  app/models/session.rb:12:in `block in invalidate_existing_sessions'
  app/models/session.rb:12:in `invalidate_existing_sessions'
  app/models/session.rb:12:in `block in invalidate_existing_sessions'
  app/models/session.rb:12:in `invalidate_existing_sessions'

Any ideas? I'm using Rails 5 alpha.

Upvotes: 0

Views: 1188

Answers (3)

Jeiwan
Jeiwan

Reputation: 954

You're running update_attributes in before_save, that means you're saving before save. That's why it goes into an infinite loop.

Upvotes: 1

Umang Raghuvanshi
Umang Raghuvanshi

Reputation: 1258

Even though all of the above answers worked for me, this is what I found simplest and I ended up using.

def invalidate_existing_sessions
  Session.where(user_id: user.id, current: true).each { |sess| sess.update_column(:current, false) }
end

Turns out update_column doesn't call any callbacks, but as an disadvantage it doesn't update updated_at if you're using timestamps in your model.

Upvotes: 2

SteveTurczyn
SteveTurczyn

Reputation: 36880

It's because your before_save method does this...

sess.update_attributes(current: false)

Since update_attributes calls before_save you are (as you say) in an infinite loop.

So you need to skip the callbacks

class Session < ActiveRecord::Base
  attr_accessor :skip_callbacks
  before_save :invalidate_existing_sessions, unless: :skip_callbacks

  def invalidate_existing_sessions
    Session.where(user_id: user.id, current: true).each do |sess|  
      sess.skip_callbacks = true
      sess.update_attributes(current: false) 
    end
  end

Upvotes: 2

Related Questions