Files
competition-website/app/lib/file_io_guard.js
WorkBuddy a303cf2d53 chore: 后端首次基线提交
运行必须源码:app/(controller/model/service/middleware/lib/utils/validate/router)、config/、database/(migrations/seeders/sql)、启动脚本、app/public/templates 模板。
已忽略运行非必须:node_modules/、logs/、run/、app/public 运行时上传数据、.env*、构建/缓存产物。
2026-08-17 12:25:42 +08:00

84 lines
2.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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<void>}
*/
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 };