'use strict'; // ==================== 文件 IO 并发护栏 ==================== // 背景(2026-08-11 压测结论): // 场景「无带宽限制 + 单 worker + 极端并发」(大量考生持续上传大文件 + // 管理后台同时持续下载大文件)下,单 worker 的 fd / 事件循环会被海量 // 并发文件流(分片写入、分片合并、Word 写入)压垮,触发 // MaxListenersExceededWarning 并最终雪崩。 // // 本护栏把「同时进行的文件 IO 操作数」限制在上限内: // - 未超限:立即放行,计数 +1; // - 已超限:进入有界队列等待(提供连接级背压,而非崩溃); // - 队列满 / 等待超时:抛出 busy 错误,调用方返回 429,由前端 // uploadWithRetry / 下载侧重试指数退避重试——保证后端存活。 // // 覆盖范围(同一全局配额,按进程单例共享): // - 上传:分片写入、分片合并、Word 写入(controller/upload.js、exam.js) // - 下载:管理后台作品/Word 下载(controller/admin.js 的 _sendFile) // 生产经 X-Accel-Redirect 卸载给 nginx,Node 仅发头、瞬间释放配额; // 开发回退 Node 流式时,配额随「流生命周期」(end/close/error)释放。 const LIMIT = parseInt(process.env.FILE_IO_CONCURRENCY || '20', 10); const MAX_QUEUE = parseInt(process.env.FILE_IO_MAX_QUEUE || '300', 10); const WAIT_TIMEOUT = parseInt(process.env.FILE_IO_WAIT_TIMEOUT || '8000', 10); let active = 0; const queue = []; function remove(entry) { const i = queue.indexOf(entry); if (i !== -1) queue.splice(i, 1); } /** * 获取一个文件 IO 配额。 * 成功(或排到队首被放行)resolve;队列满 / 超时则 reject({ busy: true })。 * @returns {Promise} */ function acquire() { return new Promise((resolve, reject) => { if (active < LIMIT) { active++; return resolve(); } if (queue.length >= MAX_QUEUE) { const err = new Error('FILE_IO_BUSY'); err.busy = true; return reject(err); } let timer = null; const entry = { run() { if (timer) clearTimeout(timer); active++; resolve(); }, cancel() { if (timer) clearTimeout(timer); const err = new Error('FILE_IO_BUSY'); err.busy = true; reject(err); }, }; timer = setTimeout(() => { remove(entry); entry.cancel(); }, WAIT_TIMEOUT); queue.push(entry); }); } /** * 释放一个文件 IO 配额(须在 finally 中调用)。 */ function release() { if (active > 0) active--; if (active < LIMIT && queue.length) { const entry = queue.shift(); entry.run(); } } module.exports = { acquire, release, LIMIT, MAX_QUEUE, WAIT_TIMEOUT };