Job Sidekiq de longa execução reiniciando código interno

Estou atualmente criando um plugin relacionado ao meritmoot.com (que está em desenvolvimento e em lançamentos contínuos) e, devido ao seu uso de dados externos, estou utilizando um job Sidekiq de longa duração. Infelizmente, por algum motivo, o job continua reiniciando o código interno sem falhar ou fornecer saída de erro. Há algo que estou deixando passar no Sidekiq que poderia causar esse reinício?

No meu último job (que deveria ocorrer apenas uma vez por dia) relacionado aos Roll Calls, o job foi reiniciado nos seguintes intervalos, sem nenhum erro:

0h-58m-42s (início da primeira iteração)
1h-30m-12s (primeiro reinício)
2h-1m-1s (segundo reinício)
3h-46m-49s
4h-17m-11s
4h-47m-33s

O job não foi concluído e todo o progresso foi reiniciado. Vale ressaltar que possuo meu próprio processo de registro, que redireciona stderr e stdout do meu código interno, embora duvide que isso interfira no Sidekiq. (Pergunte-me se quiser dar uma olhada, é muito útil para desenvolvimento!)

É possível salvar o progresso realizado dentro do código, mas eu preferiria um processo mais simples, pois isso gera sobrecarga. Há algo que estou deixando passar no Sidekiq que poderia fazer meu código disparar um reinício?

Então você quer um job do Sidekiq que intencionalmente leve mais de uma hora para ser executado? Pode explicar um pouco mais o motivo?

Estou recebendo muitos dados externos e armazenando-os internamente no meu site. Os dados tratam de informações do Congresso dos EUA.

edit: como projetos de lei e votações públicas, além de informações sobre os membros.

Está ficando sem memória? Em nossa hospedagem, configuramos o Sidekiq para reiniciar automaticamente se começar a usar muita memória.

Aqui estão algumas ideias:

  • Envie os dados via API (mas você atingirá os limites de taxa)
  • Execute um script externo, especialmente se ele precisar apenas buscar o lote inicial de dados e as atualizações levarem menos tempo

memória no sentido de memória do banco de dados / disco rígido? Estou usando bastante disso, sim. Atualmente, estou analisando uma sugestão ligeiramente modificada de @pfaffman, na qual eu faço o fork do processo, permitindo que a thread principal termine, mas criando um processo filho que mantém o mesmo contexto. (essencialmente um script externo em relação ao sidekiq)

Testando o padrão de https://stackoverflow.com/questions/806267/how-to-fire-and-forget-a-subprocess#806326 para resolver o problema:

pid = Process.fork
if pid.nil? then
  # No filho
  exec "whatever --take-very-long"
else
  # No pai
  Process.detach(pid)
end

É um problema um pouco estranho, mas a API à qual estou me conectando não possui funcionalidade de atualização, então estou basicamente apenas atualizando os dados baixando novamente grandes partes da API todos os dias :expressionless: :man_shrugging:

Vou te avisar como fica em algumas horas.

edit: parou novamente - acho que vou salvar meu progresso periodicamente e procurar maneiras de torná-lo mais eficiente.

Em vez de fazer isso, por que não ensinar seu trabalho a ser executado em partes menores? Será que realmente precisa levar 4 horas? Sincronize 10 tópicos, depois mais 10… e assim por diante.

O Sidekiq não possui nada que encerre jobs de longa execução; recompilações do aplicativo farão isso, assim como atualizações feitas pela interface web.

Obrigado por toda a ajuda.

No final, acabei removendo o código do processo, pois não era eficaz. O trabalho de reinicialização apenas mascarava um problema subjacente de ser realmente ineficiente :sweat_smile:. Em vez disso, tenho:

  • Escrevendo código SQL em lote (que é muito mais rápido do que sequencial), detectando quando realmente precisa atualizar e permitindo que eu pule o uso da classe PostRevisor para reatualizar itens que não mudaram
  • Aumentando a eficiência na recuperação de dados via HTTP usando conexões persistentes e outros pontos de dados que incluem itens compactados (quando possível)

Descobri que escrever comandos SQL em lote proporciona um aumento imenso de velocidade. O que estou atualizando é:

tabela post: cooked, colunas last_updated
tabela topic: title, colunas last_updated

Minha próxima ideia é pular completamente o PostRevisor fazendo algo na linha de:

1 - mover dados para uma tabela temporária

2 - UPDATE topics FROM temp_table
SET topics.title = temp_table.title, topics.last_updated = temp_table.last_updated
WHERE topics.id = temp_table.id AND topics.title != temp_table.title

3 - UPDATE posts FROM temp_table
SET posts.raw = temp_table.raw, posts.last_updated = temp_table.last_updated
WHERE posts.id = temp_table.id AND posts.raw != temp_table.raw

4 - e então acionar o trabalho de reindexação da pesquisa, já que o título e o conteúdo mudaram.

Há algo que estou deixando passar? O Discourse é complexo, e ao pular o PostRevisor, sinto que poderia mexer em tabelas com as quais não tenho experiência (post_stats, post_timings, post_uploads, quoted_posts são algumas que vejo no banco de dados). No entanto, também não preciso de toda a validação que o PostRevisor fornece, já que o sistema está recebendo essas revisões de uma fonte confiável e previsível. Parece uma solução meio que acerta e meio que erra.

O que você acha?

Atualização - Estava fazendo algumas verificações de código, já que houve uma quantidade estranha de atualizações ao longo do tempo, e descobri que há algo causando atualizações indevidas em itens de dados que, na verdade, não sofreram alterações em seu formato JSON bruto. Assim que esse erro for resolvido, o acima provavelmente não será mais necessário :tipping_hand_man: Deveria ter feito testes… teria me economizado muito de tempo. Acho que ainda posso tentar o acima, mas não será prioridade. Isso ajudará em atualizações rápidas quando eu alterar o formato de apresentação dos dados. Além disso, já está escrito, apenas não testado.

Finalizei o código da atualização em lote — você teria interesse em tê-lo enviado para uma branch específica assim que estiver mais estável? Seu caso de uso é bastante específico, mas, para o que faz, consegue atualizar milhares de registros rapidamente, incluindo tags. Ele foi construído para estender TopicsBulkAction. Aqui está o README que escrevi, caso queira informações mais detalhadas:

  # entrada
  #   - lista de hashes contendo cooked, post_id, topic_id, title, updated_at, tags (raw apontará para cooked)
  #   [{post_id: #, cooked: "", topic_id: #, title: "", updated_at: date_time, tags: [{tag: "", tagGroup: ""}, ... ] } ,  ... ]
  #   - category_name, o nome da categoria sendo atualizada. Isso é usado para indexação de busca.

  # atributos de hash opcionais para incluir nos itens da lista:
  #   - raw, se não incluído, será igual a cooked.
  #   - fancy_title, se não incluído, será igual a title
  #   - slug, se não incluído, será processado a partir de title (isso está relacionado à sua URL)

  # caso de uso: atualizar tópicos regularmente a partir de uma fonte de dados externa ao Discourse em constante mudança, de forma eficiente,
  # para espelhar a atualização das informações. Note que isso não foi feito para postagem geral de posts ou tópicos, mas para atualizar
  # o título do tópico e o post PRINCIPAL do tópico. Para revisão geral de posts, vá até PostRevisor em lib/post_revisor.rb

  # - Assume pré-cozido, cooked personalizado ou exibido como está. Os dados não são validados.
  # - os posts devem ter (cook_methods: Post.cook_methods[:raw_html]) definido na criação se seu raw == cooked.
  #     Você faria isso se estivesse escrevendo HTML personalizado para exibir dentro do post.
  #     Caso contrário, o Discourse pode re-cozinhar no futuro, o que seria ruim. Certifique-se de que a fonte da informação
  #     seja confiável e seu conteúdo escapado.
  # - Se o acima não for ideal, então certifique-se de incluir raw, defina o método de cozimento correto na criação do seu post
  #     (caso o sistema re-cozinhe), execute raw através do seu método de cozimento escolhido e inclua raw e o cooked gerado
  #     nos seus hashes.
  # - Mantém o rastreamento da contagem de palavras registrando as diferenças entre as contagens de palavras antes e depois do post e passando isso
  #     para o tópico.
  # - Mantém o rastreamento da contagem de tags de maneira similar