Skip to content

fix(baileys): impede sockets duplicadas da mesma instância na reconexão - #7

Open
adriano-rodrigues99 wants to merge 1 commit into
main-maxpantechfrom
fix-baileys-duplicate-sockets
Open

adriano-rodrigues99 wants to merge 1 commit into
main-maxpantechfrom
fix-baileys-duplicate-sockets

Conversation

@adriano-rodrigues99

@adriano-rodrigues99 adriano-rodrigues99 commented Oct 15, 2026 •

Copy link
Copy Markdown

Resumo

Em prod, a Evolution registra de 4 a 12 mil erros stream:error conflict type=replaced por hora (log group /ecs/maxlav-evolution-api-td). Esse erro (código 440) é o WhatsApp derrubando uma conexão porque a mesma sessão foi aberta em outro lugar: duas sockets brigando pela mesma instância.

O padrão nos logs mostra que a duplicação nasce dentro do próprio processo:

  • Depois de cada restart da task o loop some e só volta depois de um tempo. Em 15/10, o restart diário foi às 08:00 UTC e o loop voltou às 11:10 UTC. Em 14/10, o deploy foi às 18:49 UTC e o loop voltou às 19:30 UTC. Depois disso ele cresce durante o dia.
  • O stg tem zero ocorrências, e usa Redis e RDS separados. Não é outro ambiente disputando as sessões.
  • A memória do container sobe junto. Em 15/10 ela ficou entre 6% e 8% até as 11h e pulou para 16% às 12h, quando o loop começou.

Causa

  • O createClient cria uma socket nova com makeWASocket sem encerrar a anterior.
  • O connectionUpdate reconecta em qualquer close que não seja 401/403/402/406, inclusive no 440, e não confere de qual socket o evento veio.
  • Então, se em algum momento aparecem duas sockets da mesma instância, a nova derruba a antiga. A antiga recebe o close e reconecta, derrubando a nova, que também reconecta, e assim por diante, até o restart da task.
  • O gatilho mais provável é o fallback do /instance/restart (instance.controller.ts): ele faz ws.close() + end() e chama connectToWhatsapp() ao mesmo tempo em que o close também reconecta. Esse endpoint é chamado pelo backend no botão de reiniciar do chat e da IA B2C, e antes de excluir uma instância. Não deu pra confirmar o horário dos restarts porque os logs do backend não estão no /ecs/maxlav-be-td.

Mudanças (só whatsapp.baileys.service.ts)

  • Encerra a socket anterior antes de criar outra (endPreviousClient): primeiro desliga os eventos dela (o ev.process devolve um unsubscribe) e depois chama end(). A ordem importa: o close que o end() emite não pode disparar mais uma reconexão.
  • Descarta eventos de socket substituída: o handler guarda a socket em que foi registrado e ignora eventos que não são da socket atual.
  • Uma conexão de cada vez por instância: se o connectToWhatsapp já está em andamento, uma segunda chamada (reconexão automática + chamada da API) reaproveita a mesma promessa em vez de abrir outra socket.
  • 440 com espera: loga em nível de erro com o nome da instância (aparece com LOG_LEVEL=ERROR) e reconecta depois de 10s, só se ninguém tiver reaberto a conexão nesse intervalo. Se ainda existir uma duplicata que este PR não cobre (ex.: dois objetos de serviço para a mesma instância), a briga cai de dezenas por minuto para uma a cada 10s, e o log mostra qual instância é.

O fallback do restart no controller não foi alterado: com as guardas acima, as duas reconexões dele convergem para uma socket só.

Riscos

  • Se a criação da socket nova falhar depois do endPreviousClient, a instância fica sem socket até a próxima reconexão ou restart. Antes, nesse mesmo ponto, a socket anterior já estava fechada, então o comportamento é equivalente.
  • Uma instância que realmente receber 440 de outra sessão legítima volta 10s depois, em vez de na hora.

Plano de teste

  • tsc --noEmit, eslint e build (tsup) passam
  • stg: chamar POST /instance/restart/{instance} algumas vezes seguidas numa instância conectada e confirmar que ela volta a open e que não aparece "type":"replaced" em /ecs/maxlav-evolution-api-td-stg
  • stg: enviar e receber mensagens depois do restart
  • prod: acompanhar a contagem de "type":"replaced" por hora (Logs Insights) e a inclinação de MemoryUtilization do serviço maxlav-ms-evolution no dia seguinte ao deploy

Refs

Refs maxpantech/infra-iac-aws#163
Relacionados: #5 (consultas da IsOnWhatsapp; também mexe neste arquivo, mas em trechos diferentes) e #6 (_count do fetchInstances). Os três juntam sem conflito (conferido com git merge-tree).

O createClient criava uma socket nova sem encerrar a anterior, e o
connectionUpdate reconectava em qualquer 'close' (inclusive o 440,
conflict/replaced) sem conferir de qual socket o evento vinha. Bastava
surgir uma segunda socket com a mesma sessão (ex.: o fallback do
/instance/restart reconectando junto com a reconexão automática) para as
duas se derrubarem pelo WhatsApp até o restart da task. Em prod isso
gerava de 4 a 12 mil erros conflict/replaced por hora.

- createClient desliga os eventos e encerra a socket anterior antes de
  criar a nova (unsubscribe antes do end, para o 'close' dela não
  reconectar de novo).
- Eventos de uma socket que já não é a atual são descartados.
- connectToWhatsapp reaproveita uma conexão em andamento em vez de abrir
  outra socket em paralelo.
- No 440, loga em nível de erro com o nome da instância e reconecta
  depois de 10s, só se ninguém tiver reaberto a conexão nesse intervalo.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant