# frozen_string_literal: true require "spec_helper" RSpec.describe Gitlab::Database::Migrations::SidekiqHelpers do let(:model) do ActiveRecord::Migration.new.extend(described_class) end describe "sidekiq migration helpers", :redis do let(:worker) do Class.new do include Sidekiq::Worker sidekiq_options queue: "test" def self.name "WorkerClass" end end end let(:worker_two) do Class.new do include Sidekiq::Worker sidekiq_options queue: "test_two" def self.name "WorkerTwoClass" end end end let(:same_queue_different_worker) do Class.new do include Sidekiq::Worker sidekiq_options queue: "test" def self.name "SameQueueDifferentWorkerClass" end end end let(:unrelated_worker) do Class.new do include Sidekiq::Worker sidekiq_options queue: "unrelated" def self.name "UnrelatedWorkerClass" end end end before do stub_const(worker.name, worker) stub_const(worker_two.name, worker_two) stub_const(unrelated_worker.name, unrelated_worker) stub_const(same_queue_different_worker.name, same_queue_different_worker) end describe "#sidekiq_remove_jobs", :clean_gitlab_redis_queues do def clear_queues Sidekiq::Queue.new("test").clear Sidekiq::Queue.new("test_two").clear Sidekiq::Queue.new("unrelated").clear Sidekiq::RetrySet.new.clear Sidekiq::ScheduledSet.new.clear end around do |example| clear_queues Sidekiq::Testing.disable!(&example) clear_queues end context 'when inside a transaction' do it 'raises RuntimeError' do expect(model).to receive(:transaction_open?).and_return(true) expect { model.sidekiq_remove_jobs(job_klasses: [worker.name]) } .to raise_error(RuntimeError) end end context 'when outside a transaction' do before do allow(model).to receive(:transaction_open?).and_return(false) allow(model).to receive(:disable_statement_timeout).and_call_original end context "when the constant is not defined" do it "doesn't try to delete it" do my_non_constant = +"SomeThingThatIsNotAConstant" expect(Sidekiq::Queue).not_to receive(:new).with(any_args) model.sidekiq_remove_jobs(job_klasses: [my_non_constant]) end end context "when the constant is defined" do it "will use it find job instances to delete" do my_constant = worker.name expect(Sidekiq::Queue) .to receive(:new) .with(worker.queue) .and_call_original model.sidekiq_remove_jobs(job_klasses: [my_constant]) end end it "removes all related job instances from the job classes' queues" do worker.perform_async worker_two.perform_async same_queue_different_worker.perform_async unrelated_worker.perform_async worker_queue = Sidekiq::Queue.new(worker.queue) worker_two_queue = Sidekiq::Queue.new(worker_two.queue) unrelated_queue = Sidekiq::Queue.new(unrelated_worker.queue) expect(worker_queue.size).to eq(2) expect(worker_two_queue.size).to eq(1) expect(unrelated_queue.size).to eq(1) model.sidekiq_remove_jobs(job_klasses: [worker.name, worker_two.name]) expect(worker_queue.size).to eq(1) expect(worker_two_queue.size).to eq(0) expect(worker_queue.map(&:klass)).not_to include(worker.name) expect(worker_queue.map(&:klass)).to include( same_queue_different_worker.name ) expect(worker_two_queue.map(&:klass)).not_to include(worker_two.name) expect(unrelated_queue.size).to eq(1) end context "when job instances are in the scheduled set" do it "removes all related job instances from the scheduled set" do worker.perform_in(1.hour) worker_two.perform_in(1.hour) unrelated_worker.perform_in(1.hour) scheduled = Sidekiq::ScheduledSet.new expect(scheduled.size).to eq(3) expect(scheduled.map(&:klass)).to include( worker.name, worker_two.name, unrelated_worker.name ) model.sidekiq_remove_jobs(job_klasses: [worker.name, worker_two.name]) expect(scheduled.size).to eq(1) expect(scheduled.map(&:klass)).not_to include(worker.name) expect(scheduled.map(&:klass)).not_to include(worker_two.name) expect(scheduled.map(&:klass)).to include(unrelated_worker.name) end end context "when job instances are in the retry set" do include_context "when handling retried jobs" it "removes all related job instances from the retry set" do retry_in(worker, 1.hour) retry_in(worker, 2.hours) retry_in(worker, 3.hours) retry_in(worker_two, 4.hours) retry_in(unrelated_worker, 5.hours) retries = Sidekiq::RetrySet.new expect(retries.size).to eq(5) expect(retries.map(&:klass)).to include( worker.name, worker_two.name, unrelated_worker.name ) model.sidekiq_remove_jobs(job_klasses: [worker.name, worker_two.name]) expect(retries.size).to eq(1) expect(retries.map(&:klass)).not_to include(worker.name) expect(retries.map(&:klass)).not_to include(worker_two.name) expect(retries.map(&:klass)).to include(unrelated_worker.name) end end # Imitate job deletion returning zero and then non zero. context "when job fails to be deleted" do let(:job_double) do instance_double( "Sidekiq::JobRecord", klass: worker.name ) end context "and does not work enough times in a row before max attempts" do it "tries the max attempts without succeeding" do worker.perform_async allow(job_double).to receive(:delete).and_return(true) # Scheduled set runs last so only need to stub out its values. allow(Sidekiq::ScheduledSet) .to receive(:new) .and_return([job_double]) expect(model.sidekiq_remove_jobs(job_klasses: [worker.name])) .to eq( { attempts: 5, success: false } ) end end context "and then it works enough times in a row before max attempts" do it "succeeds" do worker.perform_async # attempt 1: false will increment the streak once to 1 # attempt 2: true resets it back to 0 # attempt 3: false will increment the streak once to 1 # attempt 4: false will increment the streak once to 2, loop breaks allow(job_double).to receive(:delete).and_return(false, true, false) worker.perform_async # Scheduled set runs last so only need to stub out its values. allow(Sidekiq::ScheduledSet) .to receive(:new) .and_return([job_double]) expect(model.sidekiq_remove_jobs(job_klasses: [worker.name])) .to eq( { attempts: 4, success: true } ) end end end end end describe "#sidekiq_queue_length" do context "when queue is empty" do it "returns zero" do Sidekiq::Testing.disable! do expect(model.sidekiq_queue_length("test")).to eq 0 end end end context "when queue contains jobs" do it "returns correct size of the queue" do Sidekiq::Testing.disable! do worker.perform_async("Something", [1]) worker.perform_async("Something", [2]) expect(model.sidekiq_queue_length("test")).to eq 2 end end end end describe "#sidekiq_queue_migrate" do it "migrates jobs from one sidekiq queue to another" do Sidekiq::Testing.disable! do worker.perform_async("Something", [1]) worker.perform_async("Something", [2]) expect(model.sidekiq_queue_length("test")).to eq 2 expect(model.sidekiq_queue_length("new_test")).to eq 0 model.sidekiq_queue_migrate("test", to: "new_test") expect(model.sidekiq_queue_length("test")).to eq 0 expect(model.sidekiq_queue_length("new_test")).to eq 2 end end end end end