123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- require 'rails_helper'
- RSpec.describe HasActiveJobLock, type: :job do
- before do
- stub_const job_class_namespace, job_class
- end
- let(:job_class_namespace) { 'UniqueActiveJob' }
- let(:job_class) do
- Class.new(ApplicationJob) do
- include HasActiveJobLock
- cattr_accessor :perform_counter, default: 0
- def perform(...)
- self.class.perform_counter += 1
- end
- end
- end
- shared_examples 'handle locking of jobs' do
- context 'performing job is present' do
- before { create(:active_job_lock, lock_key: job_class.name, created_at: 1.minute.ago, updated_at: 1.second.ago) }
- it 'allows enqueueing of perform_later jobs' do
- expect { job_class.perform_later }.to have_enqueued_job(job_class).exactly(:once)
- end
- it 'allows execution of perform_now jobs' do
- expect { job_class.perform_now }.to change(job_class, :perform_counter).by(1)
- end
- end
- context 'enqueued job is present' do
- before { job_class.perform_later }
- it 'does not enqueue perform_later jobs' do
- expect { job_class.perform_later }.not_to have_enqueued_job(job_class)
- end
- it 'allows execution of perform_now jobs' do
- expect { job_class.perform_now }.to change(job_class, :perform_counter).by(1)
- end
- end
- context 'running perform_now job' do
- let(:job_class) do
- Class.new(super()) do
- cattr_accessor :task_completed, default: false
- def perform(long_running: false)
- if long_running
- sleep(0.1) until self.class.task_completed
- end
- # don't pass parameters to super method
- super()
- end
- end
- end
- let!(:thread) { Thread.new { job_class.perform_now(long_running: true) } }
- after do
- job_class.task_completed = true
- thread.join
- end
- it 'enqueues perform_later jobs' do
- expect { job_class.perform_later }.to have_enqueued_job(job_class)
- end
- it 'allows execution of perform_now jobs' do
- expect { job_class.perform_now }.to change(job_class, :perform_counter).by(1)
- end
- context 'when Delayed::Job gets destroyed' do
- before do
- ActiveJob::Base.queue_adapter = :delayed_job
- end
- it 'is ensured that ActiveJobLock gets removed' do
- job = job_class.perform_later
- expect do
- Delayed::Job.find(job.provider_job_id).destroy!
- end.to change {
- ActiveJobLock.exists?(lock_key: job.lock_key, active_job_id: job.job_id)
- }.to(false)
- end
- end
- end
- context 'dynamic lock key' do
- let(:job_class) do
- Class.new(super()) do
- def lock_key
- "#{super}/#{arguments[0]}/#{arguments[1]}"
- end
- end
- end
- it 'queues one job per lock key' do
- expect do
- 2.times { job_class.perform_later('User', 23) }
- job_class.perform_later('User', 42)
- end.to have_enqueued_job(job_class).exactly(:twice)
- end
- end
- context "when ActiveRecord::SerializationFailure 'PG::TRSerializationFailure: ERROR: 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 ActiveRecord::SerializationFailure, 'PG::TRSerializationFailure: ERROR: 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
- context "when ActiveRecord::Deadlocked 'Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction' 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 ActiveRecord::Deadlocked, 'Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction'
- 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
- include_examples 'handle locking of jobs'
- context 'when has custom lock key' do
- let(:job_class) do
- Class.new(super()) do
- def lock_key
- 'custom_lock_key'
- end
- end
- end
- include_examples 'handle locking of jobs'
- end
- context 'when has invalid custom lock key' do
- let(:job_class) do
- Class.new(super()) do
- def lock_key
- raise 'Cannot generate lock key anymore'
- end
- end
- end
- it 'does not allow enqueueing of perform_later jobs' do
- expect { job_class.perform_later }.not_to have_enqueued_job(job_class)
- end
- it 'does not allow execution of perform_now jobs' do
- expect { job_class.perform_now }.not_to change(job_class, :perform_counter)
- end
- end
- # https://github.com/zammad/zammad/issues/4141
- context 'when key becomes invalid after enqueueing' do
- let(:job_class) do
- Class.new(super()) do
- cattr_accessor :allow_lock_key, default: true
- def lock_key
- raise 'Cannot generate lock key anymore' if !allow_lock_key
- 'lock key'
- end
- end
- end
- it 'safely handles error in generating lock key', performs_jobs: true do
- job_class.perform_later
- job_class.allow_lock_key = false
- expect { perform_enqueued_jobs(only: job_class) }
- .not_to raise_error
- end
- end
- end
|