feat: enhance error handling and retry logic for Discord bot connections
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "amayo",
|
"name": "amayo",
|
||||||
"version": "0.10.210",
|
"version": "0.11.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npx tsx watch src/main.ts",
|
"start": "npx tsx watch src/main.ts",
|
||||||
"dev": "npx tsx watch src/main.ts"
|
"dev": "npx tsx watch src/main.ts"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [ ],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
@@ -25,4 +25,4 @@
|
|||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"typescript": "5.9.2"
|
"typescript": "5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,8 @@ class Amayo extends Client {
|
|||||||
|
|
||||||
async play () {
|
async play () {
|
||||||
if(!this.key) {
|
if(!this.key) {
|
||||||
return console.error('No key provided');
|
console.error('No key provided');
|
||||||
|
throw new Error('Missing DISCORD TOKEN');
|
||||||
} else {
|
} else {
|
||||||
// Ejemplo de cómo usarías prisma antes de iniciar sesión
|
// Ejemplo de cómo usarías prisma antes de iniciar sesión
|
||||||
try {
|
try {
|
||||||
@@ -40,7 +41,8 @@ class Amayo extends Client {
|
|||||||
console.log('Successfully connected to the database.');
|
console.log('Successfully connected to the database.');
|
||||||
await this.login(this.key);
|
await this.login(this.key);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to connect to the database:', error);
|
console.error('Failed to connect to DB or login to Discord:', error);
|
||||||
|
throw error; // Propaga para que withRetry en main.ts reintente
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
154
src/main.ts
154
src/main.ts
@@ -1,28 +1,160 @@
|
|||||||
import Amayo from "./core/client";
|
import Amayo from "./core/client";
|
||||||
import { loadCommands } from "./core/loader";
|
import { loadCommands } from "./core/loader";
|
||||||
import { loadEvents } from "./core/loaderEvents";
|
import { loadEvents } from "./core/loaderEvents";
|
||||||
import { redisConnect } from "./core/redis";
|
import { redis, redisConnect } from "./core/redis";
|
||||||
import { registeringCommands } from "./core/api/discordAPI";
|
import { registeringCommands } from "./core/api/discordAPI";
|
||||||
import {loadComponents} from "./core/components";
|
import {loadComponents} from "./core/components";
|
||||||
|
|
||||||
export const bot = new Amayo();
|
export const bot = new Amayo();
|
||||||
|
|
||||||
|
// Listeners de robustez del cliente Discord
|
||||||
|
bot.on('error', (e) => console.error('🐞 Discord client error:', e));
|
||||||
|
bot.on('warn', (m) => console.warn('⚠️ Discord warn:', m));
|
||||||
|
// Cuando la sesión es invalidada, intentamos reconectar/login
|
||||||
|
bot.on('invalidated', () => {
|
||||||
|
console.error('🔄 Sesión de Discord invalidada. Reintentando login...');
|
||||||
|
withRetry('Re-login tras invalidated', () => bot.play(), { minDelayMs: 2000, maxDelayMs: 60_000 }).catch(() => {
|
||||||
|
console.error('No se pudo reloguear tras invalidated, se seguirá intentando en el bucle general.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utilidad: reintentos con backoff exponencial + jitter
|
||||||
|
async function withRetry<T>(name: string, fn: () => Promise<T>, opts?: {
|
||||||
|
retries?: number;
|
||||||
|
minDelayMs?: number;
|
||||||
|
maxDelayMs?: number;
|
||||||
|
factor?: number;
|
||||||
|
jitter?: boolean;
|
||||||
|
isRetryable?: (err: unknown, attempt: number) => boolean;
|
||||||
|
}): Promise<T> {
|
||||||
|
const {
|
||||||
|
retries = Infinity,
|
||||||
|
minDelayMs = 1000,
|
||||||
|
maxDelayMs = 30_000,
|
||||||
|
factor = 1.8,
|
||||||
|
jitter = true,
|
||||||
|
isRetryable = () => true,
|
||||||
|
} = opts ?? {};
|
||||||
|
|
||||||
|
let attempt = 0;
|
||||||
|
let delay = minDelayMs;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (err) {
|
||||||
|
attempt++;
|
||||||
|
const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
||||||
|
console.error(`❌ ${name} falló (intento ${attempt}) =>`, errMsg);
|
||||||
|
|
||||||
|
if (!isRetryable(err, attempt)) {
|
||||||
|
console.error(`⛔ ${name}: error no recuperable, deteniendo reintentos.`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt >= retries) throw err;
|
||||||
|
|
||||||
|
// calcular backoff
|
||||||
|
let wait = delay;
|
||||||
|
if (jitter) {
|
||||||
|
const rand = Math.random() + 0.5; // 0.5x a 1.5x
|
||||||
|
wait = Math.min(maxDelayMs, Math.floor(delay * rand));
|
||||||
|
} else {
|
||||||
|
wait = Math.min(maxDelayMs, delay);
|
||||||
|
}
|
||||||
|
console.warn(`⏳ Reintentando ${name} en ${wait}ms...`);
|
||||||
|
await new Promise((r) => setTimeout(r, wait));
|
||||||
|
delay = Math.min(maxDelayMs, Math.floor(delay * factor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handlers globales para robustez
|
||||||
|
process.on('unhandledRejection', (reason: any, p) => {
|
||||||
|
console.error('🚨 UnhandledRejection en Promise:', p, 'razón:', reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
console.error('🚨 UncaughtException:', err);
|
||||||
|
// No salimos; dejamos que el bot continúe vivo
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('multipleResolves', (type, promise, reason) => {
|
||||||
|
console.warn('⚠️ multipleResolves:', type, reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
let shuttingDown = false;
|
||||||
|
async function gracefulShutdown() {
|
||||||
|
if (shuttingDown) return;
|
||||||
|
shuttingDown = true;
|
||||||
|
console.log('🛑 Apagado controlado iniciado...');
|
||||||
|
try {
|
||||||
|
// Cerrar Redis si procede
|
||||||
|
try {
|
||||||
|
if (redis?.isOpen) {
|
||||||
|
await redis.quit();
|
||||||
|
console.log('🔌 Redis cerrado');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('No se pudo cerrar Redis limpiamente:', e);
|
||||||
|
}
|
||||||
|
// Cerrar Prisma y Discord
|
||||||
|
try {
|
||||||
|
await bot.prisma.$disconnect();
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
await bot.destroy();
|
||||||
|
} catch {}
|
||||||
|
} finally {
|
||||||
|
console.log('✅ Apagado controlado completo');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGINT', gracefulShutdown);
|
||||||
|
process.on('SIGTERM', gracefulShutdown);
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
console.log("🚀 Iniciando bot...");
|
console.log("🚀 Iniciando bot...");
|
||||||
|
|
||||||
loadCommands(); // 1️⃣ Cargar comandos en la Collection
|
// Cargar recursos locales (no deberían tirar el proceso si fallan)
|
||||||
loadComponents()
|
try { loadCommands(); } catch (e) { console.error('Error cargando comandos:', e); }
|
||||||
loadEvents(); // 2️⃣ Cargar eventos
|
try { loadComponents(); } catch (e) { console.error('Error cargando componentes:', e); }
|
||||||
|
try { loadEvents(); } catch (e) { console.error('Error cargando eventos:', e); }
|
||||||
|
|
||||||
await registeringCommands(); // 3️⃣ Registrar los slash en Discord
|
// Registrar comandos en segundo plano con reintentos; no bloquea el arranque del bot
|
||||||
|
withRetry('Registrar slash commands', async () => {
|
||||||
|
await registeringCommands();
|
||||||
|
}).catch((e) => console.error('Registro de comandos agotó reintentos:', e));
|
||||||
|
|
||||||
await redisConnect(); // 4️⃣ Conectar Redis
|
// Conectar Redis con reintentos
|
||||||
|
await withRetry('Conectar a Redis', async () => {
|
||||||
|
await redisConnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login Discord + DB con reintentos (gestionado en Amayo.play -> conecta Prisma + login)
|
||||||
|
await withRetry('Login de Discord', async () => {
|
||||||
|
await bot.play();
|
||||||
|
}, {
|
||||||
|
isRetryable: (err) => {
|
||||||
|
const msg = err instanceof Error ? `${err.message}` : String(err);
|
||||||
|
// Si falta el TOKEN o token inválido, no tiene sentido reintentar sin cambiar config
|
||||||
|
return !/missing discord token|invalid token/i.test(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await bot.play();
|
|
||||||
console.log("✅ Bot conectado a Discord");
|
console.log("✅ Bot conectado a Discord");
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap().catch((err) => {
|
// Bucle de arranque resiliente: si bootstrap completo falla, reintenta sin matar el proceso
|
||||||
console.error("❌ Error en el arranque:", err);
|
(async function startLoop() {
|
||||||
process.exit(1);
|
await withRetry('Arranque', bootstrap, {
|
||||||
});
|
minDelayMs: 1000,
|
||||||
|
maxDelayMs: 60_000,
|
||||||
|
isRetryable: (err) => {
|
||||||
|
const msg = err instanceof Error ? `${err.message}` : String(err);
|
||||||
|
// No reintentar en bucle si el problema es falta/invalid token
|
||||||
|
return !/missing discord token|invalid token/i.test(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2024",
|
"target": "ES2024",
|
||||||
"module": "ESNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "nodenext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user