Shopify / Shopify/identity_cache

Cache not expiring when model has empty update in transaction

Open
#555 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Ruby
Stars
2k
Forks
174
Avg merge
13m
Merged PRs (30d)
3

Description

We've encountered a situation where the cache isn't expired if, within a transaction, a model has a "non-update" followed by an update. Here's the code that created a stale cache in our application:

def update_user(id, attributes) # hash
  ApplicationRecord.transaction do
    User.find(id).update(attributes.except(:email))
    update_sign_in_information(id, attributes[:email])
  end
end

def update_sign_in_information(user_id, email)
  user = User.find(user_id)
  user.update(email: email)
  # other stuff
end

If only the email attribute is passed in, this leads to a stale cache. Here are some more examples that demonstrate the bug:

class Tester
  def self.test
    reset
    ApplicationRecord.transaction do
      user = User.first
      user.update({})
      user.update(name: "test2")
    end
    puts "###### user name: #{User.fetch(User.first.id).name}" # test2, good

    reset
    ApplicationRecord.transaction do
      User.first.update({})
      User.first.update(name: "test2")
    end
    puts "###### user name: #{User.fetch(User.first.id).name}" # test1, stale

    reset
    ApplicationRecord.transaction do
      User.first.update(name: "test1")
      User.first.update(name: "test2")
    end
    puts "###### user name: #{User.fetch(User.first.id).name}" # test1, stale

  end

  def self.reset
    User.first.update(name: "test1")
    User.fetch(User.first.id) # fill the cache
  end
end

A similar "bug" exists in ActiveRecord:

class User < ApplicationRecord
  after_commit :print_name

  def print_name
    puts user.name
  end
end

ApplicationRecord.transaction do
  User.first.update(name: "test1")
  User.first.update(name: "test2")
end

# prints test1 after the whole transaction is committed

The bug goes pretty deep and seems to involve the activerecord and ar_transaction_changes gems. Essentially, the @transaction_changed_attributes variable that IdentityCache's _run_commit_callbacks method is checking is attached to the record, but the after_commit callback is only being called on the first instance even if the transaction contains multiple updates. Here's the monkey patch my team is currently considering adding to address the issue, but could lead to performance regressions:

def _run_commit_callbacks
  # if destroyed? || transaction_changed_attributes.present?
    expire_cache
  # end
  super
end

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at IdentityCache's _run_commit_callbacks method and trace how @transaction_changed_attributes is set and how ActiveRecord and ar_transaction_changes invoke after_commit for multiple updates. Reproduce the three transaction examples, then verify that the cache reflects the final committed model state without regressing ordinary cache expiration.

Written by the indexing model from the issue text.

Assessment

Tech stack
ruby
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.