Skip to content

Commit df9bc0d

Browse files
authored
Replacing validate uniqueness with db constraint on Sidecar model (#4954)
* feat: unique constraint added on db level for sidecar table - db migration added on columns app_guid and name. Unit testting added too - sequel validate uniqueness removed from model and around save added instead * feat: concurrent migration for postgres db * fix: typo fixed * fix: comments are fixed
1 parent 2f042c9 commit df9bc0d

4 files changed

Lines changed: 118 additions & 5 deletions

File tree

app/models/runtime/sidecar_model.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,20 @@ class SidecarModel < Sequel::Model(:sidecars)
1616
key: :sidecar_guid,
1717
primary_key: :guid
1818

19+
def around_save
20+
yield
21+
rescue Sequel::UniqueConstraintViolation => e
22+
raise e unless e.message.include?('sidecars_app_guid_name_index')
23+
24+
errors.add(%i[app_guid name], Sequel.lit("Sidecar with name '#{name}' already exists for given app"))
25+
raise validation_failed_error
26+
end
27+
1928
def validate
2029
super
2130
validates_presence %i[name command]
2231
validates_max_length 255, :name, message: Sequel.lit('Name is too long (maximum is 255 characters)')
2332
validates_max_length 4096, :command, message: Sequel.lit('Command is too long (maximum is 4096 characters)')
24-
validates_unique %i[app_guid name], message: Sequel.lit("Sidecar with name '#{name}' already exists for given app")
2533
end
2634
end
2735
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
Sequel.migration do
2+
no_transaction # required for concurrently option on postgres
3+
4+
up do
5+
transaction do
6+
duplicates = self[:sidecars].
7+
select(:app_guid, :name).
8+
group(:app_guid, :name).
9+
having { count(1) > 1 }
10+
11+
duplicates.each do |dup|
12+
ids_to_remove = self[:sidecars].
13+
where(app_guid: dup[:app_guid], name: dup[:name]).
14+
select(:id).
15+
order(:id).
16+
offset(1).
17+
map(:id)
18+
19+
self[:sidecars].where(id: ids_to_remove).delete
20+
end
21+
end
22+
23+
if database_type == :postgres
24+
VCAP::Migration.with_concurrent_timeout(self) do
25+
add_index :sidecars, %i[app_guid name],
26+
name: :sidecars_app_guid_name_index,
27+
unique: true,
28+
concurrently: true,
29+
if_not_exists: true
30+
end
31+
else
32+
alter_table(:sidecars) do
33+
# rubocop:disable Sequel/ConcurrentIndex -- MySQL does not support concurrent index operations
34+
unless @db.indexes(:sidecars).key?(:sidecars_app_guid_name_index)
35+
add_index %i[app_guid name], unique: true,
36+
name: :sidecars_app_guid_name_index
37+
end
38+
# rubocop:enable Sequel/ConcurrentIndex
39+
end
40+
end
41+
end
42+
43+
down do
44+
if database_type == :postgres
45+
VCAP::Migration.with_concurrent_timeout(self) do
46+
drop_index :sidecars, nil,
47+
name: :sidecars_app_guid_name_index,
48+
concurrently: true,
49+
if_exists: true
50+
end
51+
else
52+
alter_table(:sidecars) do
53+
# rubocop:disable Sequel/ConcurrentIndex -- MySQL does not support concurrent index operations
54+
drop_index %i[app_guid name], name: :sidecars_app_guid_name_index if @db.indexes(:sidecars).key?(:sidecars_app_guid_name_index)
55+
# rubocop:enable Sequel/ConcurrentIndex
56+
end
57+
end
58+
end
59+
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
require 'spec_helper'
2+
require 'migrations/helpers/migration_shared_context'
3+
RSpec.describe 'add unique constraint to sidecars', isolation: :truncation, type: :migration do
4+
include_context 'migration' do
5+
let(:migration_filename) { '20260323092954_add_unique_constraint_to_sidecars.rb' }
6+
end
7+
8+
let!(:app) { VCAP::CloudController::AppModel.make }
9+
10+
it 'remove duplicates, add constraint and revert migration' do
11+
# create duplicate entries
12+
db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app.guid)
13+
db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app.guid)
14+
expect(db[:sidecars].where(name: 'app', app_guid: app.guid).count).to eq(2)
15+
16+
# run the migration
17+
Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true)
18+
19+
# verify duplicates are removed and constraint is enforced
20+
expect(db[:sidecars].where(name: 'app', app_guid: app.guid).count).to eq(1)
21+
expect(db.indexes(:sidecars)).to include(:sidecars_app_guid_name_index)
22+
expect { db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app.guid) }.to raise_error(Sequel::UniqueConstraintViolation)
23+
24+
# running the migration again should not cause any errors
25+
expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error
26+
27+
# roll back the migration
28+
Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true)
29+
30+
# verify constraint is removed and duplicates can be re-inserted
31+
expect(db.indexes(:sidecars)).not_to include(:sidecars_app_guid_name_index)
32+
expect { db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app.guid) }.not_to raise_error
33+
end
34+
end

spec/unit/models/runtime/sidecar_model_spec.rb

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,24 @@ module VCAP::CloudController
1414
end
1515

1616
describe 'validations' do
17+
it { is_expected.to validate_presence :name }
18+
it { is_expected.to validate_presence :command }
19+
end
20+
21+
describe 'sidecar model #around_save' do
1722
let(:app_model) { AppModel.make }
18-
let!(:sidecar) { SidecarModel.make(name: 'my_sidecar', app: app_model) }
23+
let!(:sidecar) { SidecarModel.make(app_guid: app_model.guid, name: 'sidecar1', command: 'exec') }
1924

20-
it 'validates unique sidecar name per app' do
21-
expect { SidecarModel.create(app: app_model, name: 'my_sidecar', command: 'some-command') }.
22-
to raise_error(Sequel::ValidationFailed, /Sidecar with name 'my_sidecar' already exists for given app/)
25+
it 'raises validation error on unique constraint violation for sidecar' do
26+
expect do
27+
SidecarModel.create(guid: SecureRandom.uuid, app_guid: app_model.guid, name: sidecar.name, command: 'exec')
28+
end.to raise_error(Sequel::ValidationFailed) { |error| expect(error.message).to include("Sidecar with name 'sidecar1' already exists for given app") }
29+
end
30+
31+
it 'raises the original error on other unique constraint violations' do
32+
expect do
33+
SidecarModel.create(guid: sidecar.guid, app_guid: app_model.guid, name: 'sidecar2', command: 'exec')
34+
end.to raise_error(Sequel::UniqueConstraintViolation)
2335
end
2436
end
2537

0 commit comments

Comments
 (0)