5

Herdei um legado sem documentação: 14 dias medindo antes de escrever qualquer linha

Peguei o acesso de um sistema que a fábrica anterior entregou e depois sumiu. README com três linhas, nenhum diagrama, ninguém do time original disponível para conversar. A primeira vontade de qualquer dev (a minha inclusive) é abrir o editor e começar a arrumar o que está feio. Nas duas primeiras semanas eu não escrevi feature nenhuma e não refatorei nada. Só medi.

A ordem que uso é sempre a mesma: subir o sistema do zero numa máquina limpa anotando cada coisa que falta, instrumentar o que já está em produção, e só então olhar o código. Estancar o sangramento é rápido. Descobrir de onde vem o sangue demora mais. Tratar a doença é remédio de meses. Quem inverte isso anuncia reescrita no dia 4 e some por um trimestre.

Medida 1: quanto tempo leva para subir isso do zero

Container vazio, cronômetro ligado, sem perguntar nada para ninguém.

$ git clone git@github.com:cliente/plataforma.git && cd plataforma
$ docker compose up -d
[+] Running 3/3
 api-1  | Error: connect ECONNREFUSED 127.0.0.1:5432
 api-1  | [nodemon] app crashed - waiting for file changes
$ docker compose logs worker | tail -3
 worker-1 | Error: Missing env var STORAGE_BUCKET_KEY
 worker-1 | Error: Missing env var LEGACY_SOAP_ENDPOINT
 worker-1 | exited with code 1

Eu não tento adivinhar nada nessa hora. Abro um arquivo de texto e anoto o que faltou, na ordem em que quebrou:

  • 11 variáveis de ambiente que só existiam no notebook do dev que saiu, e nenhuma no .env.example
  • dump do banco que a operação manda por WhatsApp, com dado de cliente real dentro
  • Node travado numa versão de 2021 por causa de uma lib nativa que não compila em versão nova
  • integração com um serviço de terceiro que não tem sandbox, então local ninguém testa
  • duas migrations que só rodam se você aplicar na ordem contrária à do timestamp

Esse arquivo virou o primeiro documento honesto do projeto. Em obra existe uma coisa chamada as-built: a planta do que foi construído de fato, não a que saiu do escritório. É isso que você está produzindo enquanto xinga o docker compose.

Do clone até o primeiro request respondendo 200 local foram uns dois dias e meio. Guardei o número, transformei o arquivo num script e passei a tratar o tempo de bootstrap como métrica:

$ time ./scripts/bootstrap.sh
==> checando dependências ok
==> subindo postgres e redis ok
==> aplicando 214 migrations ok
==> seed mínimo (3 usuários, 2 produtos) ok
==> GET /api/health -> 200

real	4m12.883s

De dois dias e meio para quatro minutos. É o único indicador do resgate que o time comercial entende sem tradução.

Medida 2: o /health que responde 200 com o banco caído

O painel do cliente estava verde havia meses. Fui ver o que ele media.

// src/routes/health.js
router.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok', version: pkg.version });
});

Esse endpoint responde 200 com o Postgres desligado, com a fila parada e com o disco cheio. A métrica existe, a garantia não. Troquei por algo que ao menos toca nas dependências:

router.get('/health', async (req, res) => {
  const checks = {};
  const started = Date.now();

  try {
    await db.raw('select 1');
    checks.db = 'ok';
  } catch (err) {
    checks.db = err.message;
  }

  try {
    checks.queueDepth = await redis.llen('bull:faturamento:wait');
  } catch (err) {
    checks.queue = err.message;
  }

  const healthy = checks.db === 'ok' && checks.queueDepth < 5000;
  res.status(healthy ? 200 : 503).json({ ...checks, ms: Date.now() - started });
});

Depois disso, o básico de observabilidade e para por aí: log estruturado com id de requisição, agrupamento de erro em qualquer ferramenta de captura, tempo de resposta por rota na borda. Nada encosta em regra de negócio, então o risco de quebrar alguma coisa é baixo.

app.use((req, res, next) => {
  req.id = req.headers['x-request-id'] || randomUUID();
  const t0 = process.hrtime.bigint();
  res.on('finish', () => {
    const ms = Number(process.hrtime.bigint() - t0) / 1e6;
    logger.info({ id: req.id, route: req.route?.path, status: res.statusCode, ms });
  });
  next();
});

Uma semana de dado real de produção me ensinou mais sobre esse sistema do que um mês lendo código. Três rotas concentravam quase todo o tráfego. E o erro que a operação descrevia como "acontece de vez em quando" acontecia umas 400 vezes por dia sem ninguém olhando.

Medida 3: a lentidão estava dentro de um laço

Com log por requisição, a rota mais lenta apareceu sozinha. 143 queries numa única chamada de fechamento de pedido.

for (const item of pedido.itens) {
  const produto = await db('produtos').where({ id: item.produto_id }).first();
  const estoque = await db('estoques').where({ produto_id: produto.id }).first();
  const preco = await db('precos').where({ produto_id: produto.id, tabela: pedido.tabela }).first();
  total += calcular(item, produto, estoque, preco);
}

O EXPLAIN ANALYZE de uma dessas, sozinha, já entregava o resto da história:

Seq Scan on estoques  (cost=0.00..18342.00 rows=1 width=48)
                      (actual time=12.431..12.433 rows=1 loops=1)
  Filter: (produto_id = 88213)
  Rows Removed by Filter: 412739
Planning Time: 0.184 ms
Execution Time: 12.470 ms

Seq scan varrendo 412 mil linhas para achar uma. Multiplica por 143 e você tem quase dois segundos gastos só em ida e volta ao banco. Índice e um whereIn fora do laço resolveram perto de 80% do tempo dessa rota, sem tocar em uma linha de regra de negócio.

Medida 4: 92% de cobertura, 41% de mutation score

A suíte tinha 92% de cobertura. Peguei o teste do módulo mais crítico:

it('calcula o total do pedido com desconto', async () => {
  const pedido = await criarPedido({ itens: 3, cupom: 'BLACK10' });
  await calcularTotal(pedido);
});

Roda, passa, conta como linha coberta e não afirma nada. Rodei mutation testing em um módulo só, para ter prova em vez de opinião:

$ npx stryker run --mutate "src/pedidos/**/*.js"
Ran 1.42 tests per mutant on average.

File        | % score | # killed | # timeout | # survived | # no cov |
------------|---------|----------|-----------|------------|----------|
All files   |   41.18 |       21 |         3 |         27 |        7 |
 total.js   |   28.57 |        4 |         0 |         10 |        4 |
 desconto.js|   45.00 |        9 |         2 |          9 |        1 |

Trocar >= por > no cálculo de desconto não matou nenhum teste. Ou seja, dava para inverter a regra de faturamento e a suíte continuava verde.

Não tentei escrever a suíte inteira em duas semanas. Escrevi teste de caracterização nos caminhos por onde o dinheiro passa, fixando o comportamento de hoje do jeito que ele está, inclusive onde está errado:

it('congela o total atual do pedido 4471 (comportamento de produção)', async () => {
  const total = await calcularTotal(fixtures.pedido4471);
  expect(total).toBe(287.4); // valor que produção devolve hoje, com o arredondamento estranho
});

Isso é rede de proteção, e ainda não é qualidade. Qualidade vem depois, quando você já sabe o que pode mexer sem derrubar receita.

Onde esse plano falha

Ele falha quando o runtime não recebe mais patch de segurança, quando uma dependência crítica está parada há anos ou quando não existe gente no mercado para a stack. Aí a conta vira e a reconstrução por pedaço entra na mesa, com o sistema antigo rodando ao lado como referência de comportamento.

Também não sei se duas semanas fazem sentido para um sistema de 3 mil linhas com quarenta usuários. Para esse, uma tarde de leitura resolve, e gastar 14 dias medindo seria só teatro de método.

A pergunta que eu deixo, porque é a que mais me interessa: quanto tempo um dev novo leva para subir o sistema que você mantém hoje, numa máquina limpa, sem perguntar nada para ninguém? Alguém aqui já mediu esse número de verdade, ou ele ainda é aquele chute otimista que a gente fala em entrevista?


Publicado originalmente no blog da Revin: https://revin.com.br/pt/blog/herdei-sistema-legado-sem-documentacao-duas-semanas

Carregando publicação patrocinada...
1

Eu também trabalho em um software um pouco maior (mais de 55 mil arquivos, quem dirá linhas).
Também leva bastante tempo para organizar e subir tudo.

Ajusta "tudo" e sempre falta alguma coisa para arrumar. Isso porque tem um arquivo de exemplo com as variáveis de ambiente. Se fosse adivinhar as variáveis de ambiente, estava lascado (quase 200 linhas, sem contar comentários).

1

Esse tipo de demanda é a que eu mais gostava de fazer, investigar, medir, depurar, documentar e só depois corrigir.

Faz muito tempo que não lido com esse tipo de tarefa, saudades 🥲...