Pular para o conteúdo
Development

Limpando um trimestre inteiro de código morto num único PR

Por Victor Da Luz•
railsrubyrefactoringdev-logblog-manager

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

Você também pode achar útil

Proton

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 mais
RackNerd

RackNerd VPS

Hospedagem VPS econômica para serviços leves que funcionam continuamente.

Como afiliado da RackNerd, ganho com compras qualificadas.

Saiba mais
AdGuard

AdGuard 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