375 lines
14 KiB
PHP
Executable File
375 lines
14 KiB
PHP
Executable File
<?php
|
|
|
|
class CommandLocker {
|
|
private $pdo;
|
|
private $lockTimeout = 300; // 5 minutos de tiempo de espera para el bloqueo
|
|
private $debug = true;
|
|
private $dbConnection;
|
|
|
|
public function __construct(PDO $pdo) {
|
|
$this->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;
|
|
}
|
|
}
|