Le impostazioni del sito sono obsolete in Sidekiq dopo il ri-fork

Priorità: Media, nessuna perdita di dati, ma invia in silenzio messaggi privati (PM) visibili agli utenti dal conto sbagliato, e qualsiasi impostazione del sito modificata dopo l’avvio può essere errata all’interno dei job in background.

Piattaforma: Self-hosted, configurazione standard discourse_docker con due container (data + web_only). Core 2026.7.0-latest (30d8364f0ab). Non specifico del browser — lato server.


Descrizione

Risultato effettivo: Dopo che il demone Sidekiq si è riforcato (ad esempio dopo il riavvio della memoria RSS in Demon::Sidekiq.rss_memory_check), il nuovo processo Sidekiq serve le impostazioni del sito dallo snapshot al momento dell’avvio del master unicorn, non dal database. Qualsiasi impostazione modificata dopo l’avvio del master è silenziosamente errata all’interno dei job in background, fino a quando (a) l’impostazione viene modificata nuovamente mentre quel processo Sidekiq è attivo, o (b) il container viene riavviato.

Il sintomo visibile sul mio sito: i messaggi privati di sistema automatizzati (post_hidden, flags_agreed_and_post_deleted, auto-unsuspend) sono stati inviati da un valore precedente di site_contact_username. Il database conteneva il valore corretto da cinque giorni, e il registro delle azioni dello staff non mostrava scritture in quell’intervallo. Nel frattempo, gli stessi tipi di messaggio inviati da una richiesta web utilizzavano l’utente corretto, perché i worker web avevano elaborato l’aggiornamento di MessageBus e Sidekiq no.

Risultato atteso: Un demone appena forcato dovrebbe leggere le impostazioni del sito correnti. I valori di SiteSetting in Sidekiq dovrebbero corrispondere al database, indipendentemente da quante volte Sidekiq si sia riavviato dall’avvio del master.


Passaggi riproducibili

  1. Avvia un’istanza self-hosted. Annota il valore corrente di site_contact_username (chiamiamolo userA).
  2. In Admin → Impostazioni, cambia site_contact_username in userB. Tutti i processi in esecuzione raccolgono correttamente questa modifica (MessageBus /site_settingsSiteSetting.refresh!).
  3. Termina il processo Sidekiq all’interno del container in modo che il master unicorn lo riforchi (kill <sidekiq_pid>; oppure aspetta semplicemente che Demon::Sidekiq.rss_memory_check lo riavvii una volta superato la soglia di 1000 MB di RSS — su un sito trafficato questo accade da solo).
  4. Attiva qualsiasi messaggio di sistema che viene consegnato tramite Jobs::SendSystemMessage — ad esempio, fai nascondere un post tramite segnalazioni della community, che accoda :send_system_message da Post#hide!.
  5. Apri il PM risultante.

Osservato: Il PM è autenticato da userA — il valore che era corrente quando il master è stato avviato, non userB.
Atteso: autenticato da userB.


Analisi

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

def after_fork
  @process_id = nil
  ensure_listen_for_changes
end

Non chiama mai refresh!, quindi il figlio eredita l’hash delle impostazioni current del genitore tramite copy-on-write e lo mantiene per tutta la sua durata di vita a meno che una modifica successiva non venga trasmessa.

ensure_listen_for_changes (lib/site_setting_extension.rb:713) è anche effettivamente un’operazione nulla nel figlio, perché @subscribed è ereditato come true:

def ensure_listen_for_changes
  return if @listen_for_changes == false

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

Funziona solo perché MessageBus.after_fork viene eseguito per primo (lib/discourse.rb:1064) e rivitalizza il thread del bus contro il registro delle callback ereditato.

Il demone è un semplice fork dal master (lib/demon/base.rb:181), e Sidekiq si riforca regolarmente in produzione: Demon::Sidekiq.rss_memory_check lo riavvia sopra DEFAULT_MAX_ALLOWED_SIDEKIQ_RSS_MEGABYTES = 1000. Ogni fork risuscita un valore che era stato vero l’ultima volta due settimane prima.

Questo è un problema di lunga data piuttosto che una regressione: after_fork ha avuto questa forma fin da 8fc2549 (2014) ed è invariato sull’attuale main.

Perché è facile da perdere: al primo avvio, master e Sidekiq condividono lo stesso snapshot (corretto), e mentre Sidekiq rimane attivo riceve le trasmissioni delle modifiche. La divergenza appare solo per le impostazioni modificate dopo l’avvio del master, in un processo Sidekiq forcolato dopo quella modifica — cioè inizia in silenzio, qualche tempo dopo, senza errori da nessuna parte.


Impatto oltre l’utente di contatto

site_contact_username è solo il caso visibile, perché marca un nome utente su un PM che gli utenti leggono. La stessa stazionarietà si applica a qualsiasi impostazione del sito consultata all’interno di un job — limiti di frequenza, impostazioni email/notifiche, toggle delle funzionalità, impostazioni dei plugin — senza errori e senza righe di log. Gli operatori concludono ragionevolmente “l’impostazione non si fissa” e vanno alla ricerca di qualcosa che sovrascriva il database.

Correzione suggerita

In SiteSetting.after_fork, eliminare lo stato ereditato e rileggere:

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

@subscribed = false rende anche l’abbonamento del figlio esplicito piuttosto che affidarsi a MessageBus.after_fork per rivitalizzare quello ereditato.

Soluzione alternativa per gli operatori

./launcher restart web_only (un riavvio completo del container, in modo che il master rilegga l’impostazione all’avvio). Salvare nuovamente l’impostazione nell’amministrazione corregge solo il processo Sidekiq correntemente attivo — il prossimo riforcamento la ripristina silenziosamente.

1 Mi Piace

Grazie per la segnalazione! Ho aperto una correzione per questo problema in

1 Mi Piace