gitlab-org--gitlab-foss/spec/models/internal_id_spec.rb
Kamil Trzciński fb07863693 Rewind IID on Ci::Pipelines
If no pipeline is created we currently have IID gap.
This is due to fact that we generate IID not on save,
but rather ahead of time. This results, us,
losing IIDs.
2019-04-16 11:22:23 +02:00

251 lines
6.9 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
describe InternalId do
let(:project) { create(:project) }
let(:usage) { :issues }
let(:issue) { build(:issue, project: project) }
let(:scope) { { project: project } }
let(:init) { ->(s) { s.project.issues.size } }
it_behaves_like 'having unique enum values'
context 'validations' do
it { is_expected.to validate_presence_of(:usage) }
end
describe '.flush_records!' do
subject { described_class.flush_records!(project: project) }
let(:another_project) { create(:project) }
before do
create_list(:issue, 2, project: project)
create_list(:issue, 2, project: another_project)
end
it 'deletes all records for the given project' do
expect { subject }.to change { described_class.where(project: project).count }.from(1).to(0)
end
it 'retains records for other projects' do
expect { subject }.not_to change { described_class.where(project: another_project).count }
end
it 'does not allow an empty filter' do
expect { described_class.flush_records!({}) }.to raise_error(/filter cannot be empty/)
end
end
describe '.generate_next' do
subject { described_class.generate_next(issue, scope, usage, init) }
context 'in the absence of a record' do
it 'creates a record if not yet present' do
expect { subject }.to change { described_class.count }.from(0).to(1)
end
it 'stores record attributes' do
subject
described_class.first.tap do |record|
expect(record.project).to eq(project)
expect(record.usage).to eq(usage.to_s)
end
end
context 'with existing issues' do
before do
create_list(:issue, 2, project: project)
described_class.delete_all
end
it 'calculates last_value values automatically' do
expect(subject).to eq(project.issues.size + 1)
end
end
context 'with concurrent inserts on table' do
it 'looks up the record if it was created concurrently' do
args = { **scope, usage: described_class.usages[usage.to_s] }
record = double
expect(described_class).to receive(:find_by).with(args).and_return(nil) # first call, record not present
expect(described_class).to receive(:find_by).with(args).and_return(record) # second call, record was created by another process
expect(described_class).to receive(:create!).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
expect(record).to receive(:increment_and_save!)
subject
end
end
end
it 'generates a strictly monotone, gapless sequence' do
seq = Array.new(10).map do
described_class.generate_next(issue, scope, usage, init)
end
normalized = seq.map { |i| i - seq.min }
expect(normalized).to eq((0..seq.size - 1).to_a)
end
context 'with an insufficient schema version' do
before do
described_class.reset_column_information
# Project factory will also call the current_version
expect(ActiveRecord::Migrator).to receive(:current_version).twice.and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
end
let(:init) { double('block') }
it 'calculates next internal ids on the fly' do
val = rand(1..100)
expect(init).to receive(:call).with(issue).and_return(val)
expect(subject).to eq(val + 1)
end
end
end
describe '.reset' do
subject { described_class.reset(issue, scope, usage, value) }
context 'in the absence of a record' do
let(:value) { 2 }
it 'does not revert back the value' do
expect { subject }.not_to change { described_class.count }
expect(subject).to be_falsey
end
end
context 'when valid iid is used to reset' do
let!(:value) { generate_next }
context 'and iid is a latest one' do
it 'does rewind and next generated value is the same' do
expect(subject).to be_truthy
expect(generate_next).to eq(value)
end
end
context 'and iid is not a latest one' do
it 'does not rewind' do
generate_next
expect(subject).to be_falsey
expect(generate_next).to be > value
end
end
def generate_next
described_class.generate_next(issue, scope, usage, init)
end
end
context 'with an insufficient schema version' do
let(:value) { 2 }
before do
described_class.reset_column_information
# Project factory will also call the current_version
expect(ActiveRecord::Migrator).to receive(:current_version).twice.and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
end
it 'does not reset any of the iids' do
expect(subject).to be_falsey
end
end
end
describe '.track_greatest' do
let(:value) { 9001 }
subject { described_class.track_greatest(issue, scope, usage, value, init) }
context 'in the absence of a record' do
it 'creates a record if not yet present' do
expect { subject }.to change { described_class.count }.from(0).to(1)
end
end
it 'stores record attributes' do
subject
described_class.first.tap do |record|
expect(record.project).to eq(project)
expect(record.usage).to eq(usage.to_s)
expect(record.last_value).to eq(value)
end
end
context 'with existing issues' do
before do
create(:issue, project: project)
described_class.delete_all
end
it 'still returns the last value to that of the given value' do
expect(subject).to eq(value)
end
end
context 'when value is less than the current last_value' do
it 'returns the current last_value' do
described_class.create!(**scope, usage: usage, last_value: 10_001)
expect(subject).to eq 10_001
end
end
end
describe '#increment_and_save!' do
let(:id) { create(:internal_id) }
subject { id.increment_and_save! }
it 'returns incremented iid' do
value = id.last_value
expect(subject).to eq(value + 1)
end
it 'saves the record' do
subject
expect(id.changed?).to be_falsey
end
context 'with last_value=nil' do
let(:id) { build(:internal_id, last_value: nil) }
it 'returns 1' do
expect(subject).to eq(1)
end
end
end
describe '#track_greatest_and_save!' do
let(:id) { create(:internal_id) }
let(:new_last_value) { 9001 }
subject { id.track_greatest_and_save!(new_last_value) }
it 'returns new last value' do
expect(subject).to eq new_last_value
end
it 'saves the record' do
subject
expect(id.changed?).to be_falsey
end
context 'when new last value is lower than the max' do
it 'does not update the last value' do
id.update!(last_value: 10_001)
subject
expect(id.reload.last_value).to eq 10_001
end
end
end
end