feat: implement Prisma singleton for memory efficiency and add memory monitoring options

This commit is contained in:
2025-09-23 22:38:06 -05:00
parent a33f033a64
commit 494617c4f6
7 changed files with 124 additions and 21 deletions

View File

@@ -1,15 +1,15 @@
// @ts-ignore
import { Client, GatewayIntentBits } from 'discord.js';
// 1. Importa PrismaClient
import { Client, GatewayIntentBits, Options, Partials } from 'discord.js';
// 1. Importa PrismaClient (singleton)
// @ts-ignore
import { PrismaClient } from '@prisma/client';
import { prisma, ensurePrismaConnection } from './prisma';
process.loadEnvFile();
class Amayo extends Client {
public key: string;
// 2. Declara la propiedad prisma
public prisma: PrismaClient;
// 2. Propiedad prisma apuntando al singleton
public prisma = prisma;
constructor() {
super({
@@ -17,17 +17,37 @@ class Amayo extends Client {
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMessageTyping
GatewayIntentBits.MessageContent
// 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: {
retries: 10
retries: 5 // bajar un poco para evitar colas largas en memoria
}
});
this.key = process.env.TOKEN ?? '';
// 3. Instancia PrismaClient en el constructor
this.prisma = new PrismaClient();
}
async play () {
@@ -35,10 +55,9 @@ class Amayo extends Client {
console.error('No key provided');
throw new Error('Missing DISCORD TOKEN');
} else {
// Ejemplo de cómo usarías prisma antes de iniciar sesión
try {
await this.prisma.$connect();
console.log('Successfully connected to the database.');
await ensurePrismaConnection();
console.log('Successfully connected to the database (singleton).');
await this.login(this.key);
} catch (error) {
console.error('Failed to connect to DB or login to Discord:', error);

50
src/core/memoryMonitor.ts Normal file
View 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
View 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;
}