From 2aff6013d7d1c81e8a6ae6d03d91835b024e56d7 Mon Sep 17 00:00:00 2001 From: aka686 Date: Thu, 26 Feb 2026 23:38:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=BA=20Logger.requestContext=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20TTL=20=E6=B8=85=E7=90=86=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=E9=98=B2=E6=AD=A2=E5=86=85=E5=AD=98=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 长时间运行的服务中,如果 clearRequestContext 未被正确调用(如请求异常中断), requestContext Map 会持续增长导致内存泄漏。 新增: - 每个上下文条目记录 _createdAt 时间戳 - 每 60 秒定期扫描并清除超过 5 分钟的过期条目 - Map 为空时自动停止定时器,避免不必要的开销 - close() 时清理定时器 --- src/utils/logger.js | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/utils/logger.js b/src/utils/logger.js index ead4b29..3603455 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -22,6 +22,8 @@ class Logger { this.logStream = null; this.currentRequestId = null; // 当前请求ID this.requestContext = new Map(); // 存储请求上下文 + this.contextTTL = 5 * 60 * 1000; // 请求上下文 TTL:5 分钟 + this._contextCleanupTimer = null; this.levels = { debug: 0, info: 1, @@ -87,7 +89,8 @@ class Logger { requestId = randomUUID().substring(0, 8); } this.currentRequestId = requestId; - this.requestContext.set(requestId, context); + this.requestContext.set(requestId, { ...context, _createdAt: Date.now() }); + this._ensureContextCleanup(); return requestId; } @@ -120,6 +123,36 @@ class Logger { this.currentRequestId = null; } + /** + * 启动定期清理过期请求上下文的定时器(防止内存泄漏) + * 每 60 秒扫描一次,清除超过 contextTTL 的条目 + */ + _ensureContextCleanup() { + if (this._contextCleanupTimer) return; + this._contextCleanupTimer = setInterval(() => { + const now = Date.now(); + let cleaned = 0; + for (const [id, ctx] of this.requestContext) { + if (now - (ctx._createdAt || 0) > this.contextTTL) { + this.requestContext.delete(id); + cleaned++; + } + } + if (cleaned > 0) { + this.log('warn', [`[Logger] Cleaned ${cleaned} stale request context(s) (TTL: ${this.contextTTL}ms)`]); + } + // 当 Map 为空时停止定时器 + if (this.requestContext.size === 0) { + clearInterval(this._contextCleanupTimer); + this._contextCleanupTimer = null; + } + }, 60_000); + // 不阻止进程退出 + if (this._contextCleanupTimer.unref) { + this._contextCleanupTimer.unref(); + } + } + /** * 格式化日志消息 * @param {string} level - 日志级别 @@ -310,6 +343,10 @@ class Logger { * 关闭日志流 */ close() { + if (this._contextCleanupTimer) { + clearInterval(this._contextCleanupTimer); + this._contextCleanupTimer = null; + } if (this.logStream && !this.logStream.destroyed) { this.logStream.end(); this.logStream = null;