Les paramètres du site sont obsolètes dans Sidekiq après un nouveau fork

Priorité : Moyenne, aucune perte de données, mais les messages privés visibles par les utilisateurs sont envoyés silencieusement depuis le mauvais compte, et tout paramètre du site modifié après le démarrage peut être incorrect dans les tâches en arrière-plan.

Plateforme : Auto-hébergée, configuration standard discourse_docker avec deux conteneurs (data + web_only). Core 2026.7.0-latest (30d8364f0ab). Non spécifique au navigateur — côté serveur.


Description

Résultat actuel : Après que le démon Sidekiq se soit ré-forké (par exemple, après le redémarrage de la mémoire RSS dans Demon::Sidekiq.rss_memory_check), le nouveau processus Sidekiq sert les paramètres du site depuis la capture instantanée au démarrage du maître unicorn, et non depuis la base de données. Tout paramètre modifié depuis le démarrage du maître est silencieusement incorrect dans les tâches en arrière-plan, jusqu’à ce que (a) le paramètre soit modifié à nouveau pendant que ce processus Sidekiq est actif, ou (b) le conteneur soit redémarré.

Le symptôme visible sur mon site : les messages privés système automatisés (post_hidden, flags_agreed_and_post_deleted, auto-unsuspend) étaient envoyés depuis une ancienne valeur de site_contact_username. La base de données contenait la valeur corrigée depuis cinq jours, et le journal des actions du personnel ne montrait aucune écriture pendant cette période. Pendant ce temps, les mêmes types de messages envoyés depuis une requête web utilisaient l’utilisateur correct, car les workers web avaient traité la mise à jour de MessageBus et Sidekiq non.

Résultat attendu : Un démon fraîchement forké devrait lire les paramètres du site actuels. Les valeurs de SiteSetting dans Sidekiq devraient correspondre à la base de données, quel que soit le nombre de redémarrages de Sidekiq depuis le démarrage du maître.


Étapes reproductibles

  1. Démarrer une instance auto-hébergée. Noter la valeur actuelle de site_contact_username (appelons-la userA).
  2. Dans Admin → Paramètres, changer site_contact_username en userB. Tous les processus en cours de fonctionnement prennent cela en compte correctement (MessageBus /site_settingsSiteSetting.refresh!).
  3. Tuer le processus Sidekiq à l’intérieur du conteneur pour que le maître unicorn le ré-forque (kill <sidekiq_pid> ; ou simplement attendre que Demon::Sidekiq.rss_memory_check le redémarre une fois qu’il dépasse le seuil RSS de 1000 Mo — sur un site actif, cela se produit naturellement).
  4. Déclencher tout message système qui est délivré via Jobs::SendSystemMessage — par exemple, laisser un message être masqué par les signalements communautaires, ce qui met en file d’attente :send_system_message depuis Post#hide!.
  5. Ouvrir le message privé résultant.

Observé : Le message privé est rédigé par userA — la valeur qui était actuelle lorsque le maître a démarré, et non userB.
Attendu : rédigé par userB.


Analyse

Discourse.after_fork appelle SiteSetting.after_fork (lib/site_setting_extension.rb:736) :

def after_fork
  @process_id = nil
  ensure_listen_for_changes
end

Il n’appelle jamais refresh!, donc l’enfant hérite du hash de paramètres current du parent via copy-on-write et le conserve pour toute sa durée de vie, sauf si un changement subsequent est diffusé.

ensure_listen_for_changes (lib/site_setting_extension.rb:713) est également effectivement un no-op dans l’enfant, car @subscribed est hérité comme true :

def ensure_listen_for_changes
  return if @listen_for_changes == false

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

Il fonctionne du tout seulement parce que MessageBus.after_fork s’exécute en premier (lib/discourse.rb:1064) et ravit le thread du bus contre le registre de callbacks hérité.

Le démon est un simple fork du maître (lib/demon/base.rb:181), et Sidekiq se ré-fork régulièrement en production : Demon::Sidekiq.rss_memory_check le redémarre au-dessus de DEFAULT_MAX_ALLOWED_SIDEKIQ_RSS_MEGABYTES = 1000. Chaque fork ressuscite une valeur qui avait été vraie pour la dernière fois il y a deux semaines.

Cela est de longue date plutôt qu’une régression : after_fork a eu cette forme depuis 8fc2549 (2014), et est inchangé sur main actuel.

Pourquoi c’est facile à manquer : au premier démarrage, le maître et Sidekiq partagent la même capture instantanée (correcte), et pendant que Sidekiq reste actif, il reçoit bien les diffusions de changement. La divergence n’apparaît que pour les paramètres modifiés après le démarrage du maître, dans un processus Sidekiq forké après ce changement — c’est-à-dire qu’elle commence silencieusement, plus tard, sans erreur nulle part.


Impact au-delà de l’utilisateur de contact

site_contact_username n’est que le cas visible, car il tamponne un nom d’utilisateur sur un message privé que les utilisateurs lisent. La même obsolescence s’applique à tout paramètre du site consulté à l’intérieur d’une tâche — limites de taux, paramètres e-mail/notification, bascules de fonctionnalité, paramètres de plugin — sans erreur et sans ligne de journal. Les opérateurs concluent raisonnablement “le paramètre ne reste pas” et cherchent quelque chose qui écrase la base de données.

Correction suggérée

Dans SiteSetting.after_fork, abandonner l’état hérité et relire :

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

@subscribed = false rend également l’abonnement propre à l’enfant explicite plutôt que de compter sur MessageBus.after_fork pour raviver celui hérité.

Contournement pour les opérateurs

./launcher restart web_only (un redémarrage complet du conteneur, afin que le maître relise le paramètre au démarrage). Re-sauvegarder le paramètre dans l’administration ne corrige que le processus Sidekiq actuellement actif — le prochain ré-fork le rétablit silencieusement.

1 « J'aime »

Merci pour ce rapport ! J’ai ouvert une correction pour cela dans

1 « J'aime »