Skip to content
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

RFC: Turn off ActiveRecord caching within lock blocks #52

Closed
jmanian opened this issue Nov 16, 2021 · 7 comments
Closed

RFC: Turn off ActiveRecord caching within lock blocks #52

jmanian opened this issue Nov 16, 2021 · 7 comments

Comments

@jmanian
Copy link

jmanian commented Nov 16, 2021

I explained this issue in depth in #47. The short version is that ActiveRecord caching can lead to stale query results within an advisory lock, at a time when it's critical to be working with up-to-date data.

I'm thinking that it may make sense for the with_advisory_lock block to automatically turn off caching via ActiveRecord::QueryCache.uncached for the duration of the block. Or perhaps there should be an argument to with_advisory_lock to control this.


The example use case is using an advisory lock to prevent the double-creation of a record. Take this for example:

def create_or_update_user(external_user)
  transaction do
    with_advisory_lock("create-user-#{external_user.id}", transaction: true) do
      user = User.find_or_initialize_by(external_id: external_user.id)
      user.update_info(external_user)
      user.save!
    end
  end
end

In this example users are created from 3rd party webhooks, and the goal is to create a new user if it doesn't already exist, or update the existing one.

The problem I ran into is that upstream of the lock in my code I had a User.find_by(external_id: external_user.id). The user didn't exist at this point, so the result was being cached as nil. Then inside the lock find_or_initialize_by used the cached result of nil, even though by this time the user record had already been created, thus defeating the purpose of the lock.

I solve this in my code by using uncached to ensure a fresh query:

def create_or_update_user(external_user)
  transaction do
    with_advisory_lock("create-user-#{external_user.id}", transaction: true) do
      self.class.uncached do
        user = User.find_or_initialize_by(external_id: external_user.id)
        user.update_info(external_user)
        user.save!
      end
    end
  end
end
@jmanian
Copy link
Author

jmanian commented Nov 16, 2021

I also asked for input from the Rails team at rails/rails#43634. I thought they might be able to provide some guidance based on how ActiveRecord deals with this in row-level locking (if at all).

@lucasprag
Copy link

My 2 cents on this. I had the exact same problem as described in #47 and adding a ApplicationRecord.uncached block solved my problem as well. This was a really hard issue to solve, so for the sake of others not having this issue, I'm all in favor of the proposed solution above☝️ .

@muxcmux
Copy link
Contributor

muxcmux commented Dec 11, 2021

+1 for this

@mceachen
Copy link
Collaborator

(I'm too rusty with Rails at this point to give an opinion: this seems like an innocuous enough change, but either way, the documentation should be updated to highlight this issue. @seuros would you accept a PR with this change, if @jmanian or @lucasprag or @muxcmux submitted one?)

@seuros
Copy link
Member

seuros commented Dec 11, 2021

Yes.

@seuros
Copy link
Member

seuros commented Jul 31, 2022

Any news on this RFC ? I'm planning in releasing a new version pretty soon. I dropped old version of rails/ruby, so this implementation should be easier now.

@muxcmux
Copy link
Contributor

muxcmux commented Feb 9, 2023

@seuros I've rolled out a PR which adds that option to with_advisory_lock and updated the README, let me know if you have any comments/suggestions!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants