328 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			328 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Ruby
		
	
	
	
| # frozen_string_literal: true
 | |
| 
 | |
| require 'spec_helper'
 | |
| 
 | |
| RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :model do
 | |
|   it_behaves_like 'having unique enum values'
 | |
| 
 | |
|   describe 'associations' do
 | |
|     it { is_expected.to have_many(:batched_jobs).with_foreign_key(:batched_background_migration_id) }
 | |
| 
 | |
|     describe '#last_job' do
 | |
|       let!(:batched_migration) { create(:batched_background_migration) }
 | |
|       let!(:batched_job1) { create(:batched_background_migration_job, batched_migration: batched_migration) }
 | |
|       let!(:batched_job2) { create(:batched_background_migration_job, batched_migration: batched_migration) }
 | |
| 
 | |
|       it 'returns the most recent (in order of id) batched job' do
 | |
|         expect(batched_migration.last_job).to eq(batched_job2)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.queue_order' do
 | |
|     let!(:migration1) { create(:batched_background_migration) }
 | |
|     let!(:migration2) { create(:batched_background_migration) }
 | |
|     let!(:migration3) { create(:batched_background_migration) }
 | |
| 
 | |
|     it 'returns batched migrations ordered by their id' do
 | |
|       expect(described_class.queue_order.all).to eq([migration1, migration2, migration3])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '.active_migration' do
 | |
|     let!(:migration1) { create(:batched_background_migration, :finished) }
 | |
|     let!(:migration2) { create(:batched_background_migration, :active) }
 | |
|     let!(:migration3) { create(:batched_background_migration, :active) }
 | |
| 
 | |
|     it 'returns the first active migration according to queue order' do
 | |
|       expect(described_class.active_migration).to eq(migration2)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#interval_elapsed?' do
 | |
|     context 'when the migration has no last_job' do
 | |
|       let(:batched_migration) { build(:batched_background_migration) }
 | |
| 
 | |
|       it 'returns true' do
 | |
|         expect(batched_migration.interval_elapsed?).to eq(true)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when the migration has a last_job' do
 | |
|       let(:interval) { 2.minutes }
 | |
|       let(:batched_migration) { create(:batched_background_migration, interval: interval) }
 | |
| 
 | |
|       context 'when the last_job is less than an interval old' do
 | |
|         it 'returns false' do
 | |
|           freeze_time do
 | |
|             create(:batched_background_migration_job,
 | |
|               batched_migration: batched_migration,
 | |
|               created_at: Time.current - 1.minute)
 | |
| 
 | |
|             expect(batched_migration.interval_elapsed?).to eq(false)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when the last_job is exactly an interval old' do
 | |
|         it 'returns true' do
 | |
|           freeze_time do
 | |
|             create(:batched_background_migration_job,
 | |
|               batched_migration: batched_migration,
 | |
|               created_at: Time.current - 2.minutes)
 | |
| 
 | |
|             expect(batched_migration.interval_elapsed?).to eq(true)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when the last_job is more than an interval old' do
 | |
|         it 'returns true' do
 | |
|           freeze_time do
 | |
|             create(:batched_background_migration_job,
 | |
|               batched_migration: batched_migration,
 | |
|               created_at: Time.current - 3.minutes)
 | |
| 
 | |
|             expect(batched_migration.interval_elapsed?).to eq(true)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'when an interval variance is given' do
 | |
|         let(:variance) { 2.seconds }
 | |
| 
 | |
|         context 'when the last job is less than an interval with variance old' do
 | |
|           it 'returns false' do
 | |
|             freeze_time do
 | |
|               create(:batched_background_migration_job,
 | |
|                 batched_migration: batched_migration,
 | |
|                 created_at: Time.current - 1.minute - 57.seconds)
 | |
| 
 | |
|               expect(batched_migration.interval_elapsed?(variance: variance)).to eq(false)
 | |
|             end
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         context 'when the last job is more than an interval with variance old' do
 | |
|           it 'returns true' do
 | |
|             freeze_time do
 | |
|               create(:batched_background_migration_job,
 | |
|                 batched_migration: batched_migration,
 | |
|                 created_at: Time.current - 1.minute - 58.seconds)
 | |
| 
 | |
|               expect(batched_migration.interval_elapsed?(variance: variance)).to eq(true)
 | |
|             end
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#create_batched_job!' do
 | |
|     let(:batched_migration) do
 | |
|       create(:batched_background_migration,
 | |
|              batch_size: 999,
 | |
|              sub_batch_size: 99,
 | |
|              pause_ms: 250
 | |
|             )
 | |
|     end
 | |
| 
 | |
|     it 'creates a batched_job with the correct batch configuration' do
 | |
|       batched_job = batched_migration.create_batched_job!(1, 5)
 | |
| 
 | |
|       expect(batched_job).to have_attributes(
 | |
|         min_value: 1,
 | |
|         max_value: 5,
 | |
|         batch_size: batched_migration.batch_size,
 | |
|         sub_batch_size: batched_migration.sub_batch_size,
 | |
|         pause_ms: 250
 | |
|       )
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#next_min_value' do
 | |
|     let!(:batched_migration) { create(:batched_background_migration) }
 | |
| 
 | |
|     context 'when a previous job exists' do
 | |
|       let!(:batched_job) { create(:batched_background_migration_job, batched_migration: batched_migration) }
 | |
| 
 | |
|       it 'returns the next value after the previous maximum' do
 | |
|         expect(batched_migration.next_min_value).to eq(batched_job.max_value + 1)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when a previous job does not exist' do
 | |
|       it 'returns the migration minimum value' do
 | |
|         expect(batched_migration.next_min_value).to eq(batched_migration.min_value)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#job_class' do
 | |
|     let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob }
 | |
|     let(:batched_migration) { build(:batched_background_migration) }
 | |
| 
 | |
|     it 'returns the class of the job for the migration' do
 | |
|       expect(batched_migration.job_class).to eq(job_class)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#batch_class' do
 | |
|     let(:batch_class) { Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy}
 | |
|     let(:batched_migration) { build(:batched_background_migration) }
 | |
| 
 | |
|     it 'returns the class of the batch strategy for the migration' do
 | |
|       expect(batched_migration.batch_class).to eq(batch_class)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   shared_examples_for 'an attr_writer that demodulizes assigned class names' do |attribute_name|
 | |
|     let(:batched_migration) { build(:batched_background_migration) }
 | |
| 
 | |
|     context 'when a module name exists' do
 | |
|       it 'removes the module name' do
 | |
|         batched_migration.public_send(:"#{attribute_name}=", '::Foo::Bar')
 | |
| 
 | |
|         expect(batched_migration[attribute_name]).to eq('Bar')
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when a module name does not exist' do
 | |
|       it 'does not change the given class name' do
 | |
|         batched_migration.public_send(:"#{attribute_name}=", 'Bar')
 | |
| 
 | |
|         expect(batched_migration[attribute_name]).to eq('Bar')
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#job_class_name=' do
 | |
|     it_behaves_like 'an attr_writer that demodulizes assigned class names', :job_class_name
 | |
|   end
 | |
| 
 | |
|   describe '#batch_class_name=' do
 | |
|     it_behaves_like 'an attr_writer that demodulizes assigned class names', :batch_class_name
 | |
|   end
 | |
| 
 | |
|   describe '#migrated_tuple_count' do
 | |
|     subject { batched_migration.migrated_tuple_count }
 | |
| 
 | |
|     let(:batched_migration) { create(:batched_background_migration) }
 | |
| 
 | |
|     before do
 | |
|       create_list(:batched_background_migration_job, 5, status: :succeeded, batch_size: 1_000, batched_migration: batched_migration)
 | |
|       create_list(:batched_background_migration_job, 1, status: :running, batch_size: 1_000, batched_migration: batched_migration)
 | |
|       create_list(:batched_background_migration_job, 1, status: :failed, batch_size: 1_000, batched_migration: batched_migration)
 | |
|     end
 | |
| 
 | |
|     it 'sums the batch_size of succeeded jobs' do
 | |
|       expect(subject).to eq(5_000)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#prometheus_labels' do
 | |
|     let(:batched_migration) { create(:batched_background_migration, job_class_name: 'TestMigration', table_name: 'foo', column_name: 'bar') }
 | |
| 
 | |
|     it 'returns a hash with labels for the migration' do
 | |
|       labels = {
 | |
|         migration_id: batched_migration.id,
 | |
|         migration_identifier: 'TestMigration/foo.bar'
 | |
|       }
 | |
| 
 | |
|       expect(batched_migration.prometheus_labels).to eq(labels)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#smoothed_time_efficiency' do
 | |
|     let(:migration) { create(:batched_background_migration, interval: 120.seconds) }
 | |
|     let(:end_time) { Time.zone.now }
 | |
| 
 | |
|     around do |example|
 | |
|       freeze_time do
 | |
|         example.run
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     let(:common_attrs) do
 | |
|       {
 | |
|         status: :succeeded,
 | |
|         batched_migration: migration,
 | |
|         finished_at: end_time
 | |
|       }
 | |
|     end
 | |
| 
 | |
|     context 'when there are not enough jobs' do
 | |
|       subject { migration.smoothed_time_efficiency(number_of_jobs: 10) }
 | |
| 
 | |
|       it 'returns nil' do
 | |
|         create_list(:batched_background_migration_job, 9, **common_attrs)
 | |
| 
 | |
|         expect(subject).to be_nil
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     context 'when there are enough jobs' do
 | |
|       subject { migration.smoothed_time_efficiency(number_of_jobs: number_of_jobs) }
 | |
| 
 | |
|       let!(:jobs) { create_list(:batched_background_migration_job, number_of_jobs, **common_attrs.merge(batched_migration: migration)) }
 | |
|       let(:number_of_jobs) { 10 }
 | |
| 
 | |
|       before do
 | |
|         expect(migration).to receive_message_chain(:batched_jobs, :successful_in_execution_order, :reverse_order, :limit).with(no_args).with(no_args).with(number_of_jobs).and_return(jobs)
 | |
|       end
 | |
| 
 | |
|       def mock_efficiencies(*effs)
 | |
|         effs.each_with_index do |eff, i|
 | |
|           expect(jobs[i]).to receive(:time_efficiency).and_return(eff)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'example 1: increasing trend, but only recently crossed threshold' do
 | |
|         it 'returns the smoothed time efficiency' do
 | |
|           mock_efficiencies(1.1, 1, 0.95, 0.9, 0.8, 0.95, 0.9, 0.8, 0.9, 0.95)
 | |
| 
 | |
|           expect(subject).to be_within(0.05).of(0.95)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'example 2: increasing trend, crossed threshold a while ago' do
 | |
|         it 'returns the smoothed time efficiency' do
 | |
|           mock_efficiencies(1.2, 1.1, 1, 1, 1.1, 1, 0.95, 0.9, 0.95, 0.9)
 | |
| 
 | |
|           expect(subject).to be_within(0.05).of(1.1)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'example 3: decreasing trend, but only recently crossed threshold' do
 | |
|         it 'returns the smoothed time efficiency' do
 | |
|           mock_efficiencies(0.9, 0.95, 1, 1.2, 1.1, 1.2, 1.1, 1.0, 1.1, 1.0)
 | |
| 
 | |
|           expect(subject).to be_within(0.05).of(1.0)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       context 'example 4: latest run spiked' do
 | |
|         it 'returns the smoothed time efficiency' do
 | |
|           mock_efficiencies(1.2, 0.9, 0.8, 0.9, 0.95, 0.9, 0.92, 0.9, 0.95, 0.9)
 | |
| 
 | |
|           expect(subject).to be_within(0.02).of(0.96)
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe '#optimize!' do
 | |
|     subject { batched_migration.optimize! }
 | |
| 
 | |
|     let(:batched_migration) { create(:batched_background_migration) }
 | |
|     let(:optimizer) { instance_double('Gitlab::Database::BackgroundMigration::BatchOptimizer') }
 | |
| 
 | |
|     it 'calls the BatchOptimizer' do
 | |
|       expect(Gitlab::Database::BackgroundMigration::BatchOptimizer).to receive(:new).with(batched_migration).and_return(optimizer)
 | |
|       expect(optimizer).to receive(:optimize!)
 | |
| 
 | |
|       subject
 | |
|     end
 | |
|   end
 | |
| end
 |