Priorität: Mittel, kein Datenverlust, aber es werden stillschweigend nutzerorientierte PMs vom falschen
Konto gesendet, und jede Site-Einstellung, die nach dem Start geändert wird, kann in Hintergrundjobs falsch sein.
Plattform: Selbst gehostet, Standard-discourse_docker-Zwei-Container-Setup (data +
web_only). Core 2026.7.0-latest (30d8364f0ab). Nicht browserabhängig — serverseitig.
Beschreibung
Tatsächliches Ergebnis: Nachdem der Sidekiq-Dämon neu geforkt wurde (z. B. nach dem RSS-Speicher-Neustart in
Demon::Sidekiq.rss_memory_check), dient der neue Sidekiq-Prozess Site-Einstellungen aus dem
Startzeitpunkt-Snapshot des Unicorn-Masters, nicht aus der Datenbank. Jede Einstellung, die seit dem
Start des Masters geändert wurde, ist stillschweigend falsch in Hintergrundjobs, bis entweder (a) die Einstellung
wieder geändert wird, während dieser Sidekiq-Prozess lebt, oder (b) der Container neu gestartet wird.
Das sichtbare Symptom auf meiner Site: Automatisierte System-PMs (post_hidden, flags_agreed_and_post_deleted, Auto-Entsuspension) wurden von einem früheren Wert von
site_contact_username gesendet. Die Datenbank hatte den korrigierten Wert seit fünf Tagen gespeichert, und das
Mitarbeiter-Aktionsprotokoll zeigte keine Schreibvorgänge in diesem Zeitraum. Währenddessen wurden die gleichen Nachrichtentypen, die von einer Webanforderung gesendet wurden, vom korrekten Benutzer verwendet, da Web-Worker die MessageBus-Aktualisierung verarbeitet hatten und Sidekiq nicht.
Erwartetes Ergebnis: Ein frisch geforkter Dämon sollte aktuelle Site-Einstellungen lesen. SiteSetting
Werte in Sidekiq sollten mit der Datenbank übereinstimmen, unabhängig davon, wie oft Sidekiq seit dem Start des Masters neu gestartet wurde.
Reproduzierbare Schritte
- Starten Sie eine selbst gehostete Instanz. Notieren Sie den aktuellen
site_contact_username(nennen wir ihnuserA). - Ändern Sie in Admin → Einstellungen
site_contact_usernameinuserB. Alle laufenden Prozesse
nehmen dies korrekt auf (MessageBus/site_settings→SiteSetting.refresh!). - Beenden Sie den Sidekiq-Prozess im Container, damit der Unicorn-Master ihn neu forkt
(kill <sidekiq_pid>; oder warten Sie einfach, bisDemon::Sidekiq.rss_memory_checkihn neu startet, sobald er die 1000 MB RSS-Schwelle überschreitet — auf einer beschäftigten Site geschieht dies von selbst). - Lösen Sie jede Systemnachricht aus, die über
Jobs::SendSystemMessagegeliefert wird — z. B. lassen Sie einen Beitrag durch Community-Flags verstecken, was:send_system_messageausPost#hide!in die Warteschlange stellt. - Öffnen Sie die resultierende PM.
Beobachtet: Die PM wird von userA verfasst — dem Wert, der zum Zeitpunkt des Master-Starts aktuell war, nicht userB.
Erwartet: verfasst von userB.
Analyse
Discourse.after_fork ruft SiteSetting.after_fork auf (lib/site_setting_extension.rb:736):
def after_fork
@process_id = nil
ensure_listen_for_changes
end
Es ruft niemals refresh! auf, sodass das Kind das current-Einstellungs-Hash des Elternteils über
copy-on-write erbt und es für seine gesamte Lebensdauer behält, es sei denn, eine nachfolgende Änderung wird verbreitet.
ensure_listen_for_changes (lib/site_setting_extension.rb:713) ist im Kind auch effektiv ein No-op, da @subscribed als true geerbt wird:
def ensure_listen_for_changes
return if @listen_for_changes == false
unless @subscribed
MessageBus.subscribe(SITE_SETTINGS_CHANNEL) { |message| ... }
@subscribed = true
end
end
Es funktioniert überhaupt nur, weil MessageBus.after_fork zuerst läuft (lib/discourse.rb:1064) und den Bus-Thread gegen das geerbte Callback-Register wiederbelebt.
Der Dämon ist ein einfacher fork vom Master (lib/demon/base.rb:181), und Sidekiq forkt routinemäßig in der Produktion neu: Demon::Sidekiq.rss_memory_check startet ihn neu, wenn er über
DEFAULT_MAX_ALLOWED_SIDEKIQ_RSS_MEGABYTES = 1000 liegt. Jeder Fork belebt einen Wert, der vor zwei Wochen zuletzt wahr war.
Dies ist langjährig und keine Regression: after_fork hat diese Form seit
8fc2549 (2014) und ist auf der aktuellen main unverändert.
Warum es leicht zu übersehen ist: Beim ersten Start teilen Master und Sidekiq denselben (korrekten)
Snapshot, und während Sidekiq lebt, empfängt es Änderungsbenachrichtigungen. Die Divergenz erscheint nur für Einstellungen, die nach dem Master-Start geändert wurden, in einem Sidekiq-Prozess, der nach dieser
Änderung geforkt wurde — d. h. sie beginnt stillschweigend, einige Zeit später, ohne Fehler irgendwo.
Auswirkungen über den Kontaktbenutzer hinaus
site_contact_username ist nur der sichtbare Fall, da er einen Benutzernamen auf eine PM stempelt, die Benutzer lesen. Die gleiche Veraltung gilt für jede Site-Einstellung, die innerhalb eines Jobs konsultiert wird — Rate-Limits, E-Mail-/Benachrichtigungseinstellungen, Feature-Toggles, Plugin-Einstellungen — ohne Fehler und ohne
Protokollzeile. Betreiber schließen zu Recht, “die Einstellung hält nicht” und suchen nach etwas, das die Datenbank überschreibt.
Vorgeschlagene Korrektur
In SiteSetting.after_fork den geerbten Zustand fallen lassen und neu lesen:
def after_fork
@process_id = nil
@subscribed = false
ensure_listen_for_changes
refresh!
end
@subscribed = false macht auch die eigene Abonnement des Kindes explizit, anstatt sich auf MessageBus.after_fork zu verlassen, das das geerbte wiederbelebt.
Workaround für Betreiber
./launcher restart web_only (ein vollständiger Container-Neustart, sodass der Master die Einstellung beim Start neu liest). Das erneute Speichern der Einstellung im Admin repariert nur den derzeit lebenden Sidekiq-Prozess — der
nächste Re-Fork kehrt ihn stillschweigend zurück.