Commit 516fda9f authored by Andreas Brandl's avatar Andreas Brandl

Fix reindexing bug

When a `statement_timeout` is set, this leads to trouble with the
inclusion of the migration helper.

There is basically a name clash between the reindexing class with
`#execute` and the migration helper for the statement timeout expecting
to be able to execute SQL queries through `#execute`.

This has been fixed with renaming the method on the reindexing class.

Relates to https://gitlab.com/gitlab-org/gitlab/-/issues/251084
parent 8e462453
...@@ -19,7 +19,7 @@ module Gitlab ...@@ -19,7 +19,7 @@ module Gitlab
@logger = logger @logger = logger
end end
def execute def perform
raise ReindexError, "index #{index_name} does not exist" unless index_exists? raise ReindexError, "index #{index_name} does not exist" unless index_exists?
raise ReindexError, 'UNIQUE indexes are currently not supported' if index_unique? raise ReindexError, 'UNIQUE indexes are currently not supported' if index_unique?
...@@ -51,6 +51,7 @@ module Gitlab ...@@ -51,6 +51,7 @@ module Gitlab
private private
delegate :execute, to: :connection
def connection def connection
@connection ||= ActiveRecord::Base.connection @connection ||= ActiveRecord::Base.connection
end end
......
...@@ -176,7 +176,7 @@ namespace :gitlab do ...@@ -176,7 +176,7 @@ namespace :gitlab do
raise ArgumentError, 'must give the index name to reindex' unless args[:index_name] raise ArgumentError, 'must give the index name to reindex' unless args[:index_name]
Gitlab::Database::ConcurrentReindex.new(args[:index_name], logger: Logger.new(STDOUT)).execute Gitlab::Database::ConcurrentReindex.new(args[:index_name], logger: Logger.new(STDOUT)).perform
end end
end end
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do RSpec.describe Gitlab::Database::ConcurrentReindex, '#perform' do
subject { described_class.new(index_name, logger: logger) } subject { described_class.new(index_name, logger: logger) }
let(:table_name) { '_test_reindex_table' } let(:table_name) { '_test_reindex_table' }
...@@ -29,7 +29,7 @@ RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do ...@@ -29,7 +29,7 @@ RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do
end end
it 'raises an error' do it 'raises an error' do
expect { subject.execute }.to raise_error(described_class::ReindexError, /does not exist/) expect { subject.perform }.to raise_error(described_class::ReindexError, /does not exist/)
end end
end end
...@@ -43,7 +43,7 @@ RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do ...@@ -43,7 +43,7 @@ RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do
it 'raises an error' do it 'raises an error' do
expect do expect do
subject.execute subject.perform
end.to raise_error(described_class::ReindexError, /UNIQUE indexes are currently not supported/) end.to raise_error(described_class::ReindexError, /UNIQUE indexes are currently not supported/)
end end
end end
...@@ -57,35 +57,20 @@ RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do ...@@ -57,35 +57,20 @@ RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do
let!(:original_index) { find_index_create_statement } let!(:original_index) { find_index_create_statement }
before do it 'integration test: executing full index replacement without mocks' do
allow(subject).to receive(:connection).and_return(connection) allow(connection).to receive(:execute).and_wrap_original do |method, sql|
allow(subject).to receive(:disable_statement_timeout).and_yield method.call(sql.sub(/CONCURRENTLY/, ''))
end
it 'replaces the existing index with an identical index' do
expect(subject).to receive(:disable_statement_timeout).exactly(3).times.and_yield
expect_to_execute_concurrently_in_order(drop_index)
expect_to_execute_concurrently_in_order(create_index)
expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
end end
expect_to_execute_in_order("ALTER INDEX #{index_name} RENAME TO #{replaced_name}") subject.perform
expect_to_execute_in_order("ALTER INDEX #{replacement_name} RENAME TO #{index_name}")
expect_to_execute_in_order("ALTER INDEX #{replaced_name} RENAME TO #{replacement_name}")
expect_to_execute_concurrently_in_order(drop_index)
subject.execute
check_index_exists check_index_exists
end end
context 'when a dangling index is left from a previous run' do context 'mocked specs' do
before do before do
connection.execute("CREATE INDEX #{replacement_name} ON #{table_name} (#{column_name})") allow(subject).to receive(:connection).and_return(connection)
allow(subject).to receive(:disable_statement_timeout).and_yield
end end
it 'replaces the existing index with an identical index' do it 'replaces the existing index with an identical index' do
...@@ -104,78 +89,105 @@ RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do ...@@ -104,78 +89,105 @@ RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do
expect_to_execute_concurrently_in_order(drop_index) expect_to_execute_concurrently_in_order(drop_index)
subject.execute subject.perform
check_index_exists check_index_exists
end end
end
context 'when it fails to create the replacement index' do context 'when a dangling index is left from a previous run' do
it 'safely cleans up and signals the error' do before do
expect_to_execute_concurrently_in_order(drop_index) connection.execute("CREATE INDEX #{replacement_name} ON #{table_name} (#{column_name})")
end
expect(connection).to receive(:execute).with(create_index).ordered it 'replaces the existing index with an identical index' do
.and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout') expect(subject).to receive(:disable_statement_timeout).exactly(3).times.and_yield
expect_to_execute_concurrently_in_order(drop_index) expect_to_execute_concurrently_in_order(drop_index)
expect_to_execute_concurrently_in_order(create_index)
expect { subject.execute }.to raise_error(described_class::ReindexError, /connect timeout/) expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
end
check_index_exists expect_to_execute_in_order("ALTER INDEX #{index_name} RENAME TO #{replaced_name}")
expect_to_execute_in_order("ALTER INDEX #{replacement_name} RENAME TO #{index_name}")
expect_to_execute_in_order("ALTER INDEX #{replaced_name} RENAME TO #{replacement_name}")
expect_to_execute_concurrently_in_order(drop_index)
subject.perform
check_index_exists
end
end end
end
context 'when the replacement index is not valid' do context 'when it fails to create the replacement index' do
it 'safely cleans up and signals the error' do it 'safely cleans up and signals the error' do
expect_to_execute_concurrently_in_order(drop_index) expect_to_execute_concurrently_in_order(drop_index)
expect_to_execute_concurrently_in_order(create_index)
expect(subject).to receive(:replacement_index_valid?).and_return(false) expect(connection).to receive(:execute).with(create_index).ordered
.and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
expect_to_execute_concurrently_in_order(drop_index) expect_to_execute_concurrently_in_order(drop_index)
expect { subject.execute }.to raise_error(described_class::ReindexError, /replacement index was created as INVALID/) expect { subject.perform }.to raise_error(described_class::ReindexError, /connect timeout/)
check_index_exists check_index_exists
end
end end
end
context 'when a database error occurs while swapping the indexes' do context 'when the replacement index is not valid' do
it 'safely cleans up and signals the error' do it 'safely cleans up and signals the error' do
expect_to_execute_concurrently_in_order(drop_index) expect_to_execute_concurrently_in_order(drop_index)
expect_to_execute_concurrently_in_order(create_index) expect_to_execute_concurrently_in_order(create_index)
expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance| expect(subject).to receive(:replacement_index_valid?).and_return(false)
expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
expect_to_execute_concurrently_in_order(drop_index)
expect { subject.perform }.to raise_error(described_class::ReindexError, /replacement index was created as INVALID/)
check_index_exists
end end
end
expect(connection).to receive(:execute).ordered context 'when a database error occurs while swapping the indexes' do
.with("ALTER INDEX #{index_name} RENAME TO #{replaced_name}") it 'safely cleans up and signals the error' do
.and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout') expect_to_execute_concurrently_in_order(drop_index)
expect_to_execute_concurrently_in_order(create_index)
expect_to_execute_concurrently_in_order(drop_index) expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
end
expect { subject.execute }.to raise_error(described_class::ReindexError, /connect timeout/) expect(connection).to receive(:execute).ordered
.with("ALTER INDEX #{index_name} RENAME TO #{replaced_name}")
.and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
check_index_exists expect_to_execute_concurrently_in_order(drop_index)
end
end
context 'when with_lock_retries fails to acquire the lock' do expect { subject.perform }.to raise_error(described_class::ReindexError, /connect timeout/)
it 'safely cleans up and signals the error' do
expect_to_execute_concurrently_in_order(drop_index)
expect_to_execute_concurrently_in_order(create_index)
expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance| check_index_exists
expect(instance).to receive(:run).with(raise_on_exhaustion: true)
.and_raise(::Gitlab::Database::WithLockRetries::AttemptsExhaustedError, 'exhausted')
end end
end
expect_to_execute_concurrently_in_order(drop_index) context 'when with_lock_retries fails to acquire the lock' do
it 'safely cleans up and signals the error' do
expect_to_execute_concurrently_in_order(drop_index)
expect_to_execute_concurrently_in_order(create_index)
expect { subject.execute }.to raise_error(described_class::ReindexError, /exhausted/) expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
expect(instance).to receive(:run).with(raise_on_exhaustion: true)
.and_raise(::Gitlab::Database::WithLockRetries::AttemptsExhaustedError, 'exhausted')
end
check_index_exists expect_to_execute_concurrently_in_order(drop_index)
expect { subject.perform }.to raise_error(described_class::ReindexError, /exhausted/)
check_index_exists
end
end end
end end
end end
......
...@@ -180,7 +180,7 @@ RSpec.describe 'gitlab:db namespace rake task' do ...@@ -180,7 +180,7 @@ RSpec.describe 'gitlab:db namespace rake task' do
.with('some_index_name', logger: instance_of(Logger)) .with('some_index_name', logger: instance_of(Logger))
.and_return(reindex) .and_return(reindex)
expect(reindex).to receive(:execute) expect(reindex).to receive(:perform)
run_rake_task('gitlab:db:reindex', '[some_index_name]') run_rake_task('gitlab:db:reindex', '[some_index_name]')
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment