Testes de brokers

Como testar a lógica de reconexão e o backoff no MQTT

Como testar a reconexão MQTT: simule reinícios do broker e quedas silenciosas de rede, verifique o backoff exponencial, a reinscrição e a retomada da sessão.

Atualizado · 9 min de leitura

Como testar a lógica de reconexão e o backoff no MQTT

O código de reconexão é a parte de um cliente MQTT que menos roda em desenvolvimento e mais roda em produção. Para testar a lógica de reconexão MQTT, quebre a conexão de propósito das mesmas formas que o mundo real faz — reinício do broker, conexões recusadas, uma queda silenciosa de rede — e verifique quatro coisas: o cliente percebe, espera com atrasos crescentes, se inscreve de novo e nenhuma mensagem QoS 1 se perde enquanto ele esteve fora.

Este guia traz um cliente de teste em Python que registra tudo, três cenários de falha que você reproduz em minutos e um checklist de verificações.

Como é um comportamento de reconexão "correto"

ComportamentoO que verificar
DetecçãoConexões recusadas/resetadas são percebidas na hora; quedas silenciosas em ~1–2 intervalos de keep-alive
BackoffOs atrasos crescem (por exemplo 1, 2, 4, 8 … s) até um teto e são zerados após uma conexão bem-sucedida
JitterOs dispositivos não tentam todos ao mesmo tempo — senão você provoca uma tempestade de reconexões
ReinscriçãoAs inscrições são restauradas, quer o broker tenha mantido a sessão ou não
Retomada de sessãoCom sessão persistente, as mensagens QoS 1/2 enfileiradas chegam após a reconexão
Desistir do jeito certoFalhas de autenticação são registradas com clareza em vez de martelar o broker para sempre

A parte de design — como escolher atrasos e configurações de sessão — está em boas práticas de reconexão MQTT. Aqui o foco é provar que o seu cliente faz isso.

Como as bibliotecas mais comuns reconectam

  • paho-mqtt (Python 2.x) reconecta automaticamente dentro de loop_forever() e loop_start(). reconnect_delay_set(min_delay, max_delay) controla o backoff exponencial; não há jitter.
  • MQTT.js 5 reconecta automaticamente a cada reconnectPeriod milissegundos (1000 por padrão) — um intervalo fixo, não exponencial. Por padrão, ele se reinscreve nos topics anteriores (resubscribe: true).
  • Eclipse Paho Java reconecta quando setAutomaticReconnect(true) está ativado, dobrando a espera até um máximo configurável.
  • MQTTnet deixa a reconexão para o seu código: você trata o evento de desconexão e chama connect de novo com o seu próprio atraso.

Seja qual for a biblioteca, os padrões raramente são o que você quer em produção — e é justamente por isso que merecem um teste.

Um cliente de teste que registra cada transição

Este cliente paho-mqtt 2.x usa MQTT 5 com sessão persistente, se reinscreve a cada conexão e imprime um timestamp monotônico para cada evento, para você ler o backoff direto na saída:

reconnect_probe.py
import os, time
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties

HOST = os.getenv("MQTT_HOST", "localhost")
PORT = int(os.getenv("MQTT_PORT", "1883"))
TOPIC = "testmqtt/reconnect/probe/#"
T0 = time.monotonic()

def log(event, **info):
    print(f"{time.monotonic() - T0:8.2f}s  {event:<15} {info}", flush=True)

def on_connect(client, userdata, flags, reason_code, properties):
    log("connected", rc=str(reason_code), session_present=flags.session_present)
    if not reason_code.is_failure:
        client.subscribe(TOPIC, qos=1)   # sempre se reinscrever

def on_disconnect(client, userdata, flags, reason_code, properties):
    log("disconnected", rc=str(reason_code))

def on_connect_fail(client, userdata):
    log("connect_failed")

def on_message(client, userdata, msg):
    log("message", topic=msg.topic, payload=msg.payload.decode(), dup=msg.dup)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id="reconnect-probe-1", protocol=mqtt.MQTTv5)
if os.getenv("MQTT_USER"):
    client.username_pw_set(os.getenv("MQTT_USER"), os.getenv("MQTT_PASS"))
client.on_connect, client.on_disconnect = on_connect, on_disconnect
client.on_connect_fail, client.on_message = on_connect_fail, on_message
client.reconnect_delay_set(min_delay=1, max_delay=32)

props = Properties(PacketTypes.CONNECT)
props.SessionExpiryInterval = 3600       # manter a sessão por 1 hora
client.connect_async(HOST, PORT, keepalive=10, clean_start=False, properties=props)
client.loop_forever(retry_first_connection=True)

Mantenha o client ID fixo durante os testes — uma sessão persistente pertence a um client ID, e dois processos com o mesmo ID vão se derrubar mutuamente sem parar. Um keepalive=10 curto faz as quedas silenciosas aparecerem rápido; use o valor de produção na execução final.

Cenário 1: reinício e queda do broker

Rode um Mosquitto descartável localmente para poder pará-lo quando quiser:

Broker local que você pode derrubar
docker run -d --name mqtt-reconnect -p 1883:1883 eclipse-mosquitto:2 \
  mosquitto -c /mosquitto-no-auth.conf
python reconnect_probe.py            # terminal 1

docker stop mqtt-reconnect && sleep 60 && docker start mqtt-reconnect   # terminal 2

Saída esperada, com os atrasos dobrando até o broker voltar:

Como é um backoff correto
    0.03s  connected       {'rc': 'Success', 'session_present': False}
   12.40s  disconnected    {'rc': 'Unspecified error'}
   13.41s  connect_failed  {}
   15.42s  connect_failed  {}
   19.43s  connect_failed  {}
   27.44s  connect_failed  {}
   43.45s  connect_failed  {}
   75.46s  connected       {'rc': 'Success', 'session_present': False}

Os textos exatos dos reason codes variam conforme a versão, mas os intervalos entre tentativas devem ser de 1, 2, 4, 8, 16 e 32 segundos. Repare em session_present: False após o reinício: este Mosquitto não tem persistência, então a sessão morreu junto com o processo. Seu cliente precisa se reinscrever — se as mensagens pararem de chegar depois deste teste, esse é o bug. Ative persistence true no Mosquitto (veja persistência no Mosquitto) e repita para ver a sessão sobreviver a um reinício.

Cenário 2: queda silenciosa de rede

Um reinício do broker fecha os sockets de forma limpa. Um access point Wi-Fi travado ou um timeout de NAT, não — os pacotes simplesmente somem, e o cliente só descobre pelo keep-alive. Em uma máquina Linux, dá para simular com uma regra de firewall:

Descartar todo o tráfego para a porta 1883 e depois restaurar
sudo iptables -I OUTPUT -p tcp --dport 1883 -j DROP
sleep 45
sudo iptables -D OUTPUT -p tcp --dport 1883 -j DROP

Use -j REJECT --reject-with tcp-reset em vez de -j DROP para simular um reset imediato. A diferença importa: com DROP, a sonda deve registrar disconnected cerca de 10–20 segundos depois de a regra ser adicionada (um a dois intervalos de keep-alive) e depois aplicar backoff até a regra ser removida. Se ela nunca registrar uma desconexão, o keep-alive não está funcionando — veja keep alive no MQTT.

Para latência, perda de pacotes, limites de banda ou ambientes multiplataforma, um proxy é mais flexível que o iptables; testar com Toxiproxy mostra como injetar essas falhas entre cliente e broker.

Cenário 3: retomada de sessão com mensagens enfileiradas

Este é o teste que a maioria das equipes pula e de onde vem a maioria dos incidentes. Aponte a sonda para um broker que mantém sessões — por exemplo o endpoint compartilhado broker.connectmq.com — e:

  1. Inicie reconnect_probe.py e espere o connected com a inscrição ativa.
  2. Bloqueie a porta 1883 com a regra de iptables do cenário 2.
  3. Com a sonda offline, publique cinco mensagens QoS 1 em testmqtt/reconnect/probe/q a partir de um caminho que a regra não bloqueia — por exemplo o cliente MQTT online, que conecta por WebSockets seguros na porta 8084.
  4. Remova a regra e observe a sonda reconectar.

A sonda deve registrar session_present: True e, em seguida, as cinco mensagens. Se ela reconectar com session_present: False, o broker descartou a sessão: verifique clean_start, o intervalo de expiração da sessão e se o seu client ID mudou. Se as mensagens chegarem com dup: True ou duas vezes, é o comportamento at-least-once do QoS 1 funcionando como projetado — seu handler precisa ser idempotente. Sessões persistentes explica o que o broker guarda e por quanto tempo.

Outros casos de falha que merecem teste

  • Credenciais alteradas enquanto estava offline. O broker responde à reconexão com "Not authorized" ou "Bad user name or password". O cliente deve registrar isso com destaque e desacelerar, não tentar a cada segundo para sempre. Consulte os códigos na ferramenta de reason codes.
  • Falha de DNS. Desconecte a rede por completo; erros de resolução devem passar pelo mesmo caminho de backoff que conexões recusadas.
  • Client ID duplicado. Inicie a sonda duas vezes. Cada instância derruba a outra e as duas reconectam sem fim — um loop clássico de produção.
  • Quedas longas. Deixe o broker parado por mais tempo que max_delay e verifique se as tentativas se estabilizam no teto em vez de continuar crescendo ou parar.
  • Reconexão durante publicação. Publique mensagens QoS 1 em loop enquanto corta a rede e conte o que chega a um subscriber após a recuperação.

Checklist de testes de reconexão

  1. Registre cada conexão, desconexão e tentativa falha com um timestamp monotônico.
  2. Reinicie o broker: verifique a detecção, os atrasos crescentes e a recuperação.
  3. Descarte tráfego silenciosamente: verifique a detecção por keep-alive em um a dois intervalos.
  4. Enfileire mensagens QoS 1 enquanto offline: verifique session_present e a entrega após reconectar.
  5. Verifique a reinscrição após um reinício sem persistência.
  6. Teste credenciais erradas, falha de DNS e client IDs duplicados.
  7. Automatize os cenários no CI com um contêiner de broker descartável.

A lógica de reconexão só é tão boa quanto a última vez que você a viu funcionar. Uma sonda que imprime timestamps, um broker que você pode parar e uma regra de firewall bastam para provar backoff, reinscrição e retomada de sessão — e para pegar a regressão antes que um reinício do broker faça isso por você.

Perguntas frequentes

Por que meu cliente MQTT não recebe mensagens depois de reconectar?

Normalmente a sessão não foi retomada: o cliente conectou com sessão limpa (ou o broker perdeu a sessão ao reiniciar) e não se inscreveu de novo. Reinscreva-se sempre no callback de conexão e verifique a flag session present do CONNACK.

Quanto tempo um cliente MQTT leva para perceber uma conexão morta?

Se a rede cai silenciosamente, o cliente só percebe quando o ping de keep-alive fica sem resposta — normalmente em um ou dois intervalos de keep-alive. Uma conexão recusada ou resetada é percebida na hora.

O paho-mqtt usa backoff exponencial?

Sim. Com loop_forever() ou loop_start(), o paho-mqtt reconecta automaticamente e dobra a espera de min_delay até max_delay, definidos com reconnect_delay_set(). Ele não adiciona jitter aleatório, então adicione o seu se muitos dispositivos seguem o mesmo cronograma.