2020-09-22 11:09:37 -04:00
# 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_'
2020-10-13 14:08:58 -04:00
STATEMENT_TIMEOUT = 6 . hours
2020-09-22 11:09:37 -04:00
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?
2020-09-25 11:09:36 -04:00
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?
2020-10-01 14:10:20 -04:00
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 )
2020-09-25 11:09:36 -04:00
logger . info " Starting reindex of #{ index } "
2020-09-22 11:09:37 -04:00
with_rebuilt_index do | replacement_index |
swap_index ( replacement_index )
end
end
private
def with_rebuilt_index
2020-09-25 11:09:36 -04:00
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
2020-09-22 11:09:37 -04:00
create_replacement_index_statement = index . definition
2020-09-25 11:09:36 -04:00
. sub ( / CREATE INDEX #{ index . name } / , " CREATE INDEX CONCURRENTLY #{ replacement_index_name } " )
2020-09-22 11:09:37 -04:00
logger . info ( " creating replacement index #{ replacement_index_name } " )
logger . debug ( " replacement index definition: #{ create_replacement_index_statement } " )
2020-10-13 14:08:58 -04:00
set_statement_timeout do
2020-09-22 11:09:37 -04:00
connection . execute ( create_replacement_index_statement )
end
2020-09-25 11:09:36 -04:00
replacement_index = Gitlab :: Database :: PostgresIndex . find_by ( schema : index . schema , name : replacement_index_name )
2020-09-22 11:09:37 -04:00
2020-09-25 11:09:36 -04:00
unless replacement_index . valid_index?
2020-09-22 11:09:37 -04:00
message = 'replacement index was created as INVALID'
logger . error ( " #{ message } , cleaning up " )
raise ReindexError , " failed to reindex #{ index } : #{ message } "
end
2020-12-01 10:09:28 -05:00
# 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?
2020-09-22 11:09:37 -04:00
yield replacement_index
ensure
2020-09-25 11:09:36 -04:00
begin
remove_index ( index . schema , replacement_index_name )
rescue = > e
logger . error ( e )
end
2020-09-22 11:09:37 -04:00
end
def swap_index ( replacement_index )
logger . info ( " swapping replacement index #{ replacement_index } with #{ index } " )
with_lock_retries do
2020-09-25 11:09:36 -04:00
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 )
2020-09-22 11:09:37 -04:00
end
end
2020-09-25 11:09:36 -04:00
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
2020-09-22 11:09:37 -04:00
end
2020-09-25 11:09:36 -04:00
def remove_index ( schema , name )
logger . info ( " Removing index #{ schema } . #{ name } " )
2020-10-13 14:08:58 -04:00
set_statement_timeout do
2020-09-25 11:09:36 -04:00
connection . execute ( << ~ SQL )
DROP INDEX CONCURRENTLY
IF EXISTS #{quote_table_name(schema)}.#{quote_table_name(name)}
SQL
2020-09-22 11:09:37 -04:00
end
end
2020-12-01 10:09:28 -05:00
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
2020-09-22 11:09:37 -04:00
def replacement_index_name
2020-09-25 11:09:36 -04:00
@replacement_index_name || = " #{ TEMPORARY_INDEX_PREFIX } #{ index . indexrelid } "
2020-09-22 11:09:37 -04:00
end
2020-09-25 11:09:36 -04:00
def replaced_index_name
@replaced_index_name || = " #{ REPLACED_INDEX_PREFIX } #{ index . indexrelid } "
2020-09-22 11:09:37 -04:00
end
def with_lock_retries ( & block )
arguments = { klass : self . class , logger : logger }
2020-09-23 14:10:15 -04:00
Gitlab :: Database :: WithLockRetries . new ( ** arguments ) . run ( raise_on_exhaustion : true , & block )
2020-09-22 11:09:37 -04:00
end
2020-10-13 14:08:58 -04:00
def set_statement_timeout
2020-10-14 11:08:42 -04:00
execute ( " SET statement_timeout TO '%ds' " % STATEMENT_TIMEOUT )
2020-10-13 14:08:58 -04:00
yield
ensure
execute ( 'RESET statement_timeout' )
end
2020-09-25 11:09:36 -04:00
delegate :execute , :quote_table_name , to : :connection
2020-09-22 11:09:37 -04:00
def connection
@connection || = ActiveRecord :: Base . connection
end
end
end
end
end