فشل الاستعادة بسبب عدم عمل إعادة الاتصال بقاعدة البيانات

الوضع:

تثبيت Discourse مكوّن من حاويتين (containers)، حيث يتم استضافة الويب والبيانات على مضيفين مختلفين.

المشكلة

فشل استعادة كبيرة (حجمها 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 اتصالاً ثانياً بقاعدة البيانات للاستعادة الفعلية، واتصالاً آخر لعملية الترحيل (migration).
عند الانتهاء من ذلك، يعيد الاتصال بقاعدة البيانات عبر اتصاله الأساسي ويؤدي إلى BackupMetadata.update_last_restore_date الذي يفشل فوراً.

يبدو أن سبب هذا الفشل هو أن إعادة الاتصال بقاعدة البيانات لا تعيد الاتصال فعلياً.
بل إنها تعيد استخدام ConnectionHandler المخزّن مؤقتاً (cached). انظر هنا. وقد انقطع هذا الاتصال بعد 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

الحل البديل (Workaround)

يتم في Postgres ضبط tcp_keepalives_idle = 0، مما يعني الرجوع إلى إعداد نظام التشغيل.
إعداد نظام التشغيل هو net.ipv4.tcp_keepalive_time = 7200 (ساعتان).

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 لتجاوز المعالج المخزّن مؤقتاً (cached handler).