has_active_job_lock_spec.rb 5.0 KB

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