190 lines
7 KiB
Ruby
190 lines
7 KiB
Ruby
|
# frozen_string_literal: true
|
||
|
|
||
|
require 'spec_helper'
|
||
|
|
||
|
RSpec.describe Gitlab::Database::ConsistencyChecker do
|
||
|
let(:batch_size) { 10 }
|
||
|
let(:max_batches) { 4 }
|
||
|
let(:max_runtime) { described_class::MAX_RUNTIME }
|
||
|
let(:metrics_counter) { Gitlab::Metrics.registry.get(:consistency_checks) }
|
||
|
|
||
|
subject(:consistency_checker) do
|
||
|
described_class.new(
|
||
|
source_model: Namespace,
|
||
|
target_model: Ci::NamespaceMirror,
|
||
|
source_columns: %w[id traversal_ids],
|
||
|
target_columns: %w[namespace_id traversal_ids]
|
||
|
)
|
||
|
end
|
||
|
|
||
|
before do
|
||
|
stub_const("#{described_class.name}::BATCH_SIZE", batch_size)
|
||
|
stub_const("#{described_class.name}::MAX_BATCHES", max_batches)
|
||
|
redis_shared_state_cleanup! # For Prometheus Counters
|
||
|
end
|
||
|
|
||
|
after do
|
||
|
Gitlab::Metrics.reset_registry!
|
||
|
end
|
||
|
|
||
|
describe '#over_time_limit?' do
|
||
|
before do
|
||
|
allow(consistency_checker).to receive(:start_time).and_return(0)
|
||
|
end
|
||
|
|
||
|
it 'returns true only if the running time has exceeded MAX_RUNTIME' do
|
||
|
allow(consistency_checker).to receive(:monotonic_time).and_return(0, max_runtime - 1, max_runtime + 1)
|
||
|
expect(consistency_checker.monotonic_time).to eq(0)
|
||
|
expect(consistency_checker.send(:over_time_limit?)).to eq(false)
|
||
|
expect(consistency_checker.send(:over_time_limit?)).to eq(true)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
describe '#execute' do
|
||
|
context 'when empty tables' do
|
||
|
it 'returns an empty response' do
|
||
|
expected_result = { matches: 0, mismatches: 0, batches: 0, mismatches_details: [], next_start_id: nil }
|
||
|
expect(consistency_checker.execute(start_id: 1)).to eq(expected_result)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
context 'when the tables contain matching items' do
|
||
|
before do
|
||
|
create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
|
||
|
end
|
||
|
|
||
|
it 'does not process more than MAX_BATCHES' do
|
||
|
max_batches = 3
|
||
|
stub_const("#{described_class.name}::MAX_BATCHES", max_batches)
|
||
|
result = consistency_checker.execute(start_id: Namespace.minimum(:id))
|
||
|
expect(result[:batches]).to eq(max_batches)
|
||
|
expect(result[:matches]).to eq(max_batches * batch_size)
|
||
|
end
|
||
|
|
||
|
it 'doesn not exceed the MAX_RUNTIME' do
|
||
|
allow(consistency_checker).to receive(:monotonic_time).and_return(0, max_runtime - 1, max_runtime + 1)
|
||
|
result = consistency_checker.execute(start_id: Namespace.minimum(:id))
|
||
|
expect(result[:batches]).to eq(1)
|
||
|
expect(result[:matches]).to eq(1 * batch_size)
|
||
|
end
|
||
|
|
||
|
it 'returns the correct number of matches and batches checked' do
|
||
|
expected_result = {
|
||
|
next_start_id: Namespace.minimum(:id) + described_class::MAX_BATCHES * described_class::BATCH_SIZE,
|
||
|
batches: max_batches,
|
||
|
matches: max_batches * batch_size,
|
||
|
mismatches: 0,
|
||
|
mismatches_details: []
|
||
|
}
|
||
|
expect(consistency_checker.execute(start_id: Namespace.minimum(:id))).to eq(expected_result)
|
||
|
end
|
||
|
|
||
|
it 'returns the min_id as the next_start_id if the check reaches the last element' do
|
||
|
expect(Gitlab::Metrics).to receive(:counter).at_most(:once)
|
||
|
.with(:consistency_checks, "Consistency Check Results")
|
||
|
.and_call_original
|
||
|
|
||
|
# Starting from the 5th last element
|
||
|
start_id = Namespace.all.order(id: :desc).limit(5).pluck(:id).last
|
||
|
expected_result = {
|
||
|
next_start_id: Namespace.first.id,
|
||
|
batches: 1,
|
||
|
matches: 5,
|
||
|
mismatches: 0,
|
||
|
mismatches_details: []
|
||
|
}
|
||
|
expect(consistency_checker.execute(start_id: start_id)).to eq(expected_result)
|
||
|
|
||
|
expect(metrics_counter.get(source_table: "namespaces", result: "mismatch")).to eq(0)
|
||
|
expect(metrics_counter.get(source_table: "namespaces", result: "match")).to eq(5)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
context 'when some items are missing from the first table' do
|
||
|
let(:missing_namespace) { Namespace.all.order(:id).limit(2).last }
|
||
|
|
||
|
before do
|
||
|
create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
|
||
|
missing_namespace.delete
|
||
|
end
|
||
|
|
||
|
it 'reports the missing elements' do
|
||
|
expected_result = {
|
||
|
next_start_id: Namespace.first.id + described_class::MAX_BATCHES * described_class::BATCH_SIZE,
|
||
|
batches: max_batches,
|
||
|
matches: 39,
|
||
|
mismatches: 1,
|
||
|
mismatches_details: [{
|
||
|
id: missing_namespace.id,
|
||
|
source_table: nil,
|
||
|
target_table: [missing_namespace.traversal_ids]
|
||
|
}]
|
||
|
}
|
||
|
expect(consistency_checker.execute(start_id: Namespace.first.id)).to eq(expected_result)
|
||
|
|
||
|
expect(metrics_counter.get(source_table: "namespaces", result: "mismatch")).to eq(1)
|
||
|
expect(metrics_counter.get(source_table: "namespaces", result: "match")).to eq(39)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
context 'when some items are missing from the second table' do
|
||
|
let(:missing_ci_namespace_mirror) { Ci::NamespaceMirror.all.order(:id).limit(2).last }
|
||
|
|
||
|
before do
|
||
|
create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
|
||
|
missing_ci_namespace_mirror.delete
|
||
|
end
|
||
|
|
||
|
it 'reports the missing elements' do
|
||
|
expected_result = {
|
||
|
next_start_id: Namespace.first.id + described_class::MAX_BATCHES * described_class::BATCH_SIZE,
|
||
|
batches: 4,
|
||
|
matches: 39,
|
||
|
mismatches: 1,
|
||
|
mismatches_details: [{
|
||
|
id: missing_ci_namespace_mirror.namespace_id,
|
||
|
source_table: [missing_ci_namespace_mirror.traversal_ids],
|
||
|
target_table: nil
|
||
|
}]
|
||
|
}
|
||
|
expect(consistency_checker.execute(start_id: Namespace.first.id)).to eq(expected_result)
|
||
|
|
||
|
expect(metrics_counter.get(source_table: "namespaces", result: "mismatch")).to eq(1)
|
||
|
expect(metrics_counter.get(source_table: "namespaces", result: "match")).to eq(39)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
context 'when elements are different between the two tables' do
|
||
|
let(:different_namespaces) { Namespace.order(:id).limit(max_batches * batch_size).sample(3).sort_by(&:id) }
|
||
|
|
||
|
before do
|
||
|
create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
|
||
|
|
||
|
different_namespaces.each do |namespace|
|
||
|
namespace.update_attribute(:traversal_ids, [])
|
||
|
end
|
||
|
end
|
||
|
|
||
|
it 'reports the difference between the two tables' do
|
||
|
expected_result = {
|
||
|
next_start_id: Namespace.first.id + described_class::MAX_BATCHES * described_class::BATCH_SIZE,
|
||
|
batches: 4,
|
||
|
matches: 37,
|
||
|
mismatches: 3,
|
||
|
mismatches_details: different_namespaces.map do |namespace|
|
||
|
{
|
||
|
id: namespace.id,
|
||
|
source_table: [[]],
|
||
|
target_table: [[namespace.id]] # old traversal_ids of the namespace
|
||
|
}
|
||
|
end
|
||
|
}
|
||
|
expect(consistency_checker.execute(start_id: Namespace.first.id)).to eq(expected_result)
|
||
|
|
||
|
expect(metrics_counter.get(source_table: "namespaces", result: "mismatch")).to eq(3)
|
||
|
expect(metrics_counter.get(source_table: "namespaces", result: "match")).to eq(37)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|