Dans le métro, un utilisateur envoie une demande d’assistance. L’interface affiche une erreur réseau. Une fois la connexion rétablie, l’application iOS renvoie la requête, mais deux demandes apparaissent côté serveur. Le problème ne vient pas nécessairement de la file d’attente : la première requête a peut-être déjà été enregistrée, sans que la réponse de succès soit parvenue au téléphone. Pour valider les écritures hors ligne, il faut distinguer « échec de la requête » et « échec de l’écriture ».
Définir l’identité d’une opération
Enregistrez chaque envoi de l’utilisateur comme une tâche persistante contenant un ID d’opération, le corps de la requête, une clé d’idempotence, la date de création et un état. Le corps est fixé dès la mise en file d’attente. Une relance ne peut modifier que le nombre de tentatives et l’état, jamais régénérer la clé d’idempotence. Ce n’est que lorsque l’utilisateur appuie de nouveau explicitement sur « Nouveau » qu’une autre opération est créée.
Le serveur doit conserver le résultat du traitement pour chaque clé d’idempotence. À clé et corps identiques, il renvoie le résultat initial ; si le corps diffère pour une même clé, il doit rejeter la requête plutôt que remplacer discrètement les données. Le client ne peut retirer la tâche qu’après avoir reçu une confirmation explicite de succès. Un délai dépassé, une déconnexion ou une erreur serveur signifient que le résultat est inconnu : ils ne justifient pas la suppression de la tâche.
Le critère de validation n’est pas « envoyer la requête une seule fois », mais « ne créer qu’un seul enregistrement métier, même si la même opération utilisateur est envoyée plusieurs fois ».
Simuler une réponse perdue avec une API locale
Lorsqu’elle reçoit une clé pour la première fois, l’API de test ci-dessous enregistre la demande, puis renvoie 503. Si elle reçoit à nouveau la même clé, elle renvoie le résultat enregistré. Elle reproduit volontairement un cas où la réponse ne permet pas au client de savoir si l’écriture a abouti. Enregistrez ce code dans fault_server.py sur le Mac cloud :
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
records = {}
attempts = {}
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path != "/items":
self.send_error(404)
return
key = self.headers.get("Idempotency-Key")
length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(length)
if not key or not body:
self.send_error(400)
return
try:
payload = json.loads(body)
except json.JSONDecodeError:
self.send_error(400)
return
attempts[key] = attempts.get(key, 0) + 1
if key in records and records[key]["body"] != payload:
self.send_error(409)
return
if key not in records:
records[key] = {"id": len(records) + 1, "body": payload}
self.send_error(503)
return
result = json.dumps({
"id": records[key]["id"],
"attempts": attempts[key],
"record_count": len(records)
}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(result)))
self.end_headers()
self.wfile.write(result)
HTTPServer(("127.0.0.1", 8081), Handler).serve_forever()
Exécutez python3 fault_server.py. Dans le simulateur iOS, l’application peut pointer son API de test vers http://127.0.0.1:8081/items. Le serveur n’écoute ici que sur l’adresse de bouclage locale : n’exposez pas directement ce port au réseau extérieur. L’API de test conserve ses enregistrements en mémoire ; redémarrer le script les efface. Ne redémarrez donc pas le script lorsque vous testez le redémarrage de l’application. En production, le serveur doit aussi persister les résultats associés aux clés d’idempotence et garantir l’atomicité des requêtes concurrentes au moyen d’une contrainte d’unicité en base de données.
Exécuter quatre scénarios de validation, couche par couche
Commencez par prévoir un point de substitution dans la couche d’envoi des requêtes. Dans les tests unitaires de la file d’attente, faites renvoyer une erreur de déconnexion par une couche d’envoi factice : vérifiez que la tâche reste dans le stockage persistant et que son corps et sa clé n’ont pas changé. Il n’est ainsi pas nécessaire de couper la connexion de tout le Mac cloud, ce qui pourrait perturber les autres tâches exécutées sur la même machine.
Branchez ensuite l’API locale et vérifiez chaque ligne du tableau. Utilisez une nouvelle clé pour chaque scénario. Pendant le test, consignez l’ID de la tâche, sa clé, le nombre d’envois et la valeur renvoyée par le serveur, sans inscrire les champs sensibles de la requête dans les journaux.
| Scénario | Action | Critère de réussite |
|---|---|---|
| Premier envoi | Envoyer la tâche et recevoir 503 | La tâche est conservée, sa clé ne change pas |
| Nouvelle tentative après la réponse perdue | Renvoyer la tâche initiale | La réponse est 200 et record_count vaut 1 |
| Redémarrage de l’application | Après le premier 503, arrêter puis relancer l’application et reprendre la file d’attente | La clé initiale est réutilisée et il n’existe toujours qu’un seul enregistrement |
| Modification accidentelle du corps | Envoyer un autre corps avec la même clé | La réponse est 409 et la tâche passe dans un état nécessitant une investigation |
Points à vérifier dans le simulateur
Vérifiez d’abord que l’application utilise bien l’API de test, puis effacez les données de l’application testée uniquement pour partir d’un état propre. Ne supprimez pas au passage les autres projets présents dans le simulateur partagé. Après le premier envoi, vérifiez que la tâche locale existe toujours. Arrêtez le processus de l’application, relancez-la et déclenchez la reprise de la file d’attente. Observez si la deuxième requête porte la même clé. Cette étape révèle généralement les applications qui ne conservent la clé qu’en mémoire.
Ce script compte uniquement les requêtes reçues pendant la durée de vie d’un processus et ne fournit aucune API de consultation. Validez donc le résultat du scénario à l’aide des valeurs attempts et record_count de la deuxième réponse. Si vous devez comptabiliser les requêtes sur plusieurs exécutions de test, ajoutez un stockage persistant au banc de test au lieu de traiter ces compteurs en mémoire comme des métriques de production.
Fixer des conditions d’arrêt claires pour la file d’attente
Toutes les réponses ne doivent pas entraîner la même politique de relance. En cas de déconnexion, de délai dépassé ou de réponse 503 comme dans cet exemple, la tâche initiale peut être conservée et renvoyée plus tard. Une réponse 409 indique un conflit entre la clé et le corps : arrêtez les renvois automatiques et conservez les informations nécessaires au diagnostic. Seule une confirmation explicite de succès permet de marquer la tâche comme terminée. Pour une véritable API, les autres erreurs côté client doivent aussi être évaluées au cas par cas selon le contrat de l’API, et non systématiquement relancées.
Limitez également la concurrence : une tâche ne doit avoir qu’un seul émetteur à la fois. Si le retour au premier plan et la synchronisation en arrière-plan peuvent tous deux déclencher la file d’attente, attribuez d’abord le droit d’envoyer la tâche dans la couche de stockage persistant. Le serveur doit malgré tout garantir l’idempotence de son côté, car un verrou côté client ne peut pas empêcher la perte d’une réponse. Les relances peuvent suivre un délai exponentiel plafonné pour éviter que toutes les tâches affluent dès le rétablissement de la connexion. Ce délai ne doit toutefois modifier ni l’ID d’opération, ni la clé, ni le corps.
Vérification finale
Avant la fusion, faites une dernière validation manuelle : après avoir reçu l’erreur, quittez l’application, rouvrez-la et attendez le renvoi. Vérifiez que l’interface n’affiche qu’un seul enregistrement, que la tâche locale finit par être marquée comme terminée et que record_count vaut toujours 1 dans le résultat du serveur. Appuyez ensuite à nouveau sur « Nouveau » pour créer une véritable nouvelle opération, et vérifiez qu’elle produit une nouvelle clé et un nouvel enregistrement. Le premier contrôle prouve que l’ancienne opération peut être rejouée sans risque ; le second, que la règle d’idempotence ne bloque pas les nouvelles opérations de l’utilisateur.
Questions fréquentes
Faut-il créer une nouvelle opération après une réponse 503 ?
Non. La première écriture peut avoir été validée. Réessayez l’opération initiale avec la même clé d’idempotence pour éviter un doublon.
Faut-il couper la connexion du Mac cloud pour tester la file hors ligne ?
Non. Testez la mise en file avec une couche réseau remplaçable, puis le véritable rejeu HTTP avec un serveur local.
Louez un Mac dédié pour votre prochaine expérience
Choisissez un Mac mini physique, un nœud et une durée de facturation selon vos besoins. Les modèles et disponibilités affichés dans la console sont mis à jour en temps réel.