Tests de brokers

Tester la logique de reconnexion et le backoff MQTT

Tester la reconnexion MQTT : simulez redémarrages du broker et coupures silencieuses, vérifiez le backoff exponentiel, le réabonnement et la reprise de session.

Mis à jour · 9 min de lecture

Tester la logique de reconnexion et le backoff MQTT

Le code de reconnexion est la partie d'un client MQTT qui s'exécute le moins en développement et le plus en production. Pour tester la logique de reconnexion MQTT, cassez volontairement la connexion de la même manière que le monde réel — redémarrage du broker, connexions refusées, coupure réseau silencieuse — et vérifiez quatre points : le client s'en rend compte, il attend avec des délais croissants, il se réabonne, et aucun message QoS 1 n'est perdu pendant son absence.

Ce guide vous fournit un client de test Python qui journalise tout, trois scénarios de panne reproductibles en quelques minutes et une checklist de vérifications.

À quoi ressemble une reconnexion « correcte »

ComportementÀ vérifier
DétectionConnexions refusées/réinitialisées détectées aussitôt ; coupures silencieuses en ~1–2 intervalles de keep-alive
BackoffLes délais augmentent (par exemple 1, 2, 4, 8 … s) jusqu'à un plafond, puis repartent à zéro après une connexion réussie
JitterLes appareils ne réessaient pas en cadence — sinon vous provoquez une tempête de reconnexions
RéabonnementLes abonnements sont rétablis, que le broker ait conservé la session ou non
Reprise de sessionAvec une session persistante, les messages QoS 1/2 en attente arrivent après la reconnexion
Abandonner proprementLes échecs d'authentification sont journalisés clairement au lieu de marteler le broker indéfiniment

L'aspect conception — comment choisir les délais et les paramètres de session — est traité dans les bonnes pratiques de reconnexion MQTT. Ici, il s'agit de prouver que votre client le fait vraiment.

Comment les bibliothèques courantes se reconnectent

  • paho-mqtt (Python 2.x) se reconnecte automatiquement dans loop_forever() et loop_start(). reconnect_delay_set(min_delay, max_delay) règle le backoff exponentiel ; il n'y a pas de jitter.
  • MQTT.js 5 se reconnecte automatiquement toutes les reconnectPeriod millisecondes (1000 par défaut) — un intervalle fixe, pas exponentiel. Par défaut, il se réabonne aux topics précédents (resubscribe: true).
  • Eclipse Paho Java se reconnecte si setAutomaticReconnect(true) est activé, en doublant l'attente jusqu'à un maximum configurable.
  • MQTTnet laisse la reconnexion à votre code : vous gérez l'événement de déconnexion et rappelez connect avec votre propre délai.

Quelle que soit la bibliothèque, les valeurs par défaut correspondent rarement à ce qu'il faut en production — c'est justement pour cela qu'elles méritent un test.

Un client de test qui journalise chaque transition

Ce client paho-mqtt 2.x utilise MQTT 5 avec une session persistante, se réabonne à chaque connexion et affiche un horodatage monotone pour chaque événement, si bien que vous lisez le backoff directement dans la sortie :

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)   # toujours se réabonner

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       # conserver la session 1 heure
client.connect_async(HOST, PORT, keepalive=10, clean_start=False, properties=props)
client.loop_forever(retry_first_connection=True)

Gardez un client ID fixe pendant les tests : une session persistante appartient à un client ID, et deux processus avec le même ID vont s'éjecter mutuellement en boucle. Un keepalive=10 court fait apparaître rapidement les coupures silencieuses ; utilisez votre valeur de production pour l'essai final.

Scénario 1 : redémarrage et panne du broker

Lancez localement un Mosquitto jetable que vous pourrez arrêter à volonté :

Broker local à abattre
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

Sortie attendue, avec des délais qui doublent jusqu'au retour du broker :

À quoi ressemble un backoff correct
    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}

Le libellé exact des reason codes varie selon la version, mais les écarts entre tentatives doivent être de 1, 2, 4, 8, 16 et 32 secondes. Notez session_present: False après le redémarrage : ce Mosquitto n'a pas de persistance, la session a donc disparu avec le processus. Votre client doit se réabonner — si les messages cessent d'arriver après ce test, c'est là qu'est le bug. Activez persistence true dans Mosquitto (voir la persistance Mosquitto) et recommencez pour voir la session survivre à un redémarrage.

Scénario 2 : coupure réseau silencieuse

Un redémarrage du broker ferme proprement les sockets. Un point d'accès Wi-Fi bloqué ou un timeout NAT, non : les paquets disparaissent, et le client ne l'apprend que par le keep-alive. Sur une machine Linux, une règle de pare-feu suffit à le simuler :

Rejeter silencieusement le trafic vers le port 1883, puis le rétablir
sudo iptables -I OUTPUT -p tcp --dport 1883 -j DROP
sleep 45
sudo iptables -D OUTPUT -p tcp --dport 1883 -j DROP

Utilisez -j REJECT --reject-with tcp-reset au lieu de -j DROP pour simuler une réinitialisation immédiate. La différence compte : avec DROP, la sonde doit journaliser disconnected environ 10 à 20 secondes après l'ajout de la règle (un à deux intervalles de keep-alive), puis appliquer le backoff jusqu'au retrait de la règle. Si elle ne journalise jamais de déconnexion, le keep-alive ne fonctionne pas — voir le keep alive MQTT.

Pour la latence, la perte de paquets, les limites de bande passante ou les environnements multiplateformes, un proxy est plus souple qu'iptables ; tester avec Toxiproxy montre comment injecter ces pannes entre client et broker.

Scénario 3 : reprise de session avec messages en attente

C'est le test que la plupart des équipes sautent, et d'où viennent la plupart des incidents. Pointez la sonde vers un broker qui conserve les sessions — par exemple l'endpoint partagé broker.connectmq.com — puis :

  1. Lancez reconnect_probe.py et attendez connected avec l'abonnement en place.
  2. Bloquez le port 1883 avec la règle iptables du scénario 2.
  3. Pendant que la sonde est hors ligne, publiez cinq messages QoS 1 sur testmqtt/reconnect/probe/q par un chemin que la règle ne bloque pas — par exemple le client MQTT en ligne, qui se connecte en WebSockets sécurisés sur le port 8084.
  4. Retirez la règle et regardez la sonde se reconnecter.

La sonde doit journaliser session_present: True, puis les cinq messages. Si elle se reconnecte avec session_present: False, le broker a abandonné la session : vérifiez clean_start, l'intervalle d'expiration de session et si votre client ID a changé. Si des messages arrivent avec dup: True ou en double, c'est le comportement at-least-once du QoS 1 qui fonctionne comme prévu — votre handler doit être idempotent. Les sessions persistantes expliquent ce que le broker conserve et combien de temps.

D'autres cas de panne à tester

  • Identifiants modifiés pendant la déconnexion. Le broker répond à la reconnexion par « Not authorized » ou « Bad user name or password ». Le client doit le journaliser clairement et ralentir, pas réessayer chaque seconde indéfiniment. Recherchez les codes avec l'outil reason codes.
  • Panne DNS. Coupez complètement le réseau ; les erreurs de résolution doivent suivre le même chemin de backoff que les connexions refusées.
  • Client ID en double. Lancez la sonde deux fois. Chaque instance éjecte l'autre et les deux se reconnectent sans fin — une boucle classique en production.
  • Pannes longues. Laissez le broker arrêté plus longtemps que max_delay et vérifiez que les tentatives se stabilisent au plafond au lieu de continuer à croître ou de s'arrêter.
  • Reconnexion pendant une publication. Publiez des messages QoS 1 en boucle pendant que vous coupez le réseau, puis comptez ce qui arrive chez un subscriber après le rétablissement.

Checklist des tests de reconnexion

  1. Journalisez chaque connexion, déconnexion et tentative échouée avec un horodatage monotone.
  2. Redémarrez le broker : vérifiez la détection, les délais croissants et le rétablissement.
  3. Supprimez le trafic silencieusement : vérifiez la détection par keep-alive en un à deux intervalles.
  4. Mettez des messages QoS 1 en attente pendant la déconnexion : vérifiez session_present et la livraison après reconnexion.
  5. Vérifiez le réabonnement après un redémarrage sans persistance.
  6. Testez des identifiants erronés, une panne DNS et des client IDs en double.
  7. Automatisez les scénarios en CI avec un conteneur de broker jetable.

Une logique de reconnexion ne vaut que par la dernière fois où vous l'avez vue fonctionner. Une sonde qui affiche des horodatages, un broker que vous pouvez arrêter et une règle de pare-feu suffisent à prouver le backoff, le réabonnement et la reprise de session — et à attraper la régression avant qu'un redémarrage du broker ne s'en charge à votre place.

Questions fréquentes

Pourquoi mon client MQTT ne reçoit-il plus de messages après la reconnexion ?

Le plus souvent, la session n'a pas été reprise : le client s'est connecté avec une session propre (ou le broker a perdu la session au redémarrage) et ne s'est pas réabonné. Réabonnez-vous à chaque connexion dans le callback et vérifiez le flag session present du CONNACK.

Combien de temps faut-il à un client MQTT pour détecter une connexion morte ?

Si le réseau tombe silencieusement, le client ne s'en aperçoit que lorsque son ping de keep-alive reste sans réponse, généralement en un à deux intervalles de keep-alive. Une connexion refusée ou réinitialisée est détectée immédiatement.

paho-mqtt utilise-t-il un backoff exponentiel ?

Oui. Avec loop_forever() ou loop_start(), paho-mqtt se reconnecte automatiquement et double l'attente de min_delay à max_delay, réglés avec reconnect_delay_set(). Il n'ajoute aucun jitter aléatoire : ajoutez le vôtre si de nombreux appareils suivent le même rythme.