diff --git a/backend/services/download-executor.js b/backend/services/download-executor.js index 6c144a4..041bdad 100644 --- a/backend/services/download-executor.js +++ b/backend/services/download-executor.js @@ -1,6 +1,7 @@ const path = require('path'); const fs = require('fs-extra'); const { defaultLogger } = require('../utils/logger'); +const abortControllerManager = require('../utils/abort-controller-manager'); // 创建logger实例 const logger = defaultLogger.child('DownloadExecutor'); @@ -17,18 +18,24 @@ class DownloadExecutor { this.historyManager = historyManager; this.downloadService = downloadService; // 添加下载服务引用 - // 存储每个任务的中断控制器 - this.abortControllers = new Map(); + // 存储每个任务的中断控制器ID(使用管理器) + this.taskControllerIds = new Set(); } /** * 执行单个作品下载 */ async executeArtworkDownload(task, images, size, artworkDir, artwork) { + const controllerId = `task_${task.id}`; + try { - // 为这个任务创建中断控制器 - const abortController = new AbortController(); - this.abortControllers.set(task.id, abortController); + // 使用管理器创建中断控制器 + const abortController = abortControllerManager.createController(controllerId); + if (!abortController) { + throw new Error('无法创建 AbortController,可能已达到数量限制'); + } + + this.taskControllerIds.add(controllerId); const results = []; @@ -41,7 +48,8 @@ class DownloadExecutor { if (this.shouldPause(task.id)) { logger.info('任务已暂停,停止下载:', task.id); // 中断当前下载 - abortController.abort(); + abortControllerManager.abortAndCleanup(controllerId); + this.taskControllerIds.delete(controllerId); // 确保任务状态为暂停 task.status = 'paused'; await this.taskManager.saveTasks(); @@ -242,8 +250,9 @@ class DownloadExecutor { await this.taskManager.saveTasks(); this.progressManager.notifyProgressUpdate(task.id, task); } finally { - // 清理中断控制器 - this.abortControllers.delete(task.id); + // 确保清理中断控制器 + abortControllerManager.abortAndCleanup(controllerId); + this.taskControllerIds.delete(controllerId); } } @@ -629,11 +638,14 @@ class DownloadExecutor { * @param {string} taskId - 任务ID */ abortTask(taskId) { - const abortController = this.abortControllers.get(taskId); - if (abortController) { + const controllerId = `task_${taskId}`; + const success = abortControllerManager.abortAndCleanup(controllerId); + + if (success) { + this.taskControllerIds.delete(controllerId); logger.info('中断任务下载', { taskId }); - abortController.abort(); - this.abortControllers.delete(taskId); + } else { + logger.debug('未找到要中断的任务控制器', { taskId }); } } diff --git a/backend/services/file-manager.js b/backend/services/file-manager.js index cff3a58..a0d60e8 100644 --- a/backend/services/file-manager.js +++ b/backend/services/file-manager.js @@ -316,6 +316,7 @@ class FileManager { return new Promise((resolve, reject) => { let isResolved = false; + let abortListener = null; const cleanup = () => { if (writer && !writer.destroyed) { @@ -324,11 +325,16 @@ class FileManager { if (response && response.data && !response.data.destroyed) { response.data.destroy(); } + // 清理 AbortSignal 监听器 + if (abortController && abortListener) { + abortController.signal.removeEventListener('abort', abortListener); + abortListener = null; + } }; // 监听中断信号 if (abortController) { - abortController.signal.addEventListener('abort', () => { + abortListener = () => { if (isResolved) return; isResolved = true; @@ -341,7 +347,8 @@ class FileManager { }); reject(new Error('下载被中断')); - }); + }; + abortController.signal.addEventListener('abort', abortListener); } writer.on('finish', async () => { @@ -404,11 +411,21 @@ class FileManager { reject(timeoutError); }, downloadConfig.timeout + 60000); // 动态超时 + 1分钟缓冲 - // 清理超时定时器 - writer.on('finish', () => clearTimeout(timeout)); - writer.on('error', () => clearTimeout(timeout)); - if (abortController) { - abortController.signal.addEventListener('abort', () => clearTimeout(timeout)); + // 清理超时定时器和监听器 + const clearTimeoutAndCleanup = () => { + clearTimeout(timeout); + cleanup(); + }; + + writer.on('finish', clearTimeoutAndCleanup); + writer.on('error', clearTimeoutAndCleanup); + if (abortListener) { + // 超时时也要清理监听器 + const originalTimeout = timeout._onTimeout; + timeout._onTimeout = () => { + cleanup(); + originalTimeout(); + }; } }); diff --git a/backend/utils/abort-controller-manager.js b/backend/utils/abort-controller-manager.js new file mode 100644 index 0000000..148f3a8 --- /dev/null +++ b/backend/utils/abort-controller-manager.js @@ -0,0 +1,229 @@ +const { defaultLogger } = require('./logger'); + +// 创建logger实例 +const logger = defaultLogger.child('AbortControllerManager'); + +/** + * AbortController 管理器 - 防止内存泄漏 + */ +class AbortControllerManager { + constructor() { + // 全局 AbortController 跟踪 + this.controllers = new Map(); + // 监听器数量限制 + this.maxListenersPerController = 10; + this.maxTotalControllers = 50; + + // 定期清理间隔(5分钟) + this.cleanupInterval = 5 * 60 * 1000; + this.cleanupTimer = null; + + this.startPeriodicCleanup(); + } + + /** + * 创建新的 AbortController + */ + createController(id) { + // 检查全局控制器数量限制 + if (this.controllers.size >= this.maxTotalControllers) { + logger.warn(`AbortController 数量已达上限 (${this.maxTotalControllers}),拒绝创建新控制器`); + return null; + } + + const controller = new AbortController(); + const controllerInfo = { + id, + controller, + createdAt: Date.now(), + listenerCount: 0 + }; + + this.controllers.set(id, controllerInfo); + logger.debug(`创建 AbortController: ${id},当前总数: ${this.controllers.size}`); + + return controller; + } + + /** + * 获取 AbortController + */ + getController(id) { + const info = this.controllers.get(id); + return info ? info.controller : null; + } + + /** + * 添加监听器(带限制检查) + */ + addListener(id, event, listener) { + const info = this.controllers.get(id); + if (!info) { + logger.warn(`未找到 AbortController: ${id}`); + return false; + } + + if (info.listenerCount >= this.maxListenersPerController) { + logger.warn(`AbortController ${id} 的监听器数量已达上限 (${this.maxListenersPerController})`); + return false; + } + + info.controller.signal.addEventListener(event, listener); + info.listenerCount++; + logger.debug(`为 AbortController ${id} 添加监听器,当前数量: ${info.listenerCount}`); + + return true; + } + + /** + * 移除监听器 + */ + removeListener(id, event, listener) { + const info = this.controllers.get(id); + if (!info) { + return false; + } + + info.controller.signal.removeEventListener(event, listener); + info.listenerCount = Math.max(0, info.listenerCount - 1); + logger.debug(`从 AbortController ${id} 移除监听器,剩余数量: ${info.listenerCount}`); + + return true; + } + + /** + * 中断并清理 AbortController + */ + abortAndCleanup(id) { + const info = this.controllers.get(id); + if (!info) { + return false; + } + + try { + if (!info.controller.signal.aborted) { + info.controller.abort(); + } + } catch (error) { + logger.warn(`中断 AbortController ${id} 时出错:`, error.message); + } finally { + this.controllers.delete(id); + logger.debug(`清理 AbortController: ${id},剩余总数: ${this.controllers.size}`); + } + + return true; + } + + /** + * 清理所有控制器 + */ + cleanupAll() { + logger.info(`清理所有 AbortController,当前数量: ${this.controllers.size}`); + + for (const [id, info] of this.controllers) { + try { + if (!info.controller.signal.aborted) { + info.controller.abort(); + } + } catch (error) { + logger.warn(`清理 AbortController ${id} 时出错:`, error.message); + } + } + + this.controllers.clear(); + } + + /** + * 获取统计信息 + */ + getStats() { + const totalControllers = this.controllers.size; + let totalListeners = 0; + let oldestController = null; + let oldestAge = 0; + + for (const [id, info] of this.controllers) { + totalListeners += info.listenerCount; + const age = Date.now() - info.createdAt; + if (age > oldestAge) { + oldestAge = age; + oldestController = id; + } + } + + return { + totalControllers, + totalListeners, + oldestController, + oldestAge: Math.round(oldestAge / 1000), // 秒 + maxTotalControllers: this.maxTotalControllers, + maxListenersPerController: this.maxListenersPerController + }; + } + + /** + * 开始定期清理检查 + */ + startPeriodicCleanup() { + if (this.cleanupTimer) { + return; + } + + this.cleanupTimer = setInterval(() => { + const stats = this.getStats(); + + // 记录统计信息 + if (stats.totalControllers > 0) { + logger.debug('AbortController 统计:', { + 总控制器数: stats.totalControllers, + 总监听器数: stats.totalListeners, + 最老控制器: stats.oldestController, + 最老年龄: `${stats.oldestAge}秒` + }); + + // 警告检查 + if (stats.totalControllers > stats.maxTotalControllers * 0.8) { + logger.warn(`AbortController 数量接近上限: ${stats.totalControllers}/${stats.maxTotalControllers}`); + } + + if (stats.totalListeners > stats.maxTotalControllers * stats.maxListenersPerController * 0.8) { + logger.warn(`AbortController 监听器总数过多: ${stats.totalListeners}`); + } + + // 清理超过 30 分钟的控制器 + const maxAge = 30 * 60 * 1000; // 30分钟 + const now = Date.now(); + const toCleanup = []; + + for (const [id, info] of this.controllers) { + if (now - info.createdAt > maxAge) { + toCleanup.push(id); + } + } + + if (toCleanup.length > 0) { + logger.info(`清理 ${toCleanup.length} 个超时的 AbortController`); + toCleanup.forEach(id => this.abortAndCleanup(id)); + } + } + }, this.cleanupInterval); + + logger.info('AbortController 定期清理已启动'); + } + + /** + * 停止定期清理 + */ + stopPeriodicCleanup() { + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = null; + logger.info('AbortController 定期清理已停止'); + } + } +} + +// 创建全局实例 +const abortControllerManager = new AbortControllerManager(); + +module.exports = abortControllerManager; \ No newline at end of file