データベースの再接続が機能しないため、復元が失敗します

状況:

Web とデータを異なるホストに配置した、2 コンテナ構成の Discourse インストール環境です。

問題

非常に古い Discourse バージョンからのコマンドラインによる復元(多くの長時間のマイグレーション実行が必要)が、22 GB の大きなデータセットに対して実行され、データベース復元の直後に 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 は、実際の復元には 2 つ目のデータベース接続を使用し、マイグレーションには別の接続を使用しています。
これらが完了すると、プライマリ接続でデータベースに再接続し、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 であり、OS の設定にフォールバックすることを意味します。
OS の設定は 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