From e222f153272694467be2b6d8505f4eef3065a0b1 Mon Sep 17 00:00:00 2001 From: Dave Pijuan-Nomura Date: Tue, 14 Apr 2026 15:53:53 -0400 Subject: [PATCH] Handle race condition --- lib/salesforce_ar_sync/salesforce_sync.rb | 28 +++++++++++++++---- .../salesforce_ar_sync_spec.rb | 28 +++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/lib/salesforce_ar_sync/salesforce_sync.rb b/lib/salesforce_ar_sync/salesforce_sync.rb index 7efffc2..6fe0a81 100644 --- a/lib/salesforce_ar_sync/salesforce_sync.rb +++ b/lib/salesforce_ar_sync/salesforce_sync.rb @@ -71,24 +71,42 @@ module ClassMethods def salesforce_update(attributes = {}) raise ArgumentError, "#{salesforce_id_attribute_name} parameter required" if attributes[salesforce_id_attribute_name].blank? + retried = false + begin + object = find_record(attributes) || build_record(attributes) + + object.salesforce_process_update(attributes) if object && (object.salesforce_updated_at.nil? || (object.salesforce_updated_at && object.salesforce_updated_at < Time.parse(attributes[:SystemModstamp]))) + rescue ActiveRecord::RecordNotUnique + if retried + raise + else + retried = true + retry + end + end + end + + def find_record(attributes) data_source = unscoped_updates ? unscoped : self object = data_source.find_by(salesforce_id: attributes[salesforce_id_attribute_name]) object ||= data_source.find_by(activerecord_web_id_attribute_name => attributes[salesforce_web_id_attribute_name]) if salesforce_sync_web_id? && attributes[salesforce_web_id_attribute_name] + return object if object.present? - if !object && additional_lookup_fields + if additional_lookup_fields additional_lookup_fields.each do |attribute_name, salesforce_attribute_name| object = data_source.find_by(attribute_name => attributes[salesforce_attribute_name]) if attributes[salesforce_attribute_name] + break if object end + return object end + end - if object.nil? - object = new + def build_record(attributes) + new.tap do |object| salesforce_default_attributes_for_create.merge(salesforce_id: attributes[salesforce_id_attribute_name]).each_pair do |k, v| object.send("#{k}=", v) end end - - object.salesforce_process_update(attributes) if object && (object.salesforce_updated_at.nil? || (object.salesforce_updated_at && object.salesforce_updated_at < Time.parse(attributes[:SystemModstamp]))) end end diff --git a/spec/salesforce_ar_sync/salesforce_ar_sync_spec.rb b/spec/salesforce_ar_sync/salesforce_ar_sync_spec.rb index e2bba45..3590b75 100644 --- a/spec/salesforce_ar_sync/salesforce_ar_sync_spec.rb +++ b/spec/salesforce_ar_sync/salesforce_ar_sync_spec.rb @@ -145,6 +145,34 @@ class TestSyncable < ActiveRecord::Base Contact.salesforce_update(Id: sf_id) end + + context 'when an existing record is not found but ActiveRecord::RecordNotUnique is raised on save' do + let(:contact) do + Contact.new( + first_name: 'Bob', + last_name: 'Smith', + phone: '519 555-1212', + email: 'bsmith@example.com', + salesforce_skip_sync: true + ) + end + let(:return_values) { [:raise, true] } + + before do + allow(Contact).to receive(:salesforce_sync_web_id?).and_return(false) + allow(Contact).to receive(:unscoped_updates?).and_return(false) + # simulate the record being created after the find but before the save + allow(Contact).to receive(:new).and_return(contact) + allow(contact).to receive(:save!).exactly(2).times do + return_value = return_values.shift + return_value == :raise ? (contact.save and raise(ActiveRecord::RecordNotUnique)) : return_value + end + end + it 'retries and updates the found record' do + expect(Contact).to receive(:new).once + expect { Contact.salesforce_update(Id: 321, WebId__c: 432) }.not_to raise_exception + end + end end describe '.salesforce_id_attribute_name' do