إعدادات الموقع قديمة في Sidekiq بعد إعادة التفرع

الأولوية: متوسطة، لا يوجد فقدان للبيانات، لكنه يرسل رسائل خاصة مرئية للمستخدمين من حساب خاطئ بصمت، وأي إعداد للموقع تم تغييره بعد التشغيل قد يكون خاطئاً داخل المهام الخلفية.

المنصة: استضافة ذاتية، إعداد قياسي discourse_docker بوعاءين (data +
web_only). الإصدار الأساسي 2026.7.0-latest (30d8364f0ab). المشكلة ليست خاصة بمتصفح معين — بل هي على جانب الخادم.


الوصف

النتيجة الفعلية: بعد أن يعيد “الشبح” (demon) التفرع لـ Sidekiq (على سبيل المثال، بعد إعادة تشغيل الذاكرة RSS في Demon::Sidekiq.rss_memory_check)، تقوم عملية Sidekiq الجديدة بخدمة إعدادات الموقع من لقطة وقت تشغيل رئيسي يونيكورن (unicorn master’s boot-time snapshot)، وليس من قاعدة البيانات. أي إعداد تم تغييره منذ تشغيل الرئيسي يكون خاطئاً بصمت داخل المهام الخلفية، حتى إما (أ) يتم تغيير الإعداد مرة أخرى بينما تكون عملية Sidekiq هذه نشطة، أو (ب) يتم إعادة تشغيل الوعاء.

العرض الظاهر على موقعي: الرسائل الخاصة الآلية للنظام (post_hidden، flags_agreed_and_post_deleted، إلغاء التعليق التلقائي) تم إرسالها من قيمة سابقة لـ
site_contact_username. كانت قاعدة البيانات تحتفظ بالقيمة المصححة منذ خمسة أيام، وسجل إجراءات الموظفين لم ي示 أي عمليات كتابة خلال تلك الفترة. في الوقت نفسه، أنواع الرسائل نفسها المرسلة من طلب ويب استخدمت المستخدم الصحيح، لأن عمال الويب قد تعاملوا مع تحديث MessageBus بينما لم يفعل Sidekiq.

النتيجة المتوقعة: يجب أن تقرأ عملية الشبح التي تم تفرعها حديثاً إعدادات الموقع الحالية. يجب أن تتطابق قيم SiteSetting في Sidekiq مع قاعدة البيانات، بغض النظر عن عدد المرات التي أعيد فيها تشغيل Sidekiq منذ تشغيل الرئيسي.


خطوات قابلة للتكرار

  1. شغّل نسخة مستضافة ذاتياً. لاحظ site_contact_username الحالي (دعنا نسميه userA).
  2. في المسؤول → الإعدادات، غيّر site_contact_username إلى userB. جميع العمليات النشطة
    تلتقط هذا التغيير بشكل صحيح (MessageBus /site_settingsSiteSetting.refresh!).
  3. أوقف عملية Sidekiq داخل الوعاء حتى يعيد الرئيسي تفرعها
    (kill <sidekiq_pid>؛ أو ببساطة انتظر حتى يعيد Demon::Sidekiq.rss_memory_check تشغيلها بمجرد تجاوز عتبة الذاكرة RSS البالغة 1000 ميجابايت — على الموقع المشغول يحدث هذا تلقائياً).
  4. شغّل أي رسالة نظام يتم تسليمها عبر Jobs::SendSystemMessage — على سبيل المثال، دع منشوراً يتم إخفاؤه بواسطة أعلام المجتمع، مما يضيف :send_system_message إلى قائمة الانتظار من Post#hide!.
  5. افتح الرسالة الخاصة الناتجة.

الملاحظ: الرسالة الخاصة مؤلفة بواسطة userA — القيمة التي كانت سائدة عند تشغيل الرئيسي، وليس userB.
المتوقع: مؤلفة بواسطة userB.


التحليل

Discourse.after_fork يستدعي SiteSetting.after_fork (lib/site_setting_extension.rb:736):

def after_fork
  @process_id = nil
  ensure_listen_for_changes
end

لا يستدعي أبداً refresh!، لذا يرث الطفل خريطة الإعدادات current الخاصة بالوالد عبر الكتابة عند النسخ (copy-on-write) ويحتفظ بها طوال عمره ما لم يتم بث تغيير لاحق.

ensure_listen_for_changes (lib/site_setting_extension.rb:713) هو أيضاً عملياً إجراء فارغ في الطفل، لأن @subscribed يرث كقيمة true:

def ensure_listen_for_changes
  return if @listen_for_changes == false

  unless @subscribed
    MessageBus.subscribe(SITE_SETTINGS_CHANNEL) { |message| ... }
    @subscribed = true
  end
end

يعمل على الإطلاق فقط لأن MessageBus.after_fork يعمل أولاً (lib/discourse.rb:1064) ويحيي خيط الحافلة ضد سجل الاستدعاءات الموروث.

الشبح هو fork عادي من الرئيسي (lib/demon/base.rb:181)، ويعيد Sidekiq التفرع بشكل روتيني في الإنتاج: Demon::Sidekiq.rss_memory_check يعيد تشغيله فوق
DEFAULT_MAX_ALLOWED_SIDEKIQ_RSS_MEGABYTES = 1000. كل تفرع يبعث قيمة كانت صحيحة آخر مرة قبل أسبوعين.

هذه مشكلة قديمة وليست انحداراً: كان after_fork بهذا الشكل منذ
8fc2549 (2014)، ولم يتغير في الفرع الرئيسي الحالي.

لماذا من السهل تفويتها: عند التشغيل الأول، يشارك الرئيسي و Sidekiq نفس اللقطة (الصحيحة)، وبينما يبقى Sidekiq نشطاً فإنه يتلقى بث التغييرات. الانحرار يظهر فقط للإعدادات التي تم تغييرها بعد تشغيل الرئيسي، في عملية Sidekiq تم تفرعها بعد ذلك
التغيير — أي أنه يبدأ بصمت، بعد فترة زمنية، دون أي خطأ في أي مكان.


التأثير خارج نطاق المستخدم المتصل

site_contact_username هو مجرد الحالة الظاهرة، لأنه يطبع اسم مستخدم على رسالة خاصة يقرؤها المستخدمون. نفس التقادم ينطبق على أي إعداد للموقع يتم الاستعلام عنه داخل مهمة — حدود المعدل، إعدادات البريد الإلكتروني/الإشعارات، مفاتيح الميزات، إعدادات الإضافات — دون أي خطأ ودون
سطر تسجيل. يستنتج المشغلون بشكل معقول “الإعداد لا يلتزم” ويبحثون عن شيء ما يكتب فوق قاعدة البيانات.

الإصلاح المقترح

في SiteSetting.after_fork، تخلص من الحالة الموروثة وأعد القراءة:

def after_fork
  @process_id = nil
  @subscribed = false
  ensure_listen_for_changes
  refresh!
end

@subscribed = false يجعل اشتراك الطفل الخاص به صريحاً بدلاً من الاعتماد على MessageBus.after_fork لإحياء الموروث.

حل بديل للمشغلين

./launcher restart web_only (إعادة تشغيل كاملة للوعاء، بحيث يعيد الرئيسي قراءة الإعداد عند التشغيل). إعادة حفظ الإعداد في المسؤول تصلح فقط عملية Sidekiq النشطة حالياً — التفرع التالي يعيده بصمت.

إعجاب واحد (1)

شكرًا لك على الإبلاغ! لقد قمت بفتح إصلاح لهذه المشكلة في

إعجابَين (2)