Logomarca da linguagem Ruby

Atualização do Ruby no monolito da RD Station

Renata Matsumoto
Ship It!

--

Como conseguimos manter nosso projeto atualizado com a última versão da linguagem

O RD Station Marketing, que hoje é a ferramenta de Automação de Marketing mais usada no Brasil, foi lançado pela RD Station em 2011. Na época, o projeto foi construído utilizando as versões mais recentes de Rails (3.0) e Ruby (1.8.7), e ambas já não são mais suportadas atualmente.

Hoje, nosso RD Station Marketing conta com cerca de 300.000 linhas de código Ruby e 470.000 linhas de código em testes automatizados unitários do rspec (dados obtidos com auxílio da gem rails_stats), distribuídas em mais de 37.000 testes e 280.000 commits. Um dos maiores desafios para quem trabalha em um projeto desse porte é manter o software atualizado, de forma a assegurar a performance e a segurança do projeto.

Entendemos que essa é uma missão a ser abraçada por todo o time de engenharia, mas, dada a complexidade dos nossos projetos, aqui na RD Station, também contamos com times especializados na atualização de frameworks, bibliotecas e dependências. No caso do projeto RD Station Marketing, esse papel é do time kernel.

A indústria de desenvolvimento de software está a todo vapor e, com isso, as pessoas desenvolvedoras estão normalmente sujeitas a grande pressão para entregar novas features e lançar novidades que acompanhem as inovações do mercado (alôoo IA). Então, será que é mesmo relevante investirmos tempo e recursos da engenharia em atualizações do sistema? Quais são os benefícios de manter o software atualizado na visão da RD Station?

Por que consideramos importante manter o sistema atualizado?

A seguir, listamos algumas das principais razões pelas quais a atualização de sistemas na RD Station é tão importante:

  1. Correções de vulnerabilidades de segurança: As atualizações incluem correções de vulnerabilidade de segurança, sendo corrigidas ao longo de suas descobertas. Ao não atualizar, você deixa seu sistema vulnerável a ataques cibernéticos e malwares. Por exemplo, a última atualização do Ruby para a versão 3.2.2 corrigiu dois “ReDÔS” (onde o ataque usa uma negação de serviço com um simples Regex para derrubar o Sistema).
  2. Correção de bugs: As atualizações também abordam problemas e bugs identificados. Por exemplo, em 2006, encontraram um Bug: ataque DoS contra servidores Ruby. A biblioteca CGI do Ruby (cgi.rb) tem um bug que permite enviar uma única requisição HTTP para qualquer programa Ruby que utiliza parsing multipart com cgi.rb com um corpo MIME mal formado, levando o processo Ruby a atingir 99% de utilização da CPU em um loop infinito, matando-o. Essas atualizações ajudam a garantir que o sistema funcione de maneira mais estável e confiável.
  3. Novos recursos e funcionalidades: Algumas atualizações introduzem novos recursos, funcionalidades e melhorias, tornando o software mais versátil e atraente para os usuários. As últimas versões de ruby, por exemplo, trouxeram melhorias nas mensagens de erro, apontando para argumentos relevantes como TypeError e ArgumentError, além do preenchimento automático no “IRB”. Outro exemplo notável em nosso histórico é a realização de uma migração crucial para o Google Analytics 4, executada com suavidade e sem impacto, mesmo em um período crítico, devido à prévia atualização das dependências essenciais.
  4. Melhorias de desempenho e de estabilidade: A cada versão nova do Ruby, o time tenta melhorar o seu desempenho, deixando-o cada vez mais rápido. Uma curiosidade é o benchmark feito com o auxílio do Optcarrot, um emulador de Nes por meio do qual é possível verificar o desempenho do Ruby a cada nova versão.

Certo, já entendemos a importância de nos mantermos atualizados. Mas isso não significa que essas atualizações ocorrem sempre de forma simples e sem riscos. Por isso, vamos te contar alguns dos maiores desafios que temos enfrentado no processo.

Quais desafios enfrentamos nos processos de atualizações?

Para iniciar a conversa, precisamos mencionar que atualizar a versão do Ruby em si não foi a mais desafiadora das missões. O verdadeiro desafio foi compreender que, juntamente com essa atualização, precisávamos explorar novos caminhos, muitas vezes desconhecidos, e analisar sistemas antigos e desatualizados. É importante destacar que atualizar a versão da linguagem não se resume apenas em mudar o número da versão e executar o “bundle update”. Requer garantir que outras dependências também estejam prontas e compatíveis com a nova versão, o que se reflete em uma camada adicional de complexidade.

Muitas outras dependências precisaram ser atualizadas antes da atualização do Ruby (o famoso bump), já que elas deviam estar compatíveis com a nova versão. Para isso, precisamos aprender o que cada “changelog” nos dizia, como testar cada nova alteração e como não quebrar nenhuma outra dependência. Além disso, foi preciso decidir o que fazer com as dependências que não recebiam mais atualizações.

E, como nem tudo são flores em uma atualização, ao subir para a versão 3.0, houve a separação dos “Keyword Arguments” ou argumentos de palavras-chave, levando à quebra de mais de 5 mil testes. Os keyword arguments alteraram a maneira como os argumentos são passados para os métodos, utilizando nomes de parâmetros ao invés de apenas a ordem ou posições no qual os argumentos são fornecidos, tornando o código mais legível e menos propenso a erros, especialmente em métodos com muitos parâmetros. Compreender essas mudanças e fazer as alterações relativas a ela em todo o projeto não foi uma tarefa fácil, mas foi possível a partir do uso da gem pry, que nos auxiliou a debugar e encontrar os problemas no código.

O RD Station Marketing, como mencionamos anteriormente, representa um monolito de porte considerável, contamos com várias equipes trabalhando num codebase único e alguns times atuando em seus microsserviços externos à base de código principal. Apesar de contarmos com inúmeros testes automatizados em muitas partes do sistema, nos trazendo maior segurança nos processos de atualizações, para garantir a qualidade das entregas, aprendemos a fazer testes de validação dos cenários de uso em todo o fluxo do nosso software. Como você pode imaginar, realizar os testes em todos os casos de uso da aplicação não foi nada fácil. Para superar esse desafio, contamos com a colaboração crucial das equipes especialistas em cada funcionalidade, o que possibilitou a elaboração de um roteiro de testes abrangente. Esse esforço conjunto nos permitiu buscar a cobertura máxima possível dos cenários e minimizar potenciais erros.

A cada desafio que encontramos, também foi fundamental contar com a especialidade de pessoas engenheiras com domínio da linguagem e do RD Station Marketing. Aqui, a nossa referência foi o André Jr, nos auxiliando pacientemente a cada novo desafio.

Vencidos os desafios, vamos explorar um pouquinho de como estruturamos nosso passo a passo para as atualizações.

Como realizamos as atualizações?

Passo a passo de uma atualização, desde a criação do pull resques até o deploy

Após resolvidos os problemas no build da aplicação e os testes quebrando, tínhamos pull requests enormes, um deles, por exemplo, apresentou 489 arquivos modificados e 11 times codeowners para solicitar review. Qual era a probabilidade dessa revisão ser feita utilizando as boas práticas?

Diante desse cenário, desenvolvemos estratégias por meio de tentativa e erro. Juntas, após a resolução de cada teste ou conclusão de cada etapa de atualização, adotamos a prática de nomear os commits contendo o caminho relativo do arquivo alterado, visando facilitar a sua separação em outra ramificação por meio do processo de cherry-pick. Cherry-pick é um comando git que permite ao usuário selecionar commits específicos de uma ramificação ou branch para outra, permitindo, assim, a criação de branchs específicas para serem avaliadas por cada time codeowner.

Isso simplificou o processo de solicitação de revisão dos times, uma vez que priorizamos pull requests menores e, consequentemente, conseguimos segmentar cada um deles por domínio, reduzindo significativamente o tempo de revisão e a complexidade para cada time e possibilitando às pessoas engenheiras avaliar apenas o código referente à sua especialidade.

Nesse ponto, nossos pull requests estavam passando em todas as etapas do CI e com as devidas aprovações dos revisores. Podíamos, então, seguir para o deploy em produção?

resposta curta é “não”. Para garantir a qualidade das entregas, iniciamos os testes de validação da maior parte possível de cenários e casos de uso da nossa aplicação. Testamos exaustivamente cada pull request. Testamos cada passo do roteiro criado em conjunto com os times nos ambientes de desenvolvimento e staging, e refizemos tudo inúmeras vezes. Nesta etapa, contamos com o auxílio indispensável de ferramentas de observabilidade e monitoramento de erros.

Após separar em pull requests menores e realizar as entregas de forma incremental — o que faz parte de um dos valores da RD Station, já que aqui priorizamos nossas entregas no formato “lean”, como você pode conferir no nosso código de cultura — restou um PR limpo, com poucos commits e bem objetivo quanto à atualização em si.

É importante destacar que, para a separação dos pull requests ser possível e, consequentemente, realizarmos o merge por etapas, foi preciso manter nosso código compatível entre as versões, reduzindo o risco de uma atualização quebrar algo base da aplicação.

Abaixo, apresentamos um exemplo de resultado, onde podemos observar o histórico completo depois de todas as alterações realizadas e mergeadas no pull request inicial:

Histórico do primeiro pull request contendo todos as etapas já mergeadas

E então, finalmente seguimos para o deploy em produção da atualização em si. Para isso, implementamos um processo faseado com o auxílio do time de SRE, que apelidamos de “Canário Manual”. Com atenção redobrada às ferramentas de observabilidade, conseguimos monitorar cada pod conforme era executado, identificando e mitigando possíveis erros. No caso da atualização que fizemos para a versão 3 do Ruby, identificamos um erro por meio do Rollbar que não ocorreu no ambiente de staging. Com o deploy faseado, conseguimos corrigir o problema e evitar um incidente, diferente do que aconteceria se estivéssemos realizando um deploy automático.

Usando as tecnologias mais recentes nos mantemos atualizados e relevantes no mercado

Apesar dos desafios, nosso software hoje utiliza a última versão lançada de Ruby (3.2) e estamos trabalhando para atualizar o Rails da versão 7.0 para 7.1 (lançada recentemente, em outubro de 2023), além de mantermos uma meta constante de manter pelo menos 90% das dependências atualizadas na versão major. Para se ter uma ideia da dimensão desse trabalho, temos atualmente 212 gems declaradas explicitamente no Gemfile e um total de 486 gems utilizadas na nossa aplicação (dados obtidos com auxílio da gem bundler-stats), mas vale lembrar que esse número está em constante mudança.

Em meio a tantas dependências, o time deve se estruturar de forma a entender quais são as prioridades, qual o benefício de tal atualização naquele momento e quanto tempo precisará ser investido. Porém, é importante ter em mente que, em algum momento, invariavelmente o processo de atualização (ou substituição) da dependência precisará ser realizado e, em geral, quanto maior a demora para abraçar o processo, mais difícil e complexo potencialmente ele se tornará.

Mantendo nosso software atualizado, acreditamos estar mais preparados para rapidamente absorver novas tecnologias que possam ter impacto positivo para o nosso cliente e a nossa marca, além de agilizar os processos de correções de bugs e vulnerabilidades.

Aprendizados, dicas e recomendações baseadas na nossa experiência

Por fim, deixamos algumas recomendações baseadas na nossa experiência e aprendizados para que o processo de uma atualização crítica ocorra da maneira mais suave possível:

  • Pequenos passos:

Confira o changelog e se atente principalmente para as mudanças que alteram o comportamento de classes ou métodos e/ou que têm potencial para quebrar o funcionamento de parte do seu código. Se a versão do seu código tem muitas diferenças para a versão atual, faça o processo de atualização aos poucos, subindo versão a versão e realizando as alterações necessárias.

  • Conte com bons testes automatizados e, se possível, ferramentas de observabilidade:

Aqui na RD, além dos testes automatizados, dispomos de testes sintéticos que monitoram o desempenho e a disponibilidade dos aplicativos, e contamos com alertas e monitoramento de erros via Datadog e Rollbar.

  • Conte com o review cuidadoso do time owner da feature potencialmente afetada:

Como mencionamos, temos um time dedicado a realizar a maior parte das atualizações no projeto RD Station Marketing, mas nem sempre esse time consegue ir a fundo para compreender todas as etapas que podem ser impactadas, principalmente em relação à experiência do usuário. Por isso, tem sido crítico contar com as pessoas especialistas por domínio para a revisão do código e validação do funcionamento da feature via app.

  • Dedique um tempo de qualidade para testar e validar o funcionamento da sua aplicação nos ambientes de desenvolvimento e staging:

A própria pessoa desenvolvedora é responsável por essa etapa aqui na RD. Por mais que possa parecer uma tarefa mecânica e que nem sempre está entre as atividades mais amadas pelas pessoas desenvolvedoras, testar cuidadosamente a sua aplicação pode evitar problemas futuros como rollback e o temido post-mortem de incidente.

Próximos passos

Para finalizar, destacamos que estamos sempre de olho em novos lançamentos para definir quais as prioridades de atualizações para o nosso momento na engenharia. Por mais confortável que a situação possa parecer, sempre pode surgir algo crítico que precise de atenção imediata, ou que traga algo benéfico para a nossa aplicação.

Manter ao menos 90% das dependências atualizadas, incorporar a nova versão do Ruby que vem sendo preparada e é lançada historicamente no Natal, atualizar o Rails para a última versão 7.1 e habilitar o compilador de código YJIT em produção visando melhorar a performance, são ações previstas ou que já estão em andamento nas prioridades do time kernel.

git do baby yoda com a mensagem: “I can do this”

Obrigada pela leitura! E se você curte trabalhar com Ruby on Rails ou ficou curioso para entender melhor como funcionam os nossos softwares e a cultura da RD Station, dá uma olhada nas vagas abertas e vem se juntar ao time!

--

--