Skip to content

Commit c716408

Browse files
authored
Replacing validate uniqueness with db constraint on RevisionProcessCommand model (#4956)
* feat: unique constraint added on revicion_process_commands table - unique constraint on revision_guid and process type columns are added on the table. - validate_uniqueness check in model side is removed. - respective test cases are added * fix: validates_unique in service_key's validate method is removed. The tests for these already refactored in PR 4930 * fix: add validates_unique added back because of the http call in service_key_create action. If key is dublicated prevent http call * fix: testing name and service_instance_id uniqueness in dedicated test * feat: concurrent migration for postgres db * fix: comments are fixed
1 parent e055785 commit c716408

5 files changed

Lines changed: 135 additions & 3 deletions

File tree

app/models/runtime/revision_process_command_model.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ class RevisionProcessCommandModel < Sequel::Model(:revision_process_commands)
66
key: :revision_guid,
77
without_guid_generation: true
88

9-
def validate
10-
super
11-
validates_unique(%i[revision_guid process_type])
9+
def around_save
10+
yield
11+
rescue Sequel::UniqueConstraintViolation => e
12+
raise e unless e.message.include?('revision_process_commands_revision_guid_process_type_index')
13+
14+
errors.add(%i[revision_guid process_type], Sequel.lit("Process type '#{process_type}' already exists for given revision"))
15+
raise validation_failed_error
1216
end
1317
end
1418
end

app/models/services/service_key.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def around_save
4949
def validate
5050
validates_presence :name
5151
validates_presence :service_instance
52+
# Keep validates_unique as a pre-guard to prevent broker HTTP call when key already exists
5253
validates_unique %i[name service_instance_id]
5354

5455
return unless service_instance
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
Sequel.migration do # rubocop:disable Metrics/BlockLength
2+
no_transaction # required for concurrently option on postgres
3+
4+
up do
5+
transaction do
6+
duplicates = self[:revision_process_commands].
7+
select(:revision_guid, :process_type).
8+
group(:revision_guid, :process_type).
9+
having { count(1) > 1 }
10+
11+
duplicates.each do |dup|
12+
ids_to_remove = self[:revision_process_commands].
13+
where(revision_guid: dup[:revision_guid], process_type: dup[:process_type]).
14+
select(:id).
15+
order(:id).
16+
offset(1).
17+
map(:id)
18+
self[:revision_process_commands].where(id: ids_to_remove).delete
19+
end
20+
end
21+
22+
if database_type == :postgres
23+
VCAP::Migration.with_concurrent_timeout(self) do
24+
add_index :revision_process_commands, %i[revision_guid process_type],
25+
name: :revision_process_commands_revision_guid_process_type_index,
26+
unique: true,
27+
concurrently: true,
28+
if_not_exists: true
29+
end
30+
else
31+
alter_table(:revision_process_commands) do
32+
# rubocop:disable Sequel/ConcurrentIndex -- MySQL does not support concurrent index operations
33+
unless @db.indexes(:revision_process_commands).key?(:revision_process_commands_revision_guid_process_type_index)
34+
add_index %i[revision_guid process_type], unique: true,
35+
name: :revision_process_commands_revision_guid_process_type_index
36+
end
37+
# rubocop:enable Sequel/ConcurrentIndex
38+
end
39+
end
40+
end
41+
42+
down do
43+
if database_type == :postgres
44+
VCAP::Migration.with_concurrent_timeout(self) do
45+
drop_index :revision_process_commands, nil,
46+
name: :revision_process_commands_revision_guid_process_type_index,
47+
concurrently: true,
48+
if_exists: true
49+
end
50+
else
51+
alter_table(:revision_process_commands) do
52+
# rubocop:disable Sequel/ConcurrentIndex -- MySQL does not support concurrent index operations
53+
if @db.indexes(:revision_process_commands).key?(:revision_process_commands_revision_guid_process_type_index)
54+
drop_index %i[revision_guid process_type],
55+
name: :revision_process_commands_revision_guid_process_type_index
56+
end
57+
# rubocop:enable Sequel/ConcurrentIndex
58+
end
59+
end
60+
end
61+
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
require 'spec_helper'
2+
require 'migrations/helpers/migration_shared_context'
3+
4+
RSpec.describe 'add unique constraint to revision_process_commands', isolation: :truncation, type: :migration do
5+
include_context 'migration' do
6+
let(:migration_filename) { '20260323144429_add_unique_constraint_to_revision_process_commands.rb' }
7+
end
8+
9+
let!(:revision) { VCAP::CloudController::RevisionModel.make }
10+
11+
it 'removes duplicates, adds constraint and reverts migration' do
12+
# create duplicate entries
13+
db[:revision_process_commands].insert(guid: SecureRandom.uuid, revision_guid: revision.guid, process_type: 'worker')
14+
db[:revision_process_commands].insert(guid: SecureRandom.uuid, revision_guid: revision.guid, process_type: 'worker')
15+
expect(db[:revision_process_commands].where(revision_guid: revision.guid, process_type: 'worker').count).to eq(2)
16+
17+
# run the migration
18+
Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true)
19+
20+
# verify duplicates are removed and constraint is enforced
21+
expect(db[:revision_process_commands].where(revision_guid: revision.guid, process_type: 'worker').count).to eq(1)
22+
expect(db.indexes(:revision_process_commands)).to include(:revision_process_commands_revision_guid_process_type_index)
23+
expect do
24+
db[:revision_process_commands].insert(guid: SecureRandom.uuid, revision_guid: revision.guid, process_type: 'worker')
25+
end.to raise_error(Sequel::UniqueConstraintViolation)
26+
27+
# running the migration again should not cause any errors
28+
expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error
29+
30+
# roll back the migration
31+
Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true)
32+
33+
# verify constraint is removed and duplicates can be re-inserted
34+
expect(db.indexes(:revision_process_commands)).not_to include(:revision_process_commands_revision_guid_process_type_index)
35+
expect do
36+
db[:revision_process_commands].insert(guid: SecureRandom.uuid, revision_guid: revision.guid, process_type: 'worker')
37+
end.not_to raise_error
38+
end
39+
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
require 'spec_helper'
2+
3+
module VCAP::CloudController
4+
RSpec.describe RevisionProcessCommandModel do
5+
describe 'revision_process_command_model #around_save' do
6+
let(:revision) { RevisionModel.make }
7+
let!(:revision_process_command) { RevisionProcessCommandModel.make(revision_guid: revision.guid, process_type: 'worker') }
8+
9+
it 'raises validation error on unique constraint violation' do
10+
expect do
11+
RevisionProcessCommandModel.make(
12+
revision_guid: revision_process_command.revision_guid,
13+
process_type: revision_process_command.process_type
14+
)
15+
end.to raise_error(Sequel::ValidationFailed) { |error|
16+
expect(error.message).to include('already exists for given revision')
17+
}
18+
end
19+
20+
it 'raises the original error on other unique constraint violations' do
21+
expect do
22+
RevisionProcessCommandModel.make(guid: revision_process_command.guid, revision_guid: revision_process_command.revision_guid, process_type: 'worker')
23+
end.to raise_error(Sequel::UniqueConstraintViolation)
24+
end
25+
end
26+
end
27+
end

0 commit comments

Comments
 (0)