From abfab22c35f2b615129869f96f778862f1a397f9 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:33:31 -0500 Subject: [PATCH] fix: advisory lock por usuario en processMessage para eliminar race condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET_LOCK('bot_user_ID', 10) serializa el procesamiento por usuario - Solo un proceso a la vez puede ejecutar processMessage para el mismo user - Recarga datos frescos del usuario tras adquirir el lock - El lock se libera siempre en el bloque finally, incluso si hay excepciones - Resuelve definitivamente el doble envío de bienvenida + términos --- services/BotService.php | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/services/BotService.php b/services/BotService.php index b2b6291..70572bf 100644 --- a/services/BotService.php +++ b/services/BotService.php @@ -21,9 +21,29 @@ class BotService { if ($messageType !== 'text') { return; } - + $messageText = trim($messageText); $phoneNumber = $user['phone_number']; + $userId = $user['id'] ?? null; + + // ── Advisory lock por usuario ───────────────────────────────────────── + // Serializar el procesamiento para el mismo usuario y evitar race conditions + // (WhatsApp puede entregar el mismo webhook dos veces en paralelo). + $lockAcquired = false; + if ($userId) { + $lockKey = 'bot_user_' . $userId; + $lockRow = $this->db->fetch("SELECT GET_LOCK(:k, 10) as ok", ['k' => $lockKey]); + $lockAcquired = !empty($lockRow['ok']); + if (!$lockAcquired) { + error_log("[BotService] processMessage - lock timeout para user_id={$userId}, ignorando llamada concurrente"); + return; + } + // Recargar datos frescos del usuario después de adquirir el lock + $fresh = $this->db->fetch("SELECT * FROM users WHERE id = :id", ['id' => $userId]); + if ($fresh) $user = $fresh; + } + + try { // Comprobar en la base de datos el estado más reciente de 'in_service' para evitar condiciones de carrera try { @@ -393,6 +413,13 @@ class BotService { // ignore } } + + } finally { + // Liberar advisory lock si fue adquirido + if ($lockAcquired && isset($lockKey)) { + try { $this->db->query("DO RELEASE_LOCK(:k)", ['k' => $lockKey]); } catch (Exception $e) {} + } + } } /**