Восстановление не удаётся из-за неработающего переподключения к базе данных

Ситуация:

Установка Discourse с двумя контейнерами: веб-часть и данные размещены на разных хостах.

Проблема

Командная строка для восстановления из резервной копии большого размера (22 ГБ) с очень старой версии Discourse (то есть требуется выполнение множества длительных миграций) завершается ошибкой через 45 минут, сразу после восстановления базы данных.

Reconnecting to the database...
EXCEPTION: PQconsumeInput() could not receive data from server: Connection timed out
SSL SYSCALL error: Connection timed out
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in 'PG::Connection#exec'
/var/www/discourse/vendor/bundle/ruby/3.4.0/gems/rack-mini-profiler-4.0.1/lib/patches/db/pg/alias_method.rb:109:in 'PG::Connection#async_exec'

…

from /var/www/discourse/app/models/backup_metadata.rb:16:in 'BackupMetadata.update_last_restore_date'
from /var/www/discourse/lib/backup_restore/database_restorer.rb:31:in 'BackupRestore::DatabaseRestorer#restore'
from /var/www/discourse/lib/backup_restore/restorer.rb:61:in 'BackupRestore::Restorer#run'
from script/discourse:242:in 'DiscourseCLI#restore'

…

Trying to rollback...
Cleaning stuff up...
Dropping functions from the discourse_functions schema...
Something went wrong while dropping functions from the discourse_functions schema
PQsocket() can't get socket descriptor

Предположение

Discourse использует второе соединение с базой данных для фактического восстановления и отдельное для миграции.
Когда они завершаются, система переподключается к базе данных через основное соединение и выполняет BackupMetadata.update_last_restore_date, что приводит к немедленному сбою.

Причина этого сбоя, по всей видимости, в том, что переподключение к базе данных фактически не происходит.
Используется кэшированный ConnectionHandler. См. здесь. И это соединение разрывается через 45 минут.

handler = connection_handlers[handler_key(spec)]

unless handler
  handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
  handler.establish_connection(spec.config)
  connection_handlers[handler_key(spec)] = handler
end

ActiveRecord::Base.connection_handler = handler

Обходной путь

В Postgres параметр tcp_keepalives_idle = 0, что означает использование настроек ОС.
В ОС установлено net.ipv4.tcp_keepalive_time = 7200 (2 часа)

ALTER SYSTEM SET tcp_keepalives_idle = 60; 
ALTER SYSTEM SET tcp_keepalives_interval = 30; 
ALTER SYSTEM SET tcp_keepalives_count = 5;

Это предотвращает закрытие соединения и решает проблему.

Предлагаемое исправление

Добавить в код переподключения вызов

ActiveRecord::Base.connection_handler.clear_all_connections! или аналогичный перед повторным установлением соединения.

Или, более универсально, добавить параметр reconnect в establish_connection, который обходит кэшированный обработчик.

1 лайк