Настройки сайта устарели в Sidekiq после его повторного форка

Приоритет: Средний, потери данных нет, но системные личные сообщения, видимые пользователям, отправляются с неправильной
учётной записи, и любые настройки сайта, изменённые после запуска, могут быть неверными внутри фоновых задач.

Платформа: Самостоятельное размещение, стандартная двухконтейнерная конфигурация discourse_docker (data +
web_only). Ядро 2026.7.0-latest (30d8364f0ab). Не зависит от браузера — проблема на стороне сервера.


Описание

Фактический результат: После повторного форка демона Sidekiq (например, после перезапуска из-за превышения памяти RSS в
Demon::Sidekiq.rss_memory_check), новый процесс Sidekiq использует настройки сайта из снапшота, созданного мастером Unicorn при его запуске, а не из базы данных. Любое изменение настроек, произошедшее после запуска мастера, будет молча неверным внутри фоновых задач, пока либо (а) настройка не будет изменена повторно, пока этот процесс 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 внутри контейнера, чтобы мастер Unicorn повторно создал его
    (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 год) и не изменился в текущей ветке main.

Почему это легко пропустить: при первом запуске мастер и 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 лайка