-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make #increment_failed_attempts
concurrency safe
#4996
Conversation
As reported in #4981, the method `#increment_failed_attempts` of `Devise::Models::Lockable` was not concurrency safe. The increment operation was being done in two steps: first the value was read from the database, and then incremented by 1. This may result in wrong values if two requests try to update the value concurrently. For example: Browser1 -------> Read `failed_attempts` from DB (1) -------> Increment `failed_attempts` to 2 Browser2 -------> Read `failed_attempts` from DB (1) -------> Increment `failed_attempts` to 2 In the example above, `failed_attempts` should have been set to 3, but it will be set to 2. This commit handles this case by calling ActiveRecord's `#increment!` method, which will do both steps at once, reading the value straight from the database. More info: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-increment-21 Co-authored-by: Marcos Ferreira <[email protected]>
`ActiveRecord::Persistence#increment!` started using `ActiveRecord::CounterCache.update_counters` only in Rails 5, which means we got some broken specs in Rails 4. It turns out that `ActiveRecord::CounterCache.increment_counter` exists since Rails 4 and it also calls `.update_counters`, so we're using it instead. This commit also adds a `ActiveRecord::AttributeMethods::Dirty#reload` call to ensure that the application gets the updated value - i.e. that other request might have updated.
Backported from heartcombo#4996
Hello, @tegon
We are investigating similar or related behavior reported for latest version, so just in case.. |
Hi @glebsts, the code you mentioned is for the other case: resetting the failed attempts account - i.e. unlocking a user. Thanks! |
As reported in #4981, the method
#increment_failed_attempts
ofDevise::Models::Lockable
wasnot concurrency safe. The increment operation was being done in two steps: first the value was read from the database, and then incremented by 1. This may result in wrong values if two requests try to update the value concurrently. For example:
In the example above,
failed_attempts
should have been set to 3, but it will be set to 2.This pull request handles this case by calling
ActiveRecord::CounterCache.increment_counter
method, which will do both steps at once, reading the value straight from the database.This pull request also adds a
ActiveRecord::AttributeMethods::Dirty#reload
call to ensure that the application gets the updated value - i.e. that other request might have updated.Although this does not ensure that the value is in fact the most recent one - other request could've updated it after the
reload
call - it seems good enough for this implementation.Even if a request does not locks the account because it has a stale value, the next one - that updated that value - will do it. That's why we decided not to use a pessimistic lock here.
Closes #4981.