優先度: 中。データ損失はありませんが、誤ったアカウントからユーザー向けPM(プライベートメッセージ)が静かに送信され、ブート後に変更されたサイト設定がバックグラウンドジョブ内で誤った値として扱われる可能性があります。
プラットフォーム: セルフホスト型。標準的な discourse_docker の2コンテナ構成(data + web_only)。コアバージョン 2026.7.0-latest(30d8364f0ab)。ブラウザに依存せず、サーバーサイドの問題です。
概要
実際の結果: Sidekiqデーモンが再フォークした後(例:Demon::Sidekiq.rss_memory_check によるRSSメモリ再起動後)、新しいSidekiqプロセスはデータベースからではなく、Unicornマスタープロセスのブート時のスナップショットからサイト設定を読み込みます。マスターがブートしてから変更された設定は、(a) そのSidekiqプロセスが存続中に設定が再度変更されるか、(b) コンテナが再起動されるまで、バックグラウンドジョブ内で静かに誤った値として扱われます。
私のサイトで見られた顕著な症状:自動システムPM(post_hidden、flags_agreed_and_post_deleted、自動解除)が、site_contact_username の旧値から送信されました。データベースには5日間正しい値が保存されており、スタッフアクションログにもその期間に書き込みはありませんでした。一方、同じメッセージタイプがウェブリクエストから送信された場合は正しいユーザーが使用されていました。これは、ウェブワーカーがMessageBusの更新を処理していたのに対し、Sidekiqは処理していなかったためです。
期待される結果: 新しくフォークされたデーモンは最新のサイト設定を読み込むべきです。Unicornマスターのブート以降にSidekiqが何度再起動しても、Sidekiq内の SiteSetting 値はデータベースと一致しているべきです。
再現手順
- セルフホスト型インスタンスをブートします。現在の
site_contact_usernameを確認し、userAと呼びます。 - 管理画面 → 設定で、
site_contact_usernameをuserBに変更します。実行中のすべてのプロセスがこれを正しく取得します(MessageBus/site_settings→SiteSetting.refresh!)。 - コンテナ内のSidekiqプロセスをkillし、Unicornマスターがそれを再フォークするようにします(
kill <sidekiq_pid>;または、RSSメモリが1000 MBの閾値を超えた際にDemon::Sidekiq.rss_memory_checkが自動的に再起動するのを待ちます。繁忙なサイトでは自然に発生します)。 Jobs::SendSystemMessageを介して配信されるシステムメッセージをトリガーします。例えば、コミュニティフラグにより投稿が非表示にされ、Post#hide!から:send_system_messageがエンキューされる状態を作ります。- 生成されたPMを開きます。
観察結果: PMは 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 設定ハッシュを継承し、その後の変更がブロードキャストされない限り、そのライフタイム全体を通じてその値を保持します。
ensure_listen_for_changes(lib/site_setting_extension.rb:713)も、@subscribed が true として継承されるため、子プロセスでは実質的に何もしません(no-op):
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 を超えた際に再起動します。各フォークは、2週間前に真だった値を復活させることになります。
これは回帰(regression)ではなく長年の問題です:after_fork はこの形を2014年のコミット 8fc2549 以来保っており、現在の main ブランチでも変更されていません。
見過ごされやすい理由: 最初のブート時、マスターとSidekiqは同じ(正しい)スナップショットを共有し、Sidekiqが存続している間、変更のブロードキャストを受信します。分散が発生するのは、マスターブート後に変更された設定が、その変更後にフォークされたSidekiqプロセスで読み込まれる場合のみです。つまり、エラーログを残さず、ある時点で静かに始まります。
連絡先ユーザーへの影響以外
site_contact_username は、ユーザーが読むPMにユーザー名を刻印するため目に見えるケースに過ぎません。同じ古くなった値の問題は、ジョブ内で参照されるすべてのサイト設定(レート制限、メール/通知設定、機能トグル、プラグイン設定など)に適用され、エラーやログ行を生成しません。運用担当者は「設定が反映されない」と合理的に結論づけ、データベースを上書きしている何かを探し始めます。
提案される修正
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プロセスのみが修正され、次の再フォークで静かに元に戻ります。