2021-01-03 14:25:43 +05:30
# frozen_string_literal: true
module Gitlab
module Database
module Reindexing
class ConcurrentReindex
include Gitlab :: Utils :: StrongMemoize
ReindexError = Class . new ( StandardError )
PG_IDENTIFIER_LENGTH = 63
TEMPORARY_INDEX_PREFIX = 'tmp_reindex_'
REPLACED_INDEX_PREFIX = 'old_reindex_'
STATEMENT_TIMEOUT = 6 . hours
attr_reader :index , :logger
def initialize ( index , logger : Gitlab :: AppLogger )
@index = index
@logger = logger
end
def perform
raise ReindexError , 'UNIQUE indexes are currently not supported' if index . unique?
raise ReindexError , 'partitioned indexes are currently not supported' if index . partitioned?
raise ReindexError , 'indexes serving an exclusion constraint are currently not supported' if index . exclusion?
raise ReindexError , 'index is a left-over temporary index from a previous reindexing run' if index . name . start_with? ( TEMPORARY_INDEX_PREFIX , REPLACED_INDEX_PREFIX )
logger . info " Starting reindex of #{ index } "
with_rebuilt_index do | replacement_index |
swap_index ( replacement_index )
end
end
private
def with_rebuilt_index
if Gitlab :: Database :: PostgresIndex . find_by ( schema : index . schema , name : replacement_index_name )
logger . debug ( " dropping dangling index from previous run (if it exists): #{ replacement_index_name } " )
remove_index ( index . schema , replacement_index_name )
end
create_replacement_index_statement = index . definition
. sub ( / CREATE INDEX #{ index . name } / , " CREATE INDEX CONCURRENTLY #{ replacement_index_name } " )
logger . info ( " creating replacement index #{ replacement_index_name } " )
logger . debug ( " replacement index definition: #{ create_replacement_index_statement } " )
set_statement_timeout do
connection . execute ( create_replacement_index_statement )
end
replacement_index = Gitlab :: Database :: PostgresIndex . find_by ( schema : index . schema , name : replacement_index_name )
unless replacement_index . valid_index?
message = 'replacement index was created as INVALID'
logger . error ( " #{ message } , cleaning up " )
raise ReindexError , " failed to reindex #{ index } : #{ message } "
end
2021-02-22 17:27:13 +05:30
# Some expression indexes (aka functional indexes)
# require additional statistics. The existing statistics
# are tightly bound to the original index. We have to
# rebuild statistics for the new index before dropping
# the original one.
rebuild_statistics if index . expression?
2021-01-03 14:25:43 +05:30
yield replacement_index
ensure
begin
remove_index ( index . schema , replacement_index_name )
rescue = > e
logger . error ( e )
end
end
def swap_index ( replacement_index )
logger . info ( " swapping replacement index #{ replacement_index } with #{ index } " )
with_lock_retries do
rename_index ( index . schema , index . name , replaced_index_name )
rename_index ( replacement_index . schema , replacement_index . name , index . name )
rename_index ( index . schema , replaced_index_name , replacement_index . name )
end
end
def rename_index ( schema , old_index_name , new_index_name )
connection . execute ( << ~ SQL )
ALTER INDEX #{quote_table_name(schema)}.#{quote_table_name(old_index_name)}
RENAME TO #{quote_table_name(new_index_name)}
SQL
end
def remove_index ( schema , name )
logger . info ( " Removing index #{ schema } . #{ name } " )
set_statement_timeout do
connection . execute ( << ~ SQL )
DROP INDEX CONCURRENTLY
IF EXISTS #{quote_table_name(schema)}.#{quote_table_name(name)}
SQL
end
end
2021-02-22 17:27:13 +05:30
def rebuild_statistics
logger . info ( " rebuilding table statistics for #{ index . schema } . #{ index . tablename } " )
connection . execute ( << ~ SQL )
ANALYZE #{quote_table_name(index.schema)}.#{quote_table_name(index.tablename)}
SQL
end
2021-01-03 14:25:43 +05:30
def replacement_index_name
@replacement_index_name || = " #{ TEMPORARY_INDEX_PREFIX } #{ index . indexrelid } "
end
def replaced_index_name
@replaced_index_name || = " #{ REPLACED_INDEX_PREFIX } #{ index . indexrelid } "
end
def with_lock_retries ( & block )
arguments = { klass : self . class , logger : logger }
Gitlab :: Database :: WithLockRetries . new ( ** arguments ) . run ( raise_on_exhaustion : true , & block )
end
def set_statement_timeout
execute ( " SET statement_timeout TO '%ds' " % STATEMENT_TIMEOUT )
yield
ensure
execute ( 'RESET statement_timeout' )
end
delegate :execute , :quote_table_name , to : :connection
def connection
@connection || = ActiveRecord :: Base . connection
end
end
end
end
end