has_active_job_lock_spec.rb 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe HasActiveJobLock, type: :job do
  4. before do
  5. stub_const job_class_namespace, job_class
  6. end
  7. let(:job_class_namespace) { 'UniqueActiveJob' }
  8. let(:job_class) do
  9. Class.new(ApplicationJob) do
  10. include HasActiveJobLock
  11. cattr_accessor :perform_counter, default: 0
  12. def perform(...)
  13. self.class.perform_counter += 1
  14. end
  15. end
  16. end
  17. shared_examples 'handle locking of jobs' do
  18. context 'performing job is present' do
  19. before { create(:active_job_lock, lock_key: job_class.name, created_at: 1.minute.ago, updated_at: 1.second.ago) }
  20. it 'allows enqueueing of perform_later jobs' do
  21. expect { job_class.perform_later }.to have_enqueued_job(job_class).exactly(:once)
  22. end
  23. it 'allows execution of perform_now jobs' do
  24. expect { job_class.perform_now }.to change(job_class, :perform_counter).by(1)
  25. end
  26. end
  27. context 'enqueued job is present' do
  28. before { job_class.perform_later }
  29. it 'does not enqueue perform_later jobs' do
  30. expect { job_class.perform_later }.not_to have_enqueued_job(job_class)
  31. end
  32. it 'allows execution of perform_now jobs' do
  33. expect { job_class.perform_now }.to change(job_class, :perform_counter).by(1)
  34. end
  35. end
  36. context 'running perform_now job' do
  37. let(:job_class) do
  38. Class.new(super()) do
  39. cattr_accessor :task_completed, default: false
  40. def perform(long_running: false)
  41. if long_running
  42. sleep(0.1) until self.class.task_completed
  43. end
  44. # don't pass parameters to super method
  45. super()
  46. end
  47. end
  48. end
  49. let!(:thread) { Thread.new { job_class.perform_now(long_running: true) } }
  50. after do
  51. job_class.task_completed = true
  52. thread.join
  53. end
  54. it 'enqueues perform_later jobs' do
  55. expect { job_class.perform_later }.to have_enqueued_job(job_class)
  56. end
  57. it 'allows execution of perform_now jobs' do
  58. expect { job_class.perform_now }.to change(job_class, :perform_counter).by(1)
  59. end
  60. context 'when Delayed::Job gets destroyed' do
  61. before do
  62. ActiveJob::Base.queue_adapter = :delayed_job
  63. end
  64. it 'is ensured that ActiveJobLock gets removed' do
  65. job = job_class.perform_later
  66. expect do
  67. Delayed::Job.find(job.provider_job_id).destroy!
  68. end.to change {
  69. ActiveJobLock.exists?(lock_key: job.lock_key, active_job_id: job.job_id)
  70. }.to(false)
  71. end
  72. end
  73. end
  74. context 'dynamic lock key' do
  75. let(:job_class) do
  76. Class.new(super()) do
  77. def lock_key
  78. "#{super}/#{arguments[0]}/#{arguments[1]}"
  79. end
  80. end
  81. end
  82. it 'queues one job per lock key' do
  83. expect do
  84. 2.times { job_class.perform_later('User', 23) }
  85. job_class.perform_later('User', 42)
  86. end.to have_enqueued_job(job_class).exactly(:twice)
  87. end
  88. end
  89. context "when ActiveRecord::SerializationFailure 'PG::TRSerializationFailure: ERROR: could not serialize access due to concurrent update' is raised" do
  90. it 'retries execution until succeed' do
  91. allow(ActiveRecord::Base.connection).to receive(:open_transactions).and_return(0)
  92. allow(ActiveJobLock).to receive(:transaction).and_call_original
  93. exception_raised = false
  94. allow(ActiveJobLock).to receive(:transaction).with(isolation: :serializable) do |&block|
  95. if !exception_raised
  96. exception_raised = true
  97. raise ActiveRecord::SerializationFailure, 'PG::TRSerializationFailure: ERROR: could not serialize access due to concurrent update'
  98. end
  99. block.call
  100. end
  101. expect { job_class.perform_later }.to have_enqueued_job(job_class).exactly(:once)
  102. expect(exception_raised).to be true
  103. end
  104. end
  105. context "when ActiveRecord::Deadlocked 'Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction' is raised" do
  106. it 'retries execution until succeed' do
  107. allow(ActiveRecord::Base.connection).to receive(:open_transactions).and_return(0)
  108. allow(ActiveJobLock).to receive(:transaction).and_call_original
  109. exception_raised = false
  110. allow(ActiveJobLock).to receive(:transaction).with(isolation: :serializable) do |&block|
  111. if !exception_raised
  112. exception_raised = true
  113. raise ActiveRecord::Deadlocked, 'Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction'
  114. end
  115. block.call
  116. end
  117. expect { job_class.perform_later }.to have_enqueued_job(job_class).exactly(:once)
  118. expect(exception_raised).to be true
  119. end
  120. end
  121. end
  122. include_examples 'handle locking of jobs'
  123. context 'when has custom lock key' do
  124. let(:job_class) do
  125. Class.new(super()) do
  126. def lock_key
  127. 'custom_lock_key'
  128. end
  129. end
  130. end
  131. include_examples 'handle locking of jobs'
  132. end
  133. context 'when has invalid custom lock key' do
  134. let(:job_class) do
  135. Class.new(super()) do
  136. def lock_key
  137. raise 'Cannot generate lock key anymore'
  138. end
  139. end
  140. end
  141. it 'does not allow enqueueing of perform_later jobs' do
  142. expect { job_class.perform_later }.not_to have_enqueued_job(job_class)
  143. end
  144. it 'does not allow execution of perform_now jobs' do
  145. expect { job_class.perform_now }.not_to change(job_class, :perform_counter)
  146. end
  147. end
  148. # https://github.com/zammad/zammad/issues/4141
  149. context 'when key becomes invalid after enqueueing' do
  150. let(:job_class) do
  151. Class.new(super()) do
  152. cattr_accessor :allow_lock_key, default: true
  153. def lock_key
  154. raise 'Cannot generate lock key anymore' if !allow_lock_key
  155. 'lock key'
  156. end
  157. end
  158. end
  159. it 'safely handles error in generating lock key', performs_jobs: true do
  160. job_class.perform_later
  161. job_class.allow_lock_key = false
  162. expect { perform_enqueued_jobs(only: job_class) }
  163. .not_to raise_error
  164. end
  165. end
  166. end