fix: advisory lock por usuario en processMessage para eliminar race condition

- 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
This commit is contained in:
Lizandro Guarnizo
2026-03-13 19:33:31 -05:00
parent bcfaf62293
commit abfab22c35
+28 -1
View File
@@ -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) {}
}
}
}
/**