Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Receiving "Attempted to update a stale object: User" exception when updating current_user before logging out

I enabled optimistic locking on my User model to handle possible conflicts in various parts of my codebase. However, I'm experiencing an unexpected conflict and I don't know how to handle it because I don't know what's causing it.

I'm using the Devise gem for authentication and I'm using the before_logout method to reset a security token...

class SessionsController < Devise::SessionsController
  after_filter :after_login, :only => :create
  before_filter :before_logout, :only => :destroy

  def after_login
    # logic to set the security token
  end

  def before_logout
    current_user.update(security_token: nil) # <--"Attempted to update a stale object: User"
  end
end

I'm new to Rails (only a few weeks in), so I don't really know where else in my code to look or why this code is throwing the exception. Any suggestions or ideas are greatly appreciated.

Thanks in advance for your wisdom!

like image 645
BeachRunnerFred Avatar asked Oct 18 '14 18:10

BeachRunnerFred


3 Answers

You need to set a default value (0) for the lock_version column in the migration file

like image 200
Deepak Sharma Avatar answered Oct 11 '22 07:10

Deepak Sharma


Optimistic locking means you want multiple threads to be able to edit the same user record.

What's happening in your app is that two (or more) edits are happening to the same user record, and your app is correctly detecting that, and raising the suitable error.

What you would typically do is handle the error:

  • For example, you could print a message such as "Sorry, someone else edited this, so your changes were not saved".

  • For example, you could code any way to reconcile the edits, such as detecting that one edit changed the person's name, wherease the other edit changed the person's phone number, and then you could code a way to merge these two into one save.

In general, you can also use user.reload to fetch a fresh version of the user.

I suggest you first try turning off optimistic locking, while you're learning Rails. Optimistic locking is a great optimization for speed, yet it shouldn't be needed to get a typical Rails & Devise app running successfully.

You asked about what specifically Devise may be doing:

  • When a user record is updated, the typical Rails ActiveRecord setup updates a table field updated_at with the current timestamp.

  • A typical Devise setup of modules such as trackable updates the user record with the most recent sign-in and/or sign-out timestamps.

An easy way to watch what's happening is to look at the lock_version field:

  • When Rails loads a record (e.g. a user) then Rails increments the lock version number.

  • When your app saves the user, Rails ActiveRecord compares the user object lock_version number with the database lock_version number.

  • If the numbers match, then the save succeeds; if the number mismatch, then Rails raises the stale object exception.

Example code:

def before_logout
  logger.debug current_user.lock_version
  current_user.reload      
  logger.debug current_user.lock_version
  current_user.update(security_token: nil) 
end

Devise and some of its various modules add fields to the user table. This is quick and convenient for typical web apps, yet these added fields tend to disrupt caching, locking, persistence, serializing, and the like.

(As an aside, you may want to consider evaluating Devise vs. other security solutions. One in particular that I like is called Sorcery; it provides a toolkit of a handful of methods that you can assemble the way you want. In my personal experience, Sorcery is a great way to learn exactly what each step is doing in your own code, and you can control the caching and locking in the ways that you want.)

like image 7
joelparkerhenderson Avatar answered Nov 20 '22 11:11

joelparkerhenderson


I've faced a similar issue. The error is due to the difference in the lock_version of the object you're trying to update. You can attempt calling reload on the object before performing the update. This would ensure the object is of the most recent lock_version.

like image 3
osifo Avatar answered Nov 20 '22 10:11

osifo