Tengo una aplicación NodeJS que es como un administrador de tareas. Cada segundo (usando un temporizador rxjs), obtiene las tareas de la base de datos que están programadas. Cada tarea tiene un nombre de archivo y ese archivo se carga con el archivo required Cada archivo debe tener un método run() que ejecute la tarea. Una vez completada la tarea, el archivo requerido se elimina de require.cache . La aplicación falla después de un par de días porque el montón está lleno. Verifiqué con node --inspect y vi que los archivos requeridos se mantienen en las cadenas. ¿Alguien puede explicarme por qué los archivos requeridos se mantienen como cadenas en el montón, mientras que el archivo se elimina de require.cache ? Y mejor aún, si alguien me puede explicar cómo se puede resolver, ¡sería aún mejor!
A continuación se muestra una versión abreviada de mi código
async test() { timer(0, 1000).pipe( exhaustMap(async () => { return await this.executeTasks(); }) ).subscribe(); } async executeTasks(): Promise<void> { // get all queue items const tasks = await this.getTasks(); // for each item for (const task of tasks) { // execute item await this.executeTask(task); } } async executeTask(item: QueueItem) { try { const Callback: typeof AutomationCallback = require(item.callbackFile); const callback = new Callback(); await callback.run(); } catch (error) { if (error instanceof Error && (error as any).code === 'MODULE_NOT_FOUND') { log_message('Failed executing', item.callback, item.id, `Cannot find file ${item.callbackFile}`); } else if (error instanceof RescheduleError) { log_message('Failed executing', item.callback, item.id, error.message); } else { log_message('Failed executing', item.callback, item.id, (error as any).toString()); } } finally { // remove require from cache; try { delete require.cache[require.resolve(item.callbackFile)]; } catch (error) { } } } A continuación se muestra una captura de pantalla de una instantánea, el archivo resaltado se agrega al montón cada vez que el archivo se carga con require . Como puede ver en el lado izquierdo, el montón se llenará lentamente, el tiempo entre las instantáneas 1 y 2 es probablemente de 5 minutos. 
Si todo está previamente definido, ¿por qué no tenerlos requeridos en la parte superior y llamarlos condicionalmente?
Ejemplo:
class TaskRunnerBase { async run() { this.doRun(); } async doRun() {} // implemented by specific runners } class TaskRunnerA extends TaskRunnerBase { async doRun() {...} // custom implementation for TaskRunnerA } class TaskRunnerB extends TaskRunnerBase { async doRun() {...} // custom implementation for TaskRunnerB } class DefaultRunner extends TaskRunnerBase { async doRun() { console.log('Default Runner'); } }Administrador de tareas
const TaskRunnerA = require('./task-runners/task-runner-a'); const TaskRunnerB = require('./task-runners/task-runner-b'); // more runners const DefaultRunner = require('./task-runners/default-runner'); ... ... async executeTask(item: QueueItem) { try { const Callback: typeof AutomationCallback = this.getRunnerByType(item.callbackFile); const callback = new Callback(); await callback.run(); } catch (error) { ... } finally { ... } } getRunnerByType(type) { if(type === 'RunnerA') { // "RunnerA" for example return TaskRunnerA; } else if(type === 'RunnerB') { // "RunnerB" for example return TaskRunnerB; } .... else { return DefaultRunner; // If unknown type is passed, let's keep going, with DefaultRunner. } }Este tipo de enfoque es simple, manejable y obvio a largo plazo.
require.cache no es el único lugar donde se almacenan en caché los módulos cargados. Hay algunos más según el tipo de módulo y cómo se carga, y Node.js ni siquiera expone algunos de estos objetos, lo que significa que no podría eliminarlos si quisiera.
Un ejemplo bien documentado es module.children , una matriz de módulos cargados. Si observa la longitud de esta matriz, verá que se hace cada vez más grande después de cada require , porque efectivamente está cargando los mismos módulos una y otra vez, en lugar de reutilizar los objetos almacenados en caché:
const Callback: typeof AutomationCallback = require(item.callbackFile); console.log('Loaded modules:', module.children.length); // <- const callback = new Callback(); await callback.run(); Por supuesto, podría borrar la lista de niños (que de todos modos no se usa como caché) junto con el caché require :
// No guarantee that this will help. delete require.cache[require.resolve(item.callbackFile)]; module.children.pop();Incluso escribí mi propia herramienta para hacer algo así de una manera que no afecte al resto del programa.
Hacerlo puede ayudar o no y, de hecho, podría ser incluso contraproducente si el único objetivo es evitar fugas de memoria, asignando recursos para cargar un módulo cada vez que se requiere el módulo.
Su mejor opción probablemente sea deshacerse del bloque " finally " y mantener el require.cache sin cambios. De esta forma, cada módulo se cargará como máximo una vez.