-
Notifications
You must be signed in to change notification settings - Fork 171
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
endIf 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
endA 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 committedThe 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