From 9311ed3693f199281f89eda0f07064ef6dfb96f6 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Thu, 30 Apr 2026 12:04:59 +0200 Subject: [PATCH 1/2] fix(test): eliminate Winston uncaughtException listener leak in jest test runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each jest.resetModules() call in beforeEach re-imports lib/services/logger.js, creating a new Winston logger with handleExceptions:true. Winston registers a new process.uncaughtException listener on every import but never removes orphaned listeners from prior module registry generations. Across 1226 tests with resetModules in beforeEach, this leaked ~570 listeners and ~285-1100MB of heap. The billing PRs (#3535-#3539) amplified an existing pre-billing leak (162 instances) by +409 new instances (+252%) — pushing trawl_node past its 6144MB cap. Fix: guard handleExceptions with `process.env.NODE_ENV !== 'test'` on both the Console transport (logger creation) and the File transport (setupFileLogger / getLogOptions). Exception handling is preserved in production and development. Also add `forceExit: true` to jest.config.js to prevent Jest from hanging on open MongoDB handles from integration test afterAll disconnect races. Verified: MaxListenersExceededWarning fully eliminated. 99 suites / 1226 tests all pass. --- jest.config.js | 4 ++++ lib/services/logger.js | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/jest.config.js b/jest.config.js index e1602813d..75b84aaf7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -162,6 +162,10 @@ export default { // The test environment that will be used for testing testEnvironment: 'node', + // Force Jest to exit after all tests complete — prevents hanging on open MongoDB + // handles from integration tests whose afterAll disconnect races with Jest's exit + forceExit: true, + // Global timeout for tests and hooks (integration tests bootstrap MongoDB + Express) testTimeout: 15000, diff --git a/lib/services/logger.js b/lib/services/logger.js index 49bd92049..476ca2f22 100644 --- a/lib/services/logger.js +++ b/lib/services/logger.js @@ -28,7 +28,11 @@ const logger = new winston.createLogger({ new winston.transports.Console({ level: logLevel, format: consoleFormat, - handleExceptions: true, + // Disable exception handling in test env — each jest.resetModules() re-imports + // this module, and handleExceptions:true registers a new process.uncaughtException + // listener on every import without ever removing the old one. Across 1226 tests + // with resetModules in beforeEach, this leaks ~570 listeners + ~500MB+ of heap. + handleExceptions: process.env.NODE_ENV !== 'test', }), ], exitOnError: false, @@ -71,7 +75,8 @@ const getLogOptions = () => { eol: '\n', tailable: true, showLevel: true, - handleExceptions: true, + // Same guard as the Console transport — see comment at top of createLogger call + handleExceptions: process.env.NODE_ENV !== 'test', humanReadableUnhandledException: true, }; }; From cad417a94bf80e5b85efb00f4c1cd79547b0a03a Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Thu, 30 Apr 2026 12:17:56 +0200 Subject: [PATCH 2/2] docs(logger): replace volatile metrics with stable explanation in handleExceptions guard --- lib/services/logger.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/logger.js b/lib/services/logger.js index 476ca2f22..817e54fa1 100644 --- a/lib/services/logger.js +++ b/lib/services/logger.js @@ -30,8 +30,8 @@ const logger = new winston.createLogger({ format: consoleFormat, // Disable exception handling in test env — each jest.resetModules() re-imports // this module, and handleExceptions:true registers a new process.uncaughtException - // listener on every import without ever removing the old one. Across 1226 tests - // with resetModules in beforeEach, this leaks ~570 listeners + ~500MB+ of heap. + // listener on every import without ever removing the old one, causing listener + // accumulation and heap growth across module reloads in beforeEach hooks. handleExceptions: process.env.NODE_ENV !== 'test', }), ],