feat: implement Prisma singleton for memory efficiency and add memory monitoring options
This commit is contained in:
@@ -5,7 +5,11 @@
|
|||||||
"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",
|
||||||
|
"dev:light": "CACHE_MESSAGES_LIMIT=25 CACHE_MEMBERS_LIMIT=50 SWEEP_MESSAGES_LIFETIME_SECONDS=600 SWEEP_MESSAGES_INTERVAL_SECONDS=240 npx tsx watch --clear-screen=false src/main.ts",
|
||||||
|
"dev:mem": "MEMORY_LOG_INTERVAL_SECONDS=120 npx tsx watch src/main.ts",
|
||||||
|
"start:prod": "NODE_ENV=production NODE_OPTIONS=--max-old-space-size=384 npx tsx src/main.ts",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"keywords": [ ],
|
"keywords": [ ],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { Client, GatewayIntentBits } from 'discord.js';
|
import { Client, GatewayIntentBits, Options, Partials } from 'discord.js';
|
||||||
// 1. Importa PrismaClient
|
// 1. Importa PrismaClient (singleton)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { prisma, ensurePrismaConnection } from './prisma';
|
||||||
|
|
||||||
process.loadEnvFile();
|
process.loadEnvFile();
|
||||||
|
|
||||||
class Amayo extends Client {
|
class Amayo extends Client {
|
||||||
public key: string;
|
public key: string;
|
||||||
// 2. Declara la propiedad prisma
|
// 2. Propiedad prisma apuntando al singleton
|
||||||
public prisma: PrismaClient;
|
public prisma = prisma;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
@@ -17,17 +17,37 @@ class Amayo extends Client {
|
|||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
GatewayIntentBits.GuildMembers,
|
GatewayIntentBits.GuildMembers,
|
||||||
GatewayIntentBits.GuildMessages,
|
GatewayIntentBits.GuildMessages,
|
||||||
GatewayIntentBits.MessageContent,
|
GatewayIntentBits.MessageContent
|
||||||
GatewayIntentBits.GuildMessageTyping
|
// Eliminado GuildMessageTyping para reducir tráfico/memoria si no se usa
|
||||||
],
|
],
|
||||||
|
partials: [Partials.Channel, Partials.Message], // Permite recibir eventos sin cachear todo
|
||||||
|
makeCache: Options.cacheWithLimits({
|
||||||
|
// Limitar el tamaño de los managers más pesados
|
||||||
|
MessageManager: parseInt(process.env.CACHE_MESSAGES_LIMIT || '50', 10),
|
||||||
|
GuildMemberManager: parseInt(process.env.CACHE_MEMBERS_LIMIT || '100', 10),
|
||||||
|
ThreadManager: 10,
|
||||||
|
ReactionManager: 0,
|
||||||
|
GuildInviteManager: 0,
|
||||||
|
StageInstanceManager: 0,
|
||||||
|
PresenceManager: 0
|
||||||
|
}),
|
||||||
|
sweepers: {
|
||||||
|
messages: {
|
||||||
|
// Cada 5 min barrer mensajes más antiguos que 15 min (ajustable por env)
|
||||||
|
interval: parseInt(process.env.SWEEP_MESSAGES_INTERVAL_SECONDS || '300', 10),
|
||||||
|
lifetime: parseInt(process.env.SWEEP_MESSAGES_LIFETIME_SECONDS || '900', 10)
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
interval: 60 * 30, // cada 30 minutos
|
||||||
|
filter: () => (user) => user.bot && user.id !== this.user?.id
|
||||||
|
}
|
||||||
|
},
|
||||||
rest: {
|
rest: {
|
||||||
retries: 10
|
retries: 5 // bajar un poco para evitar colas largas en memoria
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.key = process.env.TOKEN ?? '';
|
this.key = process.env.TOKEN ?? '';
|
||||||
// 3. Instancia PrismaClient en el constructor
|
|
||||||
this.prisma = new PrismaClient();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async play () {
|
async play () {
|
||||||
@@ -35,10 +55,9 @@ class Amayo extends Client {
|
|||||||
console.error('No key provided');
|
console.error('No key provided');
|
||||||
throw new Error('Missing DISCORD TOKEN');
|
throw new Error('Missing DISCORD TOKEN');
|
||||||
} else {
|
} else {
|
||||||
// Ejemplo de cómo usarías prisma antes de iniciar sesión
|
|
||||||
try {
|
try {
|
||||||
await this.prisma.$connect();
|
await ensurePrismaConnection();
|
||||||
console.log('Successfully connected to the database.');
|
console.log('Successfully connected to the database (singleton).');
|
||||||
await this.login(this.key);
|
await this.login(this.key);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to connect to DB or login to Discord:', error);
|
console.error('Failed to connect to DB or login to Discord:', error);
|
||||||
|
|||||||
50
src/core/memoryMonitor.ts
Normal file
50
src/core/memoryMonitor.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Monitor ligero de memoria y event loop.
|
||||||
|
// Se activa si defines MEMORY_LOG_INTERVAL_SECONDS.
|
||||||
|
import { monitorEventLoopDelay } from 'node:perf_hooks';
|
||||||
|
|
||||||
|
export interface MemoryMonitorOptions {
|
||||||
|
intervalSeconds: number;
|
||||||
|
warnHeapRatio?: number; // 0-1 fracción del máximo estimado antes de warning
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startMemoryMonitor(opts: MemoryMonitorOptions) {
|
||||||
|
const { intervalSeconds, warnHeapRatio = 0.8 } = opts;
|
||||||
|
if (intervalSeconds <= 0) return;
|
||||||
|
|
||||||
|
const h = monitorEventLoopDelay({ resolution: 20 });
|
||||||
|
h.enable();
|
||||||
|
|
||||||
|
const formatMB = (n: number) => (n / 1024 / 1024).toFixed(1) + 'MB';
|
||||||
|
|
||||||
|
// Intento de obtener el límite de heap (puede variar según flags)
|
||||||
|
let heapLimit = 0;
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
const v8 = require('v8');
|
||||||
|
const stats = v8.getHeapStatistics();
|
||||||
|
heapLimit = stats.heap_size_limit || 0;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
const m = process.memoryUsage();
|
||||||
|
const rss = formatMB(m.rss);
|
||||||
|
const heapUsed = formatMB(m.heapUsed);
|
||||||
|
const heapTotal = formatMB(m.heapTotal);
|
||||||
|
const external = formatMB(m.external);
|
||||||
|
const elDelay = h.mean / 1e6; // ms
|
||||||
|
|
||||||
|
let warn = '';
|
||||||
|
if (heapLimit) {
|
||||||
|
const ratio = m.heapUsed / heapLimit;
|
||||||
|
if (ratio >= warnHeapRatio) {
|
||||||
|
warn = ` ⚠ heap ${(ratio * 100).toFixed(1)}% del límite (~${formatMB(heapLimit)})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[MEM] rss=${rss} heapUsed=${heapUsed} heapTotal=${heapTotal} ext=${external} evLoopDelay=${elDelay.toFixed(2)}ms${warn}`);
|
||||||
|
|
||||||
|
// Resetear métricas de event loop delay para la siguiente ventana
|
||||||
|
h.reset();
|
||||||
|
}, intervalSeconds * 1000).unref();
|
||||||
|
}
|
||||||
|
|
||||||
24
src/core/prisma.ts
Normal file
24
src/core/prisma.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Prisma singleton para evitar múltiples instancias (especialmente en modo watch)
|
||||||
|
// y reducir consumo de memoria.
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
// Contenedor global seguro para hot-reload / watch sin duplicar instancias
|
||||||
|
const globalForPrisma = globalThis as unknown as { __amayo_prisma?: PrismaClient };
|
||||||
|
|
||||||
|
export const prisma: PrismaClient = globalForPrisma.__amayo_prisma ?? new PrismaClient({
|
||||||
|
log: process.env.PRISMA_LOG_QUERIES === '1' ? ['query', 'error', 'warn'] : ['error', 'warn']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
globalForPrisma.__amayo_prisma = prisma;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensurePrismaConnection() {
|
||||||
|
// Evita múltiples $connect si ya está conectada (no hay API directa, usamos heurística)
|
||||||
|
try {
|
||||||
|
await prisma.$queryRaw`SELECT 1`;
|
||||||
|
} catch {
|
||||||
|
await prisma.$connect();
|
||||||
|
}
|
||||||
|
return prisma;
|
||||||
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
Message
|
Message
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
// Se agrega ts
|
// Reemplaza instancia local -> usa singleton
|
||||||
//@ts-ignore
|
import { prisma } from "../../core/prisma";
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import { replaceVars } from "../../core/lib/vars";
|
import { replaceVars } from "../../core/lib/vars";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// Regex para detectar URLs válidas (corregido)
|
// Regex para detectar URLs válidas (corregido)
|
||||||
const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)/gi;
|
const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)/gi;
|
||||||
@@ -389,8 +387,8 @@ async function convertConfigToDisplayComponent(config: any, user: any, guild: an
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Función helper para validar URLs
|
// Función helper para validar URLs
|
||||||
function isValidUrl(url: string): boolean {
|
function isValidUrl(url: unknown): url is string {
|
||||||
if (!url || typeof url !== 'string') return false;
|
if (typeof url !== 'string' || !url) return false;
|
||||||
try {
|
try {
|
||||||
new URL(url);
|
new URL(url);
|
||||||
return url.startsWith('http://') || url.startsWith('https://');
|
return url.startsWith('http://') || url.startsWith('https://');
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ import { loadEvents } from "./core/loaderEvents";
|
|||||||
import { redis, 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";
|
||||||
|
import { startMemoryMonitor } from "./core/memoryMonitor"; // añadido
|
||||||
|
|
||||||
|
// Activar monitor de memoria si se define la variable
|
||||||
|
const __memInt = parseInt(process.env.MEMORY_LOG_INTERVAL_SECONDS || '0', 10);
|
||||||
|
if (__memInt > 0) {
|
||||||
|
startMemoryMonitor({ intervalSeconds: __memInt });
|
||||||
|
}
|
||||||
|
|
||||||
export const bot = new Amayo();
|
export const bot = new Amayo();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user