I'm trying to implement create_or_update method for User model, which will either create a new record or update the existing one.
def create_or_update_user(external_user_id, data)
user = User.find_or_initialize_by(external_user_id: external_user_id)
user.title = data[:title].downcase
# and so on - here we assign other attributes from data
user.save! if user.changed?
end
The pitfall here is that this table is being updated concurrently by different processes and when they are trying to modify user with the same external_user_id then race condition happens and ActiveRecord::RecordNotUnique is raised. I tried to use database lock to solve this problem, but it didn't work as expected - exception is still raised sometimes.
The table structure looks like this:
create_table :users do |t|
t.integer :external_user_id, index: { unique: true }
# ...
end
updated method - what I'm doing wrong here?:
def create_or_update_user(external_user_id, data)
user = User.find_or_initialize_by(external_user_id: external_user_id)
user.with_lock do
user.title = data[:title].downcase
# and so on - here we assign other attributes from data
user.save! if user.changed?
end
end
I can't use upsert, because I need model callbacks.
It probably can be fixed by adding
rescue ActiveRecord::RecordNotUnique
retry
end
but I want to use locks for best performance cause the race conditions is not a rare in this part of code.
UPD: added a gist to reproduce this race condition https://gist.github.com/krelly/520a2397f64269f96489c643a7346d0f