Situation :
Installation Discourse sur deux conteneurs, avec le web et les données sur des hôtes différents.
Problème
Une restauration en ligne de commande d’un gros fichier de 22 Go, provenant d’une très ancienne version de Discourse (c’est-à-dire de nombreuses migrations longues à exécuter), échoue après 45 minutes, juste après la restauration de la base de données.
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
Théorie
Discourse utilise une seconde connexion à la base de données pour la restauration proprement dite et une autre pour la migration.
Une fois celles-ci terminées, il se reconnecte à la base de données via sa connexion principale et exécute BackupMetadata.update_last_restore_date, ce qui échoue immédiatement.
La raison de cet échec semble être que la reconnexion à la base de données ne se fait pas réellement.
Elle réutilise le ConnectionHandler mis en cache. Voir ici. Et cette connexion a disparu après 45 minutes.
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
Contournement
Postgres a tcp_keepalives_idle = 0, ce qui signifie qu’il se fie au réglage du système d’exploitation.
Le système d’exploitation a net.ipv4.tcp_keepalive_time = 7200 (2 heures)
ALTER SYSTEM SET tcp_keepalives_idle = 60;
ALTER SYSTEM SET tcp_keepalives_interval = 30;
ALTER SYSTEM SET tcp_keepalives_count = 5;
Cela empêche la connexion d’être fermée et résout le problème.
Correction suggérée
Faire en sorte que le code de reconnexion exécute
ActiveRecord::Base.connection_handler.clear_all_connections! ou une commande similaire avant de rétablir la connexion.
Ou, de manière plus générique, ajouter un paramètre reconnect à establish_connection qui contourne le gestionnaire mis en cache.