Commit inicial con archivos existentes
This commit is contained in:
374
src/CommandLocker.php
Executable file
374
src/CommandLocker.php
Executable file
@@ -0,0 +1,374 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
16
src/DiscordSender.php
Executable file
16
src/DiscordSender.php
Executable file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* ARCHIVO BRIDGE - Migración Progresiva
|
||||
*
|
||||
* Este archivo mantiene compatibilidad con código existente
|
||||
* mientras redirige a la nueva ubicación de la clase.
|
||||
*
|
||||
* Nueva ubicación: /discord/DiscordSender.php
|
||||
* Fecha de migración: 2025-11-25
|
||||
*/
|
||||
|
||||
// Cargar el archivo desde la nueva ubicación
|
||||
require_once __DIR__ . '/../discord/DiscordSender.php';
|
||||
|
||||
// Este archivo puede ser eliminado cuando toda la migración esté completa
|
||||
// y todas las referencias apunten directamente a discord/DiscordSender.php
|
||||
16
src/HtmlToDiscordMarkdownConverter.php
Executable file
16
src/HtmlToDiscordMarkdownConverter.php
Executable file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* ARCHIVO BRIDGE - Migración Progresiva
|
||||
*
|
||||
* Este archivo mantiene compatibilidad con código existente
|
||||
* mientras redirige a la nueva ubicación de la clase.
|
||||
*
|
||||
* Nueva ubicación: /discord/converters/HtmlToDiscordMarkdownConverter.php
|
||||
* Fecha de migración: 2025-11-25
|
||||
*/
|
||||
|
||||
// Cargar el archivo desde la nueva ubicación
|
||||
require_once __DIR__ . '/../discord/converters/HtmlToDiscordMarkdownConverter.php';
|
||||
|
||||
// Este archivo puede ser eliminado cuando toda la migración esté completa
|
||||
// y todas las referencias apunten directamente a discord/converters/HtmlToDiscordMarkdownConverter.php
|
||||
16
src/HtmlToTelegramHtmlConverter.php
Executable file
16
src/HtmlToTelegramHtmlConverter.php
Executable file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* ARCHIVO BRIDGE - Migración Progresiva
|
||||
*
|
||||
* Este archivo mantiene compatibilidad con código existente
|
||||
* mientras redirige a la nueva ubicación de la clase.
|
||||
*
|
||||
* Nueva ubicación: /telegram/converters/HtmlToTelegramHtmlConverter.php
|
||||
* Fecha de migración: 2025-11-25
|
||||
*/
|
||||
|
||||
// Cargar el archivo desde la nueva ubicación
|
||||
require_once __DIR__ . '/../telegram/converters/HtmlToTelegramHtmlConverter.php';
|
||||
|
||||
// Este archivo puede ser eliminado cuando toda la migración esté completa
|
||||
// y todas las referencias apunten directamente a telegram/converters/HtmlToTelegramHtmlConverter.php
|
||||
16
src/TelegramSender.php
Executable file
16
src/TelegramSender.php
Executable file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* ARCHIVO BRIDGE - Migración Progresiva
|
||||
*
|
||||
* Este archivo mantiene compatibilidad con código existente
|
||||
* mientras redirige a la nueva ubicación de la clase.
|
||||
*
|
||||
* Nueva ubicación: /telegram/TelegramSender.php
|
||||
* Fecha de migración: 2025-11-25
|
||||
*/
|
||||
|
||||
// Cargar el archivo desde la nueva ubicación
|
||||
require_once __DIR__ . '/../telegram/TelegramSender.php';
|
||||
|
||||
// Este archivo puede ser eliminado cuando toda la migración esté completa
|
||||
// y todas las referencias apunten directamente a telegram/TelegramSender.php
|
||||
172
src/Translate.php
Executable file
172
src/Translate.php
Executable file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
class Translate
|
||||
{
|
||||
private $apiUrl;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiUrl = rtrim($_ENV['LIBRETRANSLATE_URL'], '/');
|
||||
}
|
||||
|
||||
public function detectLanguage($text)
|
||||
{
|
||||
if (empty(trim($text))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$response = $this->request('/detect', ['q' => $text]);
|
||||
|
||||
if (isset($response[0]['language'])) {
|
||||
return $response[0]['language'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function translateText($text, $source, $target)
|
||||
{
|
||||
if (empty(trim($text))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$response = $this->request('/translate', [
|
||||
'q' => $text,
|
||||
'source' => $source,
|
||||
'target' => $target,
|
||||
'format' => 'text'
|
||||
]);
|
||||
|
||||
return $response['translatedText'] ?? null;
|
||||
}
|
||||
|
||||
public function translateHtml($html, $source, $target)
|
||||
{
|
||||
if (empty(trim($html))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$maxLength = 4000; // Límite de caracteres para LibreTranslate
|
||||
$translatedHtml = '';
|
||||
|
||||
custom_log("translateHtml: Original HTML length: " . mb_strlen($html));
|
||||
// Dividir el HTML en bloques para evitar exceder el límite de LibreTranslate
|
||||
$chunks = $this->splitHtmlIntoChunks($html, $maxLength);
|
||||
custom_log("translateHtml: Number of chunks: " . count($chunks));
|
||||
|
||||
foreach ($chunks as $index => $chunk) {
|
||||
custom_log("translateHtml: Processing chunk " . ($index + 1) . "/" . count($chunks) . ", length: " . mb_strlen($chunk));
|
||||
$response = $this->request('/translate', [
|
||||
'q' => $chunk,
|
||||
'source' => $source,
|
||||
'target' => $target,
|
||||
'format' => 'html'
|
||||
]);
|
||||
|
||||
$translatedChunk = $response['translatedText'] ?? null;
|
||||
if ($translatedChunk) {
|
||||
$translatedHtml .= $translatedChunk;
|
||||
custom_log("translateHtml: Chunk " . ($index + 1) . " translated successfully.");
|
||||
} else {
|
||||
// Si una parte falla, devolver la parte original para no perder contenido
|
||||
$translatedHtml .= $chunk;
|
||||
custom_log("translateHtml: Chunk " . ($index + 1) . " failed to translate. Appending original chunk.");
|
||||
}
|
||||
}
|
||||
|
||||
return $translatedHtml;
|
||||
}
|
||||
|
||||
private function splitHtmlIntoChunks($html, $maxLength)
|
||||
{
|
||||
$chunks = [];
|
||||
$currentChunk = '';
|
||||
$dom = new DOMDocument();
|
||||
// Suprimir errores de HTML mal formado
|
||||
@$dom->loadHTML('<div>' . $html . '</div>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
$xpath = new DOMXPath($dom);
|
||||
|
||||
// Query all direct children of the body (or the implied div)
|
||||
$nodes = $xpath->query('//body/*');
|
||||
if ($nodes->length === 0) {
|
||||
// If no block-level elements, treat the whole HTML as one chunk
|
||||
$nodes = $xpath->query('//body/text()');
|
||||
if ($nodes->length === 0) {
|
||||
// Fallback if no text nodes either, just return the original HTML as one chunk
|
||||
return [$html];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
$nodeHtml = $dom->saveHTML($node);
|
||||
custom_log("splitHtmlIntoChunks: Processing node, length: " . mb_strlen($nodeHtml) . ", HTML: " . substr($nodeHtml, 0, 100) . "...");
|
||||
|
||||
if (mb_strlen($currentChunk . $nodeHtml) <= $maxLength) {
|
||||
$currentChunk .= $nodeHtml;
|
||||
} else {
|
||||
if (!empty($currentChunk)) {
|
||||
$chunks[] = $currentChunk;
|
||||
custom_log("splitHtmlIntoChunks: Added chunk, length: " . mb_strlen($currentChunk));
|
||||
}
|
||||
$currentChunk = $nodeHtml;
|
||||
}
|
||||
}
|
||||
if (!empty($currentChunk)) {
|
||||
$chunks[] = $currentChunk;
|
||||
custom_log("splitHtmlIntoChunks: Added final chunk, length: " . mb_strlen($currentChunk));
|
||||
}
|
||||
return $chunks;
|
||||
}
|
||||
|
||||
public function translateTextBatch($texts, $source, $target)
|
||||
{
|
||||
if (empty($texts)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$response = $this->request('/translate', [
|
||||
'q' => $texts,
|
||||
'source' => $source,
|
||||
'target' => $target,
|
||||
'format' => 'text'
|
||||
]);
|
||||
|
||||
return $response['translatedText'] ?? null;
|
||||
}
|
||||
|
||||
public function getSupportedLanguages()
|
||||
{
|
||||
$response = $this->request('/languages', [], 'GET');
|
||||
return $response ?? [];
|
||||
}
|
||||
|
||||
private function request($endpoint, $data, $method = 'POST')
|
||||
{
|
||||
$url = $this->apiUrl . $endpoint;
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Considera la seguridad de esto
|
||||
|
||||
if ($method === 'POST') {
|
||||
custom_log("LibreTranslate POST Request: URL=" . $url . " | Data=" . json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
} else {
|
||||
custom_log("LibreTranslate GET Request: URL=" . $url);
|
||||
// Para GET, los datos (si los hubiera) se añadirían a la URL, pero /languages no requiere
|
||||
}
|
||||
|
||||
$response_body = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
custom_log("LibreTranslate Response: HTTP Code=" . $http_code . " | Body=" . $response_body);
|
||||
|
||||
if ($http_code >= 400) {
|
||||
return null; // Devolver null si hay un error de cliente o servidor
|
||||
}
|
||||
|
||||
return json_decode($response_body, true);
|
||||
}
|
||||
}
|
||||
223
src/TranslationCache.php
Executable file
223
src/TranslationCache.php
Executable file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
// Cargar logger si está disponible
|
||||
if (file_exists(__DIR__ . '/../includes/logger.php')) {
|
||||
require_once __DIR__ . '/../includes/logger.php';
|
||||
}
|
||||
|
||||
// Función fallback si custom_log no está definida
|
||||
if (!function_exists('custom_log')) {
|
||||
function custom_log($message) {
|
||||
error_log($message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TranslationCache {
|
||||
private $redis;
|
||||
private $ttl = 604800; // 7 días por defecto
|
||||
private $enabled = true;
|
||||
private $prefix = 'trans:';
|
||||
|
||||
public function __construct() {
|
||||
// Verificar si el caché está habilitado
|
||||
$this->enabled = ($_ENV['TRANSLATION_CACHE_ENABLED'] ?? 'true') === 'true';
|
||||
|
||||
if (!$this->enabled) {
|
||||
custom_log("[CACHE] Caché de traducciones deshabilitado");
|
||||
return;
|
||||
}
|
||||
|
||||
// Configurar TTL desde environment
|
||||
if (isset($_ENV['TRANSLATION_CACHE_TTL'])) {
|
||||
$this->ttl = (int)$_ENV['TRANSLATION_CACHE_TTL'];
|
||||
}
|
||||
|
||||
try {
|
||||
// Intentar conectar a Redis
|
||||
$this->redis = new Redis();
|
||||
$host = $_ENV['REDIS_HOST'] ?? 'localhost';
|
||||
$port = (int)($_ENV['REDIS_PORT'] ?? 6379);
|
||||
$password = $_ENV['REDIS_PASSWORD'] ?? null;
|
||||
|
||||
$connected = $this->redis->connect($host, $port, 2.0); // 2 segundos timeout
|
||||
|
||||
if (!$connected) {
|
||||
throw new Exception("No se pudo conectar a Redis en {$host}:{$port}");
|
||||
}
|
||||
|
||||
// Autenticar si hay password
|
||||
if ($password) {
|
||||
$this->redis->auth($password);
|
||||
}
|
||||
|
||||
// Verificar conexión
|
||||
$this->redis->ping();
|
||||
custom_log("[CACHE] Conectado a Redis en {$host}:{$port}");
|
||||
|
||||
} catch (Exception $e) {
|
||||
custom_log("[CACHE] Error al conectar a Redis: " . $e->getMessage());
|
||||
custom_log("[CACHE] El sistema funcionará sin caché");
|
||||
$this->enabled = false;
|
||||
$this->redis = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener traducción del caché
|
||||
*/
|
||||
public function get($text, $source, $target) {
|
||||
if (!$this->enabled || !$this->redis) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$key = $this->generateKey($text, $source, $target);
|
||||
$cached = $this->redis->get($key);
|
||||
|
||||
if ($cached !== false) {
|
||||
$this->incrementHits();
|
||||
custom_log("[CACHE] HIT: {$source}->{$target} (key: " . substr($key, 0, 20) . "...)");
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$this->incrementMisses();
|
||||
custom_log("[CACHE] MISS: {$source}->{$target}");
|
||||
return false;
|
||||
|
||||
} catch (Exception $e) {
|
||||
custom_log("[CACHE] Error al obtener del caché: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardar traducción en el caché
|
||||
*/
|
||||
public function set($text, $source, $target, $translation) {
|
||||
if (!$this->enabled || !$this->redis || empty($translation)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$key = $this->generateKey($text, $source, $target);
|
||||
$result = $this->redis->setex($key, $this->ttl, $translation);
|
||||
|
||||
if ($result) {
|
||||
custom_log("[CACHE] Guardado: {$source}->{$target} (TTL: {$this->ttl}s)");
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (Exception $e) {
|
||||
custom_log("[CACHE] Error al guardar en caché: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar clave única para la traducción
|
||||
*/
|
||||
private function generateKey($text, $source, $target) {
|
||||
// Normalizar texto para mejor hit rate
|
||||
$normalized = trim(strtolower($text));
|
||||
$hash = md5($normalized . '|' . $source . '|' . $target);
|
||||
return $this->prefix . $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrementar contador de hits
|
||||
*/
|
||||
private function incrementHits() {
|
||||
if (!$this->enabled || !$this->redis) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->redis->incr($this->prefix . 'stats:hits');
|
||||
} catch (Exception $e) {
|
||||
// Ignorar errores en estadísticas
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrementar contador de misses
|
||||
*/
|
||||
private function incrementMisses() {
|
||||
if (!$this->enabled || !$this->redis) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->redis->incr($this->prefix . 'stats:misses');
|
||||
} catch (Exception $e) {
|
||||
// Ignorar errores en estadísticas
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener estadísticas del caché
|
||||
*/
|
||||
public function getStats() {
|
||||
if (!$this->enabled || !$this->redis) {
|
||||
return [
|
||||
'enabled' => false,
|
||||
'hits' => 0,
|
||||
'misses' => 0,
|
||||
'hit_rate' => 0
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$hits = (int)$this->redis->get($this->prefix . 'stats:hits') ?: 0;
|
||||
$misses = (int)$this->redis->get($this->prefix . 'stats:misses') ?: 0;
|
||||
$total = $hits + $misses;
|
||||
$hitRate = $total > 0 ? round(($hits / $total) * 100, 2) : 0;
|
||||
|
||||
return [
|
||||
'enabled' => true,
|
||||
'hits' => $hits,
|
||||
'misses' => $misses,
|
||||
'total_requests' => $total,
|
||||
'hit_rate' => $hitRate
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
custom_log("[CACHE] Error al obtener estadísticas: " . $e->getMessage());
|
||||
return [
|
||||
'enabled' => true,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpiar todo el caché de traducciones
|
||||
*/
|
||||
public function clear() {
|
||||
if (!$this->enabled || !$this->redis) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$keys = $this->redis->keys($this->prefix . '*');
|
||||
if (!empty($keys)) {
|
||||
$deleted = $this->redis->del($keys);
|
||||
custom_log("[CACHE] Limpiado: {$deleted} claves eliminadas");
|
||||
return $deleted;
|
||||
}
|
||||
return 0;
|
||||
|
||||
} catch (Exception $e) {
|
||||
custom_log("[CACHE] Error al limpiar caché: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si el caché está habilitado y funcionando
|
||||
*/
|
||||
public function isEnabled() {
|
||||
return $this->enabled && $this->redis !== null;
|
||||
}
|
||||
}
|
||||
236
src/TranslationWorker.php
Executable file
236
src/TranslationWorker.php
Executable file
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
class TranslationWorker {
|
||||
private $workerId;
|
||||
private $pdo;
|
||||
private $translator;
|
||||
private $running = true;
|
||||
private $sleepTime = 2; // segundos entre iteraciones
|
||||
|
||||
public function __construct($workerId, $pdo) {
|
||||
$this->workerId = "worker_" . $workerId . "_" . getmypid();
|
||||
$this->pdo = $pdo;
|
||||
$this->translator = new Translate();
|
||||
|
||||
// Configurar sleep time desde environment
|
||||
if (isset($_ENV['TRANSLATION_WORKER_SLEEP'])) {
|
||||
$this->sleepTime = (int)$_ENV['TRANSLATION_WORKER_SLEEP'];
|
||||
}
|
||||
|
||||
custom_log("[{$this->workerId}] Inicializado");
|
||||
}
|
||||
|
||||
public function run() {
|
||||
// Configurar manejadores de señales
|
||||
pcntl_async_signals(true);
|
||||
pcntl_signal(SIGINT, [$this, 'handleShutdown']);
|
||||
pcntl_signal(SIGTERM, [$this, 'handleShutdown']);
|
||||
|
||||
custom_log("[{$this->workerId}] Iniciando loop de procesamiento");
|
||||
|
||||
while ($this->running) {
|
||||
$job = null;
|
||||
|
||||
try {
|
||||
// Obtener trabajo de la cola
|
||||
$job = $this->getNextJob();
|
||||
|
||||
if ($job) {
|
||||
$this->processJob($job);
|
||||
} else {
|
||||
// No hay trabajos, esperar
|
||||
sleep($this->sleepTime);
|
||||
}
|
||||
|
||||
// Procesar señales
|
||||
pcntl_signal_dispatch();
|
||||
|
||||
} catch (Exception $e) {
|
||||
custom_log("[{$this->workerId}] Error: " . $e->getMessage());
|
||||
if ($job) {
|
||||
$this->markJobAsFailed($job['id'], $e->getMessage());
|
||||
}
|
||||
sleep($this->sleepTime);
|
||||
}
|
||||
}
|
||||
|
||||
custom_log("[{$this->workerId}] Detenido");
|
||||
}
|
||||
|
||||
|
||||
private function getNextJob() {
|
||||
try {
|
||||
// Verificar y reconectar si es necesario
|
||||
try {
|
||||
$this->pdo->query('SELECT 1');
|
||||
} catch (PDOException $e) {
|
||||
custom_log("[{$this->workerId}] Reconectando a BD...");
|
||||
// La conexión se perdió, será recreada por el pool
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Iniciar transacción
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
// Obtener trabajo con bloqueo (FOR UPDATE SKIP LOCKED)
|
||||
$stmt = $this->pdo->prepare("
|
||||
SELECT * FROM translation_queue
|
||||
WHERE status = 'pending'
|
||||
AND attempts < 5
|
||||
ORDER BY priority DESC, created_at ASC
|
||||
LIMIT 1
|
||||
FOR UPDATE SKIP LOCKED
|
||||
");
|
||||
$stmt->execute();
|
||||
$job = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($job) {
|
||||
// Marcar como procesando
|
||||
$updateStmt = $this->pdo->prepare("
|
||||
UPDATE translation_queue
|
||||
SET status = 'processing',
|
||||
attempts = attempts + 1,
|
||||
worker_id = ?,
|
||||
processing_started_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$updateStmt->execute([$this->workerId, $job['id']]);
|
||||
|
||||
custom_log("[{$this->workerId}] Job #{$job['id']} obtenido (intento #{$job['attempts']})");
|
||||
}
|
||||
|
||||
// Confirmar transacción
|
||||
$this->pdo->commit();
|
||||
|
||||
return $job;
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function processJob($job) {
|
||||
custom_log("[{$this->workerId}] Procesando job #{$job['id']}: {$job['source_lang']}->{$job['target_lang']}");
|
||||
|
||||
try {
|
||||
// Traducir texto
|
||||
$translatedText = $this->translator->translateHtml(
|
||||
$job['text_to_translate'],
|
||||
$job['source_lang'],
|
||||
$job['target_lang']
|
||||
);
|
||||
|
||||
// Verificar que la traducción sea válida
|
||||
if ($translatedText && trim(strtolower($translatedText)) !== trim(strtolower($job['text_to_translate']))) {
|
||||
// Enviar traducción
|
||||
$success = $this->sendTranslation($job, $translatedText);
|
||||
|
||||
if ($success) {
|
||||
$this->markJobAsCompleted($job['id']);
|
||||
custom_log("[{$this->workerId}] Job #{$job['id']} completado");
|
||||
} else {
|
||||
throw new Exception("Fallo al enviar traducción");
|
||||
}
|
||||
} else {
|
||||
// Traducción vacía o idéntica
|
||||
$this->markJobAsCompleted($job['id'], 'Traducción vacía o idéntica al original');
|
||||
custom_log("[{$this->workerId}] Job #{$job['id']} completado (sin envío)");
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function sendTranslation($job, $translatedText) {
|
||||
if ($job['platform'] === 'discord') {
|
||||
return $this->sendToDiscord($job, $translatedText);
|
||||
} else {
|
||||
return $this->sendToTelegram($job, $translatedText);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendToDiscord($job, $translatedText) {
|
||||
require_once __DIR__ . '/DiscordSender.php';
|
||||
|
||||
$discordSender = new DiscordSender($_ENV['DISCORD_BOT_TOKEN']);
|
||||
|
||||
// Obtener bandera del idioma
|
||||
$flag = $this->getLanguageFlag($job['target_lang']);
|
||||
|
||||
$replyContent = "*{$flag} Traducción:*\n> {$translatedText}";
|
||||
$response = $discordSender->sendRawMessage($job['chat_id'], $replyContent);
|
||||
|
||||
return $response !== null;
|
||||
}
|
||||
|
||||
private function sendToTelegram($job, $translatedText) {
|
||||
require_once __DIR__ . '/TelegramSender.php';
|
||||
|
||||
$telegram = new TelegramSender($_ENV['TELEGRAM_BOT_TOKEN'], $this->pdo);
|
||||
|
||||
$response = $telegram->sendMessage($job['chat_id'], $translatedText, [
|
||||
'parse_mode' => 'HTML',
|
||||
'reply_to_message_id' => $job['message_id']
|
||||
], false, $job['target_lang']);
|
||||
|
||||
return $response !== null;
|
||||
}
|
||||
|
||||
private function getLanguageFlag($langCode) {
|
||||
try {
|
||||
$stmt = $this->pdo->prepare("
|
||||
SELECT flag_emoji
|
||||
FROM supported_languages
|
||||
WHERE language_code = ? AND is_active = 1
|
||||
");
|
||||
$stmt->execute([$langCode]);
|
||||
$flag = $stmt->fetchColumn();
|
||||
|
||||
if ($flag) {
|
||||
return $flag;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Ignorar error
|
||||
}
|
||||
|
||||
// Fallback flags
|
||||
$fallbackFlags = [
|
||||
'es' => '🇲🇽', 'en' => '🇺🇸', 'pt' => '🇵🇹',
|
||||
'fr' => '🇫🇷', 'de' => '🇩🇪', 'it' => '🇮🇹'
|
||||
];
|
||||
|
||||
return $fallbackFlags[strtolower($langCode)] ?? '🏳️';
|
||||
}
|
||||
|
||||
private function markJobAsCompleted($jobId, $message = null) {
|
||||
$stmt = $this->pdo->prepare("
|
||||
UPDATE translation_queue
|
||||
SET status = 'completed',
|
||||
error_message = ?,
|
||||
processed_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$message, $jobId]);
|
||||
}
|
||||
|
||||
private function markJobAsFailed($jobId, $errorMessage) {
|
||||
$stmt = $this->pdo->prepare("
|
||||
UPDATE translation_queue
|
||||
SET status = 'failed',
|
||||
error_message = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$errorMessage, $jobId]);
|
||||
|
||||
custom_log("[{$this->workerId}] Job #{$jobId} marcado como fallido: {$errorMessage}");
|
||||
}
|
||||
|
||||
public function handleShutdown($signal) {
|
||||
custom_log("[{$this->workerId}] Señal {$signal} recibida, deteniendo...");
|
||||
$this->running = false;
|
||||
}
|
||||
}
|
||||
187
src/TranslationWorkerPool.php
Executable file
187
src/TranslationWorkerPool.php
Executable file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
class TranslationWorkerPool {
|
||||
private $workers = [];
|
||||
private $maxWorkers = 4;
|
||||
private $pdo;
|
||||
private $environment;
|
||||
private $running = true;
|
||||
|
||||
public function __construct($pdo, $maxWorkers = null) {
|
||||
$this->pdo = $pdo;
|
||||
|
||||
// Configurar número de workers desde environment
|
||||
if ($maxWorkers !== null) {
|
||||
$this->maxWorkers = $maxWorkers;
|
||||
} elseif (isset($_ENV['TRANSLATION_WORKERS'])) {
|
||||
$this->maxWorkers = (int)$_ENV['TRANSLATION_WORKERS'];
|
||||
}
|
||||
|
||||
// Capturar el environment del proceso padre
|
||||
$this->environment = getenv('APP_ENVIRONMENT') ?: 'pruebas';
|
||||
|
||||
custom_log("[WORKER_POOL] Inicializando con {$this->maxWorkers} workers");
|
||||
custom_log("[WORKER_POOL] Environment: {$this->environment}");
|
||||
}
|
||||
|
||||
public function start() {
|
||||
// Configurar manejadores de señales para el pool
|
||||
pcntl_async_signals(true);
|
||||
pcntl_signal(SIGINT, [$this, 'handleShutdown']);
|
||||
pcntl_signal(SIGTERM, [$this, 'handleShutdown']);
|
||||
|
||||
custom_log("[WORKER_POOL] Iniciando {$this->maxWorkers} workers...");
|
||||
|
||||
// Iniciar workers
|
||||
for ($i = 0; $i < $this->maxWorkers; $i++) {
|
||||
$this->spawnWorker($i);
|
||||
}
|
||||
|
||||
custom_log("[WORKER_POOL] Todos los workers iniciados");
|
||||
|
||||
// Supervisar workers
|
||||
$this->supervise();
|
||||
}
|
||||
|
||||
private function spawnWorker($id) {
|
||||
$pid = pcntl_fork();
|
||||
|
||||
if ($pid == -1) {
|
||||
// Error al hacer fork
|
||||
custom_log("[WORKER_POOL] ERROR: No se pudo crear worker {$id}");
|
||||
return false;
|
||||
} elseif ($pid == 0) {
|
||||
// Proceso hijo - ejecutar worker
|
||||
try {
|
||||
// Heredar environment del padre
|
||||
putenv('APP_ENVIRONMENT=' . $this->environment);
|
||||
|
||||
custom_log("[WORKER_{$id}] Iniciado con PID " . getmypid() . ", APP_ENVIRONMENT={$this->environment}");
|
||||
|
||||
// IMPORTANTE: Crear nueva conexión PDO para este worker
|
||||
// No usar la conexión del padre porque se cierra por inactividad
|
||||
$maxRetries = 3;
|
||||
$retryDelay = 2;
|
||||
$pdo = null;
|
||||
|
||||
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
|
||||
try {
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
// $pdo se crea en db.php
|
||||
if (isset($pdo) && $pdo !== null) {
|
||||
custom_log("[WORKER_{$id}] Conexión a BD establecida");
|
||||
break;
|
||||
}
|
||||
} catch (Exception $dbError) {
|
||||
custom_log("[WORKER_{$id}] Intento {$attempt}/{$maxRetries} de conexión a BD falló: " . $dbError->getMessage());
|
||||
if ($attempt < $maxRetries) {
|
||||
sleep($retryDelay);
|
||||
} else {
|
||||
throw new Exception("No se pudo conectar a la BD después de {$maxRetries} intentos");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cargar dependencias necesarias
|
||||
require_once __DIR__ . '/Translate.php';
|
||||
require_once __DIR__ . '/TranslationWorker.php';
|
||||
|
||||
// Crear y ejecutar worker con su propia conexión
|
||||
$worker = new TranslationWorker($id, $pdo);
|
||||
$worker->run();
|
||||
|
||||
} catch (Exception $e) {
|
||||
custom_log("[WORKER_{$id}] ERROR FATAL: " . $e->getMessage());
|
||||
custom_log("[WORKER_{$id}] Stack trace: " . $e->getTraceAsString());
|
||||
}
|
||||
|
||||
exit(0);
|
||||
} else {
|
||||
// Proceso padre - registrar worker
|
||||
$this->workers[$id] = [
|
||||
'pid' => $pid,
|
||||
'started' => time(),
|
||||
'restarts' => 0
|
||||
];
|
||||
|
||||
custom_log("[WORKER_POOL] Worker {$id} spawned con PID {$pid}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function supervise() {
|
||||
custom_log("[WORKER_POOL] Iniciando supervisión de workers");
|
||||
|
||||
while ($this->running) {
|
||||
// Verificar estado de cada worker
|
||||
foreach ($this->workers as $id => $info) {
|
||||
$status = pcntl_waitpid($info['pid'], $exitStatus, WNOHANG);
|
||||
|
||||
if ($status > 0) {
|
||||
// Worker terminó
|
||||
$exitCode = pcntl_wexitstatus($exitStatus);
|
||||
custom_log("[WORKER_POOL] Worker {$id} (PID {$info['pid']}) terminó con código {$exitCode}");
|
||||
|
||||
// Reiniciar worker si el pool sigue corriendo
|
||||
if ($this->running) {
|
||||
custom_log("[WORKER_POOL] Reiniciando worker {$id}...");
|
||||
$this->workers[$id]['restarts']++;
|
||||
|
||||
// Esperar un poco antes de reiniciar
|
||||
sleep(1);
|
||||
|
||||
$this->spawnWorker($id);
|
||||
}
|
||||
} elseif ($status < 0) {
|
||||
// Error al verificar estado
|
||||
custom_log("[WORKER_POOL] Error al verificar worker {$id}");
|
||||
}
|
||||
}
|
||||
|
||||
// Procesar señales
|
||||
pcntl_signal_dispatch();
|
||||
|
||||
// Esperar antes de la siguiente verificación
|
||||
sleep(5);
|
||||
}
|
||||
|
||||
custom_log("[WORKER_POOL] Supervisión detenida");
|
||||
}
|
||||
|
||||
public function handleShutdown($signal) {
|
||||
custom_log("[WORKER_POOL] Señal {$signal} recibida, deteniendo pool...");
|
||||
$this->running = false;
|
||||
|
||||
// Enviar señal de terminación a todos los workers
|
||||
foreach ($this->workers as $id => $info) {
|
||||
custom_log("[WORKER_POOL] Enviando SIGTERM a worker {$id} (PID {$info['pid']})");
|
||||
posix_kill($info['pid'], SIGTERM);
|
||||
}
|
||||
|
||||
// Esperar a que todos los workers terminen
|
||||
custom_log("[WORKER_POOL] Esperando a que los workers terminen...");
|
||||
foreach ($this->workers as $id => $info) {
|
||||
pcntl_waitpid($info['pid'], $status);
|
||||
custom_log("[WORKER_POOL] Worker {$id} terminado");
|
||||
}
|
||||
|
||||
custom_log("[WORKER_POOL] Pool detenido completamente");
|
||||
}
|
||||
|
||||
public function getStats() {
|
||||
$stats = [
|
||||
'total_workers' => $this->maxWorkers,
|
||||
'workers' => []
|
||||
];
|
||||
|
||||
foreach ($this->workers as $id => $info) {
|
||||
$stats['workers'][$id] = [
|
||||
'pid' => $info['pid'],
|
||||
'uptime' => time() - $info['started'],
|
||||
'restarts' => $info['restarts']
|
||||
];
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user