Files
sistema_para_juego/Sistema_discord/CommandLocker.php

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;
}
}