diff --git a/app/jobs/concerns/has_active_job_lock.rb b/app/jobs/concerns/has_active_job_lock.rb index 0c09bf92f..57d784957 100644 --- a/app/jobs/concerns/has_active_job_lock.rb +++ b/app/jobs/concerns/has_active_job_lock.rb @@ -89,6 +89,12 @@ module HasActiveJobLock ActiveJobLock.transaction(isolation: :serializable) do yield end + # PostgeSQL prevents locking on records that are already locked + # for UPDATE in Serializable Isolation Level transactions, + # but it's safe to retry as described in the docs: + # https://www.postgresql.org/docs/10/transaction-iso.html + rescue PG::TRSerializationFailure + retry rescue ActiveRecord::RecordNotUnique existing_active_job_lock! end diff --git a/spec/jobs/concerns/has_active_job_lock_spec.rb b/spec/jobs/concerns/has_active_job_lock_spec.rb index e312e0274..d7354eeae 100644 --- a/spec/jobs/concerns/has_active_job_lock_spec.rb +++ b/spec/jobs/concerns/has_active_job_lock_spec.rb @@ -100,6 +100,29 @@ RSpec.describe HasActiveJobLock, type: :job do end.to have_enqueued_job(job_class).exactly(:twice) end end + + if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' + context "when PG::TRSerializationFailure 'Could not serialize access due to concurrent update' is raised" do + + it 'retries execution until succeed' do + allow(ActiveRecord::Base.connection).to receive(:open_transactions).and_return(0) + allow(ActiveJobLock).to receive(:transaction).and_call_original + exception_raised = false + allow(ActiveJobLock).to receive(:transaction).with(isolation: :serializable) do |&block| + + if !exception_raised + exception_raised = true + raise PG::TRSerializationFailure, 'Could not serialize access due to concurrent update' + end + + block.call + end + + expect { job_class.perform_later }.to have_enqueued_job(job_class).exactly(:once) + expect(exception_raised).to be true + end + end + end end include_examples 'handle locking of jobs'