« Mon rendez-vous du mardi a disparu de la confirmation que j’avais reçue » : ce signalement d’un client, une semaine après sa prise de rendez-vous par un agent conversationnel, a mené à la découverte d’un incident précis. Le créneau du mardi avait bien été réservé une première fois, puis une seconde réservation, pour un autre client, avait silencieusement écrasé la première dans le système de planification utilisé.
Ce texte détaille le symptôme observé, le diagnostic mené à partir du journal des appels de l’agent, le correctif appliqué à l’outil de réservation, et la mesure de prévention mise en place ensuite. Il ne traite pas de l’interface de prise de rendez-vous elle-même, restée inchangée du début à la fin de ce travail.
Symptôme : un créneau réservé deux fois
Le système de planification utilisé ne permettait normalement pas de double réservation sur un même créneau : une contrainte d’unicité existait bien en base pour l’empêcher. Pourtant, l’historique montrait deux tentatives de réservation distinctes pour le même créneau, à quelques secondes d’intervalle, la seconde ayant simplement remplacé la première plutôt que d’être rejetée, en raison d’une logique de mise à jour trop permissive côté outil.
Diagnostic : un appel rejoué après un délai réseau
Le journal de la session de l’agent a montré que la première tentative de réservation avait bien abouti côté serveur, mais que la confirmation n’était jamais parvenue à l’agent dans le délai attendu, en raison d’une latence réseau ponctuelle. L’agent, interprétant cette absence de réponse comme un échec, a relancé l’appel de réservation pour le même créneau quelques secondes plus tard.

{
"appels": [
{ "outil": "reserver_creneau", "t": "09:14:02.100", "params": { "creneau_id": 4821, "client_id": 118 }, "reponse": null },
{ "outil": "reserver_creneau", "t": "09:14:07.340", "params": { "creneau_id": 4821, "client_id": 118 }, "reponse": "ok" }
]
}
Pourquoi la contrainte d’unicité n’a pas suffi
La contrainte d’unicité en base empêchait bien deux clients différents de réserver le même créneau simultanément, mais l’outil de réservation, lui, traitait chaque appel comme une mise à jour du créneau plutôt que comme une création stricte : un second appel avec les mêmes paramètres mettait simplement à jour l’enregistrement existant, sans erreur, ce qui masquait le rejeu plutôt que de le signaler.
Correctif : une clé d’idempotence portée par le créneau et le client
Le correctif a consisté à faire porter, par chaque appel de réservation, une clé d’idempotence dérivée de l’identifiant du créneau et de celui du client, plutôt que de laisser l’outil accepter silencieusement tout appel répété comme une simple mise à jour.
function agence_reserver_creneau_callback( $params ) {
$creneau_id = $params['creneau_id'];
$client_id = $params['client_id'];
$cle = 'reservation-' . $creneau_id . '-' . $client_id;
$existante = agence_trouver_reservation_par_cle( $cle );
if ( $existante ) {
return $existante; // Renvoie le résultat déjà obtenu, sans nouvel effet
}
return agence_creer_reservation( array(
'creneau_id' => $creneau_id,
'client_id' => $client_id,
'cle' => $cle,
) );
}
Prévention : distinguer explicitement création et mise à jour
Au-delà de la clé d’idempotence, l’outil de réservation a été scindé en deux fonctions distinctes côté callback, l’une strictement réservée à la création d’une réservation, l’autre à la modification d’une réservation déjà existante. Cette séparation, absente avant l’incident, empêche désormais qu’un appel de création rejoué ne puisse jamais, même en cas d’erreur de logique ailleurs, se comporter comme une mise à jour silencieuse.
- Clé d’idempotence dérivée d’identifiants stables, jamais générée à chaque appel.
- Séparation stricte entre création et mise à jour dans la logique de l’outil.
- Contrainte d’unicité en base conservée comme filet de sécurité, pas comme seule protection.
Une absence de réponse ne signifie jamais un échec certain : elle signifie seulement une incertitude, et une incertitude ne devrait jamais se résoudre par un nouvel appel identique sans protection contre son propre rejeu.
En résumé
Un agent qui rejoue un appel après un délai réseau applique une logique parfaitement raisonnable en apparence ; le risque réel se situe dans l’outil appelé, qui doit être capable de reconnaître un rejeu et de renvoyer le résultat déjà obtenu plutôt que de produire un second effet. Une clé d’idempotence, combinée à une séparation stricte entre création et mise à jour, reste la protection la plus fiable contre ce type d’incident.