debian-mirror-gitlab/lib/gitlab/database/concurrent_reindex.rb
2020-11-24 15:15:51 +05:30

144 lines
4.4 KiB
Ruby

# frozen_string_literal: true
module Gitlab
module Database
class ConcurrentReindex
include Gitlab::Utils::StrongMemoize
include MigrationHelpers
ReindexError = Class.new(StandardError)
PG_IDENTIFIER_LENGTH = 63
TEMPORARY_INDEX_PREFIX = 'tmp_reindex_'
REPLACED_INDEX_PREFIX = 'old_reindex_'
attr_reader :index_name, :logger
def initialize(index_name, logger:)
@index_name = index_name
@logger = logger
end
def execute
raise ReindexError, "index #{index_name} does not exist" unless index_exists?
raise ReindexError, 'UNIQUE indexes are currently not supported' if index_unique?
logger.debug("dropping dangling index from previous run: #{replacement_index_name}")
remove_replacement_index
begin
create_replacement_index
unless replacement_index_valid?
message = 'replacement index was created as INVALID'
logger.error("#{message}, cleaning up")
raise ReindexError, "failed to reindex #{index_name}: #{message}"
end
swap_replacement_index
rescue Gitlab::Database::WithLockRetries::AttemptsExhaustedError => e
logger.error('failed to obtain the required database locks to swap the indexes, cleaning up')
raise ReindexError, e.message
rescue ActiveRecord::ActiveRecordError, PG::Error => e
logger.error("database error while attempting reindex of #{index_name}: #{e.message}")
raise ReindexError, e.message
ensure
logger.info("dropping unneeded replacement index: #{replacement_index_name}")
remove_replacement_index
end
end
private
def connection
@connection ||= ActiveRecord::Base.connection
end
def replacement_index_name
@replacement_index_name ||= constrained_index_name(TEMPORARY_INDEX_PREFIX)
end
def index
strong_memoize(:index) do
find_index(index_name)
end
end
def index_exists?
!index.nil?
end
def index_unique?
index.indisunique
end
def constrained_index_name(prefix)
"#{prefix}#{index_name}".slice(0, PG_IDENTIFIER_LENGTH)
end
def create_replacement_index
create_replacement_index_statement = index.indexdef
.sub(/CREATE INDEX/, 'CREATE INDEX CONCURRENTLY')
.sub(/#{index_name}/, replacement_index_name)
logger.info("creating replacement index #{replacement_index_name}")
logger.debug("replacement index definition: #{create_replacement_index_statement}")
disable_statement_timeout do
connection.execute(create_replacement_index_statement)
end
end
def replacement_index_valid?
find_index(replacement_index_name).indisvalid
end
def find_index(index_name)
record = connection.select_one(<<~SQL)
SELECT
pg_index.indisunique,
pg_index.indisvalid,
pg_indexes.indexdef
FROM pg_index
INNER JOIN pg_class ON pg_class.oid = pg_index.indexrelid
INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
INNER JOIN pg_indexes ON pg_class.relname = pg_indexes.indexname
WHERE pg_namespace.nspname = 'public'
AND pg_class.relname = #{connection.quote(index_name)}
SQL
OpenStruct.new(record) if record
end
def swap_replacement_index
replaced_index_name = constrained_index_name(REPLACED_INDEX_PREFIX)
logger.info("swapping replacement index #{replacement_index_name} with #{index_name}")
with_lock_retries do
rename_index(index_name, replaced_index_name)
rename_index(replacement_index_name, index_name)
rename_index(replaced_index_name, replacement_index_name)
end
end
def rename_index(old_index_name, new_index_name)
connection.execute("ALTER INDEX #{old_index_name} RENAME TO #{new_index_name}")
end
def remove_replacement_index
disable_statement_timeout do
connection.execute("DROP INDEX CONCURRENTLY IF EXISTS #{replacement_index_name}")
end
end
def with_lock_retries(&block)
arguments = { klass: self.class, logger: logger }
Gitlab::Database::WithLockRetries.new(arguments).run(raise_on_exhaustion: true, &block)
end
end
end
end