Configurações do site estão desatualizadas no Sidekiq após re-fork

Prioridade: Média, sem perda de dados, mas envia silenciosamente mensagens privadas (PMs) voltadas ao usuário a partir da conta errada, e qualquer configuração do site alterada após a inicialização pode estar incorreta dentro dos jobs em segundo plano.

Plataforma: Auto-hospedado, configuração padrão de dois contêineres discourse_docker (data + web_only). Núcleo 2026.7.0-latest (30d8364f0ab). Não é específico do navegador — é do lado do servidor.


Descrição

Resultado atual: Após o demônio Sidekiq re-forkar (por exemplo, após a reinicialização de memória RSS em Demon::Sidekiq.rss_memory_check), o novo processo Sidekiq serve as configurações do site a partir da captura no momento da inicialização do mestre unicorn, e não do banco de dados. Qualquer configuração alterada desde que o mestre foi iniciado está silenciosamente incorreta dentro dos jobs em segundo plano, até que (a) a configuração seja alterada novamente enquanto esse processo Sidekiq estiver ativo, ou (b) o contêiner seja reiniciado.

O sintoma visível no meu site: mensagens privadas do sistema automatizadas (post_hidden, flags_agreed_and_post_deleted, auto-unsuspend) foram enviadas a partir de um valor anterior de site_contact_username. O banco de dados continha o valor corrigido há cinco dias, e o log de ações da equipe não mostrava gravações nesse intervalo. Enquanto isso, os mesmos tipos de mensagem enviados a partir de uma solicitação web usavam o usuário correto, porque os trabalhadores web haviam processado a atualização do MessageBus e o Sidekiq não.

Resultado esperado: Um demônio recém-forkado deve ler as configurações atuais do site. Os valores de SiteSetting no Sidekiq devem corresponder ao banco de dados, independentemente de quantas vezes o Sidekiq tenha sido reiniciado desde que o mestre foi iniciado.


Passos reproduzíveis

  1. Inicialize uma instância auto-hospedada. Anote o site_contact_username atual (chame-o de userA).
  2. Em Admin → Configurações, altere site_contact_username para userB. Todos os processos em execução capturam isso corretamente (MessageBus /site_settingsSiteSetting.refresh!).
  3. Mate o processo Sidekiq dentro do contêiner para que o mestre unicorn o re-forque (kill <sidekiq_pid>; ou simplesmente espere que Demon::Sidekiq.rss_memory_check o reinicie quando ultrapassar o limite de 1000 MB de RSS — em um site movimentado, isso acontece sozinho).
  4. Dispare qualquer mensagem do sistema que seja entregue via Jobs::SendSystemMessage — por exemplo, deixe que uma postagem seja ocultada por sinalizações da comunidade, o que enfileira :send_system_message de Post#hide!.
  5. Abra a PM resultante.

Observado: A PM é autorada por userA — o valor que estava vigente quando o mestre foi iniciado, e não userB.
Esperado: autorada por userB.


Análise

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

def after_fork
  @process_id = nil
  ensure_listen_for_changes
end

Ele nunca chama refresh!, então o filho herda o hash de configurações current do pai via copy-on-write e o mantém por toda a sua vida útil, a menos que uma alteração subsequente seja transmitida.

ensure_listen_for_changes (lib/site_setting_extension.rb:713) também é efetivamente um no-op no filho, porque @subscribed é herdado como true:

def ensure_listen_for_changes
  return if @listen_for_changes == false

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

Ele funciona apenas porque MessageBus.after_fork é executado primeiro (lib/discourse.rb:1064) e revive a thread do barramento contra o registro de callbacks herdado.

O demônio é um fork simples do mestre (lib/demon/base.rb:181), e o Sidekiq re-forka rotineiramente em produção: Demon::Sidekiq.rss_memory_check o reinicia acima de DEFAULT_MAX_ALLOWED_SIDEKIQ_RSS_MEGABYTES = 1000. Cada fork ressuscita um valor que tinha sido verdadeiro pela última vez há duas semanas.

Isso é de longa data, não uma regressão: after_fork tem essa forma desde 8fc2549 (2014) e está inalterado no main atual.

Por que é fácil passar despercebido: na inicialização inicial, o mestre e o Sidekiq compartilham a mesma captura (correta), e enquanto o Sidekiq permanece ativo, ele recebe transmissões de alterações. A divergência só aparece para configurações alteradas após a inicialização do mestre, em um processo Sidekiq forkado após essa alteração — ou seja, começa silenciosamente, algum tempo depois, sem erro em nenhum lugar.


Impacto além do usuário de contato

site_contact_username é apenas o caso visível, porque ele marca um nome de usuário em uma PM que os usuários leem. A mesma desatualização se aplica a qualquer configuração do site consultada dentro de um job — limites de taxa, configurações de e-mail/notificação, alternadores de recurso, configurações de plugin — sem erro e sem linha de log. Operadores concluem razoavelmente que “a configuração não gruda” e vão procurar algo que esteja sobrescrevendo o banco de dados.

Correção sugerida

Em SiteSetting.after_fork, descarte o estado herdado e leia novamente:

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

@subscribed = false também torna a assinatura do filho explícita, em vez de depender do MessageBus.after_fork para reviver a herdada.

Solução alternativa para operadores

./launcher restart web_only (uma reinicialização completa do contêiner, para que o mestre relia a configuração na inicialização). Re-salvar a configuração no admin apenas corrige o processo Sidekiq atualmente ativo — o próximo re-fork o reverte silenciosamente.

1 curtida

Obrigado pelo relatório! Abri uma correção para isso em

2 curtidas