Files
whatsapp/worker.php
T
2026-01-27 23:56:49 -05:00

292 lines
9.2 KiB
PHP
Executable File

#!/usr/bin/env php
<?php
/**
* Worker para procesar mensajes de WhatsApp de forma asíncrona
*
* Uso:
* php worker.php [queue_name] [--daemon]
*
* Ejemplos:
* php worker.php messages # Procesa cola 'messages' una vez
* php worker.php --daemon # Daemon que escucha todas las colas
* php worker.php media --daemon # Daemon solo para cola 'media'
*
* Para ejecutar en producción con supervisor:
* supervisorctl start whatsapp-worker:*
*/
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/config/config.php';
use WhatsApp\Queue\RedisQueue;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\LineFormatter;
// Configuración
$queueName = $argv[1] ?? null;
$isDaemon = in_array('--daemon', $argv) || in_array('-d', $argv);
$queues = ['messages', 'media', 'notifications']; // Colas a escuchar
// Si se especifica una cola, solo escuchar esa
if ($queueName && $queueName !== '--daemon' && $queueName !== '-d') {
$queues = [$queueName];
}
// Configurar Monolog
$logger = new Logger('worker');
$logPath = __DIR__ . '/logs/worker.log';
// Handler para archivo rotativo (mantener últimos 7 días)
$handler = new RotatingFileHandler($logPath, 7, Logger::DEBUG);
$formatter = new LineFormatter(
"[%datetime%] %channel%.%level_name%: %message% %context%\n",
"Y-m-d H:i:s",
true,
true
);
$handler->setFormatter($formatter);
$logger->pushHandler($handler);
// Handler para STDOUT en modo no-daemon
if (!$isDaemon) {
$consoleHandler = new StreamHandler('php://stdout', Logger::INFO);
$consoleHandler->setFormatter($formatter);
$logger->pushHandler($consoleHandler);
}
// Inicializar servicios
$queue = new RedisQueue(null, $logger);
$db = Database::getInstance();
$whatsappService = new WhatsAppService();
$botService = new BotService();
$logger->info("Worker started", [
'queues' => $queues,
'daemon' => $isDaemon,
'pid' => getmypid()
]);
// Manejadores de señales para shutdown graceful
$shutdown = false;
pcntl_async_signals(true);
pcntl_signal(SIGTERM, function() use (&$shutdown, $logger) {
$logger->info("Received SIGTERM, shutting down gracefully...");
$shutdown = true;
});
pcntl_signal(SIGINT, function() use (&$shutdown, $logger) {
$logger->info("Received SIGINT, shutting down gracefully...");
$shutdown = true;
});
/**
* Procesar un mensaje de la cola
*/
function processMessage($message, $queue, $botService, $whatsappService, $db, $logger) {
$data = $message['data'];
$attempts = $message['attempts'];
try {
$logger->debug("Processing message", [
'queue' => $queue,
'attempts' => $attempts,
'data' => array_keys($data)
]);
switch ($queue) {
case 'messages':
// Procesar mensaje de texto/interactivo
if (!isset($data['user']) || !isset($data['messageText'])) {
throw new Exception("Invalid message data: missing user or messageText");
}
$user = $data['user'];
$messageText = $data['messageText'];
$messageType = $data['messageType'] ?? 'text';
// Procesar con BotService
$botService->processMessage($user, $messageText, $messageType);
$logger->info("Message processed successfully", [
'user_id' => $user['id'],
'phone' => $user['phone_number'],
'type' => $messageType
]);
break;
case 'media':
// Procesar descarga de media
if (!isset($data['media_id']) && !isset($data['media_url'])) {
throw new Exception("Invalid media data: missing media_id or media_url");
}
$conversationId = $data['conversation_id'] ?? null;
$mediaId = $data['media_id'] ?? null;
$mediaUrl = $data['media_url'] ?? null;
$subdir = $data['subdir'] ?? date('Y/m');
$mediaService = new MediaService();
if ($mediaId) {
$result = $mediaService->fetchAndStoreFromGraph($mediaId, $subdir);
} else {
$result = $mediaService->fetchAndStoreFromUrl($mediaUrl, $subdir);
}
// Actualizar conversación con archivo local
if ($result && $conversationId) {
$update = [];
if (!empty($result['local_file'])) {
$update['local_file'] = $result['local_file'];
}
if (!empty($result['local_thumb'])) {
$update['local_thumb'] = $result['local_thumb'];
}
if (!empty($update)) {
$db->update('conversations', $update, 'id = :id', ['id' => $conversationId]);
}
}
$logger->info("Media processed successfully", [
'conversation_id' => $conversationId,
'media_id' => $mediaId,
'local_file' => $result['local_file'] ?? null
]);
break;
case 'notifications':
// Procesar notificación (enviar push, email, etc.)
if (!isset($data['type']) || !isset($data['message'])) {
throw new Exception("Invalid notification data");
}
$type = $data['type'];
$notificationMessage = $data['message'];
$userId = $data['user_id'] ?? null;
// Aquí puedes integrar servicios de notificación
// Por ahora solo lo registramos
$logger->info("Notification processed", [
'type' => $type,
'user_id' => $userId,
'message' => substr($notificationMessage, 0, 100)
]);
break;
default:
$logger->warning("Unknown queue type", ['queue' => $queue]);
}
return true;
} catch (Exception $e) {
$logger->error("Failed to process message", [
'queue' => $queue,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'attempts' => $attempts
]);
return false;
}
}
/**
* Loop principal del worker
*/
$processedCount = 0;
$errorCount = 0;
$maxErrors = 10; // Detener si hay muchos errores consecutivos
do {
try {
// Procesar mensajes delayed cada 10 iteraciones
if ($processedCount % 10 === 0) {
foreach ($queues as $q) {
$queue->processDelayed($q);
}
}
// Esperar mensaje de cualquier cola (timeout 5 segundos)
$message = $queue->pop($queues, 5);
if (!$message) {
// No hay mensajes, continuar esperando
if (!$isDaemon) {
// En modo no-daemon, salir si no hay más mensajes
$logger->info("No more messages, exiting");
break;
}
continue;
}
// Procesar mensaje
$success = processMessage(
$message,
$message['queue'],
$botService,
$whatsappService,
$db,
$logger
);
if ($success) {
$processedCount++;
$errorCount = 0; // Reset error counter
} else {
$errorCount++;
// Re-encolar con retry
if (!$queue->retry($message['queue'], $message, 3)) {
$logger->critical("Failed to retry message", [
'queue' => $message['queue']
]);
}
// Si hay muchos errores consecutivos, detener worker
if ($errorCount >= $maxErrors) {
$logger->critical("Too many consecutive errors, stopping worker", [
'error_count' => $errorCount
]);
break;
}
}
// Limitar memoria (reiniciar si supera 128MB)
$memoryUsage = memory_get_usage(true) / 1024 / 1024;
if ($memoryUsage > 128) {
$logger->warning("Memory limit reached, restarting worker", [
'memory_mb' => round($memoryUsage, 2)
]);
break;
}
} catch (Exception $e) {
$logger->error("Worker error", [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
$errorCount++;
if ($errorCount >= $maxErrors) {
break;
}
// Esperar un poco antes de reintentar
sleep(5);
}
} while ($isDaemon && !$shutdown);
// Cleanup
$queue->disconnect();
$logger->info("Worker stopped", [
'processed' => $processedCount,
'errors' => $errorCount
]);
exit(0);