Limpando um trimestre inteiro de código morto num único PR
Eu vinha acumulando uma lista de pequenos itens de limpeza no blog-manager: colunas remanescentes de uma integração com o Postiz que eu já tinha arrancado, uma consulta de configurações que bate no banco a cada chamada, e alguns filtros do lado do Ruby que deveriam ter sido SQL desde o início. Nada disso era urgente o suficiente para corrigir sozinho, então foi ficando adiado. Essa foi a issue “faz tudo isso de uma vez, num PR só”.
Colunas mortas do Postiz
Antes de eu construir a publicação nativa no Medium e no Dev.to, esse app usava o Postiz como intermediário de syndication. Essa integração já era, mas as colunas que ela deixou para trás não: 7 em posts (postiz_channels, postiz_error, postiz_group_id, postiz_posted_at, postiz_publish, postiz_scheduled_for, postiz_status mais o seu índice), 2 em app_settings (postiz_api_key, postiz_base_url). Primeiro rodei um grep no app inteiro por postiz: os únicos resultados restantes eram comentários explicando por que algo não é mais feito do jeito antigo do Postiz. Seguro para remover. Uma migration só, espelhando o padrão de quando removi as integrações do Hashnode e, antes dela, do Dev.to/Medium:
class RemovePostizIntegration < ActiveRecord::Migration[8.1]
def change
remove_index :posts, :postiz_status
remove_column :posts, :postiz_channels, :text
# ...six more remove_column calls
remove_column :app_settings, :postiz_api_key, :string
remove_column :app_settings, :postiz_base_url, :string
end
end
Fazendo memoização de AppSetting.current, e por que o Rails.cache era a ferramenta errada
AppSetting.current executa first_or_create! a cada chamada. Existem 11 pontos de chamada espalhados entre controllers, jobs e views, então uma única renderização de página pode bater na tabela de configurações meia dúzia de vezes para uma linha que quase nunca muda.
Meu primeiro instinto foi Rails.cache.fetch. Aí lembrei que AppSetting tem encrypts :dev_to_api_key, :medium_bridge_token, :listmonk_api_token, :pexels_api_key, e o cache store de produção é o solid_cache_store, que persiste numa tabela do banco. O encrypts só protege a coluna em repouso na sua própria tabela. Depois que o registro é carregado, esses atributos ficam em texto puro na memória. Se eu cachear o objeto inteiro já carregado, o Rails serializa (Marshal) esse texto puro para dentro da tabela do cache store. Eu teria anulado a criptografia silenciosamente, cacheando ao redor dela.
O app já tinha a ferramenta certa parada ali sem uso para esse propósito: Current < ActiveSupport::CurrentAttributes, que até então só guardava a sessão:
class Current < ActiveSupport::CurrentAttributes
attribute :session, :app_setting
delegate :user, to: :session, allow_nil: true
end
def self.current
Current.app_setting ||= first_or_create!
end
CurrentAttributes é in-process, local à thread/fiber, e o Rails o reseta automaticamente a cada requisição de controller e a cada execução de Active Job, o que importa aqui porque esse app roda um processo de worker Solid Queue separado. Uma mudança de configuração no processo web nunca fica desatualizada no worker além do job que está rodando no momento, e nada toca uma store persistente. Mesma redução no número de chamadas, sem nenhum dos riscos.
Movendo filtros do lado do Ruby para SQL
Em três lugares os posts eram carregados e filtrados em Ruby em vez de no banco. Os filtros de syndication do controller mapeavam nomes de filtro para métodos predicate, e depois faziam @posts.select(&predicate) depois de carregar o scope inteiro (~80 linhas). O dashboard carregava cada post mantido (kept) num array só para fazer .count(&:needs_medium?) quatro vezes. E o seletor da newsletter era o que tinha um bug de verdade: Post.kept.by_pub_date.limit(50).select { |post| post.live_url.present? }. Ele filtra depois de limitar. Se 50 posts não publicados aparecerem ordenados antes dos publicáveis, o seletor da newsletter mostra silenciosamente zero posts selecionáveis, mesmo havendo vários logo depois desse limite.
Adicionei scopes espelhando os predicates de instância que já existiam:
scope :needs_medium, -> { where(medium_status: [ :not_posted, :failed ]) }
scope :needs_devto, -> { where(devto_status: [ :not_posted, :failed ]) }
scope :syndication_failed, -> { where(medium_status: :failed).or(where(devto_status: :failed)) }
scope :awaiting_publish, -> { where(medium_status: :draft).or(where(devto_status: :draft)) }
scope :with_live_url, -> { joins(:blog).where.not(pub_date: nil).where.not(blogs: { base_url: [ nil, "" ] }) }
e mudei o seletor da newsletter para filtrar antes de limitar: Post.kept.includes(:blog).with_live_url.by_pub_date.limit(50).
Para garantir que os scopes SQL realmente batiam com os predicates Ruby que substituíram (em vez de simplesmente confiar na minha própria tradução), escrevi um teste de equivalência:
assert_equal Post.all.select(&:needs_medium?).map(&:id).sort, Post.needs_medium.pluck(:id).sort
Um seguro barato contra um valor de enum errado por um silenciosamente mudando o comportamento.
A única coisa que eu quase errei
A issue descrevia o código em publish_devto_article depois de um redirect_to(...) and return antecipado como “inalcançável… apaga isso”. Eu quase apaguei. Olhando de perto, aquilo não era código descartável: é o fluxo nativo inteiro de publicação no Dev.to (checagem de API key, URL ao vivo, imagem de destaque, token do GitHub, enfileiramento de job), construído antes e desligado por um gate à espera de verificação contra a API real do Forem. Apagar teria significado reconstruir tudo depois a partir do histórico do git.
Em vez disso, coloquei um gate nele com uma flag explícita:
DEVTO_PUBLISHING_ENABLED = false
# ...
def publish_devto_article
return redirect_to(@post, alert: DEVTO_DISABLED_MESSAGE) unless DEVTO_PUBLISHING_ENABLED
# full publish flow, untouched, ready to flip on once verified
end
Mesmo comportamento hoje, e habilitar isso depois vira uma mudança de uma linha só, em vez de um projeto de arqueologia.
Também nesse lote: removi um hello_controller.js sem uso (boilerplate gerado pelo Stimulus, zero referências), e corrigi uma falha de escaping de HTML no builder de campanhas da newsletter: os títulos dos posts eram escapados, mas o href não, então um título de post ou uma URL com um & teria produzido HTML malformado no corpo da newsletter.
O que vem a seguir
Nada disso muda o comportamento visível para o usuário, exceto a correção do sub-preenchimento no seletor da newsletter, que é uma correção de bug, não uma feature. 282 testes, rubocop e brakeman todos verdes antes do merge.
Leitura relacionada
Eliminando o copia-e-cola dos jobs de sindicação do blog-manager
Oito jobs, um esqueleto de broadcast, dois jobs gêmeos que divergiram na única linha que importa, e um bug de rescue achado por rastreamento, não por testes.
O que acontece quando um job faz broadcast para ninguém
Fechando o loop da imagem de destaque: frontmatter só por inserção, uma guarda que pegou divergência real na primeira execução, e um broadcast sem ouvinte.
Uma correção de desvio de documentação que não foi tão chata quanto parecia
Três itens da auditoria que cada um virou outra coisa: uma alegação meio corrigida, uma recuperação de senha silenciosamente morta, e um e-mail de staging que apontaria para produção.
Você também pode achar útil
Proton Drive
Armazenamento em nuvem criptografado, da equipe por trás do Proton Mail.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba maisRackNerd VPS
Hospedagem VPS econômica para serviços leves que funcionam continuamente.
Como afiliado da RackNerd, ganho com compras qualificadas.
Saiba maisAdGuard para iOS
Bloqueio de anúncios e rastreadores em todo o sistema no iOS, sem necessidade de um servidor DNS separado.
Como afiliado da AdGuard, ganho com compras qualificadas.
Saiba mais