La restauración falla por la reconexión de base de datos no funcional

Situación:

Una instalación de Discourse de dos contenedores, con la web y los datos en hosts diferentes.

Problema

Una restauración por línea de comandos de un archivo grande (22 GB) desde una versión muy antigua de Discourse (es decir, con muchas migraciones largas que ejecutar) falla después de 45 minutos, justo después de la restauración de la base de datos.

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

Teoría

Discourse utiliza una segunda conexión a la base de datos para la restauración real y otra para la migración.
Cuando estas terminan, se reconecta a la base de datos en su conexión principal y ejecuta BackupMetadata.update_last_restore_date, lo cual falla inmediatamente.

La razón de este fallo parece ser que la reconexión a la base de datos no se realiza realmente.
Reutiliza el ConnectionHandler en caché. Véase aquí. Y esa conexión se pierde después de 45 minutos.

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

Solución temporal

Postgres tiene tcp_keepalives_idle = 0, lo que significa que se recurre a la configuración del sistema operativo.
El sistema operativo tiene net.ipv4.tcp_keepalive_time = 7200 (2 horas)

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

Mantiene la conexión abierta y resuelve el problema.

Corrección sugerida

Que el código de reconexión ejecute

ActiveRecord::Base.connection_handler.clear_all_connections! o algo similar antes de restablecer la conexión.

O, de forma más genérica, añadir un parámetro reconnect a establish_connection que omita el handler en caché.

1 me gusta