pdo = $pdo; // Configurar PDO para que lance excepciones $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // Inicializar la conexión a la base de datos $this->getConnection(); } /** * Obtiene una conexión activa a la base de datos * * @return PDO La conexión a la base de datos */ private function getConnection() { try { // Verificar si la conexión actual es válida if ($this->pdo === null || !$this->isConnectionAlive()) { $this->log("Estableciendo nueva conexión a la base de datos"); $this->pdo = $this->createNewConnection(); } return $this->pdo; } catch (Exception $e) { $this->log("Error al obtener conexión a la base de datos", [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw $e; } } /** * Verifica si la conexión actual está activa * * @return bool True si la conexión está activa, false en caso contrario */ private function isConnectionAlive() { try { $this->pdo->query('SELECT 1'); return true; } catch (Exception $e) { $this->log("La conexión a la base de datos no está activa", [ 'error' => $e->getMessage() ]); return false; } } /** * Crea una nueva conexión a la base de datos * * @return PDO La nueva conexión */ private function createNewConnection() { // Obtener la configuración de la base de datos del archivo de configuración require_once __DIR__ . '/../includes/db.php'; // Obtener la instancia de la conexión del archivo db.php global $pdo; if (!($pdo instanceof PDO)) { throw new Exception("No se pudo establecer una conexión a la base de datos"); } return $pdo; } private function log($message, $data = []) { if ($this->debug) { $logMessage = date('[Y-m-d H:i:s] ') . $message; if (!empty($data)) { $logMessage .= ' ' . json_encode($data); } error_log($logMessage); } } /** * Intenta adquirir un bloqueo para un comando * * @param string $command El comando a bloquear * @param int $chatId El ID del chat donde se ejecutó el comando * @param string $type Tipo de bloqueo ('command' o 'translation') * @param array $data Datos adicionales a almacenar con el bloqueo * @return array|false Retorna el ID del bloqueo si se adquiere, false si ya existe un bloqueo activo */ public function acquireLock($command, $chatId, $type = 'command', $data = []) { $this->log("Intentando adquirir bloqueo", [ 'command' => $command, 'chatId' => $chatId, 'type' => $type, 'data' => $data ]); $this->cleanupExpiredLocks(); try { $this->pdo->beginTransaction(); // Buscar cualquier bloqueo existente para este chat y comando (sin filtrar por estado) $query = " SELECT id, status, expires_at, data, created_at FROM command_locks WHERE command = ? AND chat_id = ? ORDER BY created_at DESC LIMIT 1 FOR UPDATE "; $stmt = $this->pdo->prepare($query); $this->log("Ejecutando consulta de bloqueo existente", [ 'query' => $stmt->queryString, 'params' => [$command, $chatId, $type] ]); $stmt->execute([$command, $chatId]); $existingLock = $stmt->fetch(PDO::FETCH_ASSOC); if ($existingLock) { $this->log("Bloqueo existente encontrado", $existingLock); // Si hay un bloqueo activo y reciente, bloquear nueva ejecución if ($existingLock['status'] === 'processing' && strtotime($existingLock['created_at']) > strtotime('-5 minutes')) { $this->log("Bloqueo ya en proceso, rechazando nueva solicitud"); $this->pdo->rollBack(); return false; // Ya hay un bloqueo activo } // Reutilizar el mismo registro para evitar violar la única (chat_id,command) $expiresAt = (new DateTime('+5 minutes'))->format('Y-m-d H:i:s'); $dataJson = !empty($data) ? json_encode($data) : null; $upd = $this->pdo->prepare("UPDATE command_locks SET type = ?, status='processing', data = COALESCE(?, data), message_id = NULL, expires_at = ?, updated_at = NOW() WHERE id = ?"); $upd->execute([$type, $dataJson, $expiresAt, $existingLock['id']]); $this->pdo->commit(); $this->log("Bloqueo reutilizado y pasado a processing", ['lockId' => $existingLock['id']]); return (int)$existingLock['id']; } else { $this->log("No se encontraron bloqueos existentes"); } // Insertar un nuevo bloqueo $expiresAt = (new DateTime('+5 minutes'))->format('Y-m-d H:i:s'); $dataJson = !empty($data) ? json_encode($data) : null; $query = " INSERT INTO command_locks (chat_id, command, type, data, status, expires_at, created_at, updated_at) VALUES (?, ?, ?, ?, 'processing', ?, NOW(), NOW()) "; $stmt = $this->pdo->prepare($query); $this->log("Insertando nuevo bloqueo", [ 'chat_id' => $chatId, 'command' => $command, 'type' => $type, 'has_data' => !empty($data), 'expires_at' => $expiresAt ]); $stmt->execute([$chatId, $command, $type, $dataJson, $expiresAt]); $lockId = $this->pdo->lastInsertId(); $this->pdo->commit(); $this->log("Bloqueo adquirido exitosamente", ['lockId' => $lockId]); return (int)$lockId; } catch (Exception $e) { $this->log("Error al adquirir bloqueo", [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); try { if ($this->pdo->inTransaction()) { $this->pdo->rollBack(); } } catch (Exception $rollbackException) { $this->log("Error al hacer rollback", [ 'error' => $rollbackException->getMessage() ]); } return false; } } /** * Actualiza el estado de un bloqueo * * @param int $lockId ID del bloqueo a actualizar * @param string $status Nuevo estado ('processing', 'completed', 'failed') * @param string|null $messageId ID del mensaje asociado (opcional) * @return bool True si se actualizó correctamente, false en caso contrario */ public function updateLockStatus($lockId, $status, $messageId = null) { $this->log("Actualizando estado de bloqueo", [ 'lockId' => $lockId, 'status' => $status, 'messageId' => $messageId ]); try { // Asegurarse de que tenemos una conexión válida $pdo = $this->getConnection(); $query = " UPDATE command_locks SET status = ?, message_id = COALESCE(?, message_id), expires_at = CASE WHEN ? = 'completed' THEN DATE_ADD(NOW(), INTERVAL 1 MINUTE) ELSE expires_at END, updated_at = NOW() WHERE id = ? "; $stmt = $pdo->prepare($query); $result = $stmt->execute([$status, $messageId, $status, $lockId]); $rowCount = $stmt->rowCount(); $this->log("Resultado de actualización de estado", [ 'lockId' => $lockId, 'status' => $status, 'rowCount' => $rowCount, 'result' => $result ]); // Si no se actualizó ninguna fila, verificar si el bloqueo existe if ($rowCount === 0) { $checkStmt = $pdo->prepare("SELECT id FROM command_locks WHERE id = ?"); $checkStmt->execute([$lockId]); $exists = $checkStmt->fetch(PDO::FETCH_ASSOC); $this->log("Verificación de existencia de bloqueo", [ 'lockId' => $lockId, 'exists' => (bool)$exists ]); if (!$exists) { $this->log("Error: El bloqueo no existe", ['lockId' => $lockId]); return false; } } return $result && $rowCount > 0; } catch (Exception $e) { $this->log("Error al actualizar estado de bloqueo", [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return false; } } /** * Libera un bloqueo marcándolo como completado * * @param int $lockId ID del bloqueo a liberar * @param string|null $messageId ID del mensaje asociado (opcional) * @return bool True si se actualizó correctamente, false en caso contrario */ public function releaseLock($lockId, $messageId = null) { $this->log("Liberando bloqueo", [ 'lockId' => $lockId, 'messageId' => $messageId ]); $result = $this->updateLockStatus($lockId, 'completed', $messageId); $this->log("Resultado de liberación de bloqueo", [ 'lockId' => $lockId, 'success' => $result ]); return $result; } /** * Marca un bloqueo como fallido * * @param int $lockId ID del bloqueo a marcar como fallido * @param string $errorMessage Mensaje de error (opcional) * @return bool True si se actualizó correctamente, false en caso contrario */ public function failLock($lockId, $errorMessage = '') { $this->log("Marcando bloqueo como fallido", [ 'lockId' => $lockId, 'errorMessage' => $errorMessage ]); try { // Asegurarse de que tenemos una conexión válida $pdo = $this->getConnection(); $query = " UPDATE command_locks SET status = 'failed', data = JSON_MERGE_PATCH(COALESCE(data, '{}'), ?), expires_at = DATE_ADD(NOW(), INTERVAL 5 MINUTE), updated_at = NOW() WHERE id = ? "; $stmt = $pdo->prepare($query); $result = $stmt->execute([json_encode(['error' => $errorMessage, 'failed_at' => date('Y-m-d H:i:s')]), $lockId]); $rowCount = $stmt->rowCount(); $this->log("Resultado de marcar bloqueo como fallido", [ 'lockId' => $lockId, 'rowCount' => $rowCount, 'result' => $result ]); return $result && $rowCount > 0; } catch (Exception $e) { $this->log("Error al marcar bloqueo como fallido", [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return false; } } /** * Limpia los bloqueos expirados */ private function cleanupExpiredLocks() { try { $this->pdo->exec(" DELETE FROM command_locks WHERE (status = 'completed' AND expires_at <= NOW()) OR (status = 'processing' AND created_at < DATE_SUB(NOW(), INTERVAL 1 HOUR)) "); } catch (Exception $e) { error_log("Error al limpiar bloqueos expirados: " . $e->getMessage()); } } /** * Verifica si un comando está siendo procesado actualmente * * @param string $command Comando a verificar * @param int $chatId ID del chat * @param string $type Tipo de bloqueo ('command' o 'translation') * @return bool True si el comando está siendo procesado, false en caso contrario */ public function isCommandProcessing($command, $chatId, $type = 'command') { $stmt = $this->pdo->prepare(" SELECT COUNT(*) FROM command_locks WHERE command = ? AND chat_id = ? AND type = ? AND status = 'processing' AND created_at > DATE_SUB(NOW(), INTERVAL 5 MINUTE) "); $stmt->execute([$command, $chatId, $type]); return (int)$stmt->fetchColumn() > 0; } }