Files
AIclinicalresearch/docs/09-架构实施/分布式Fan-out指南破壁级审查报告.md
HaHafeng 85fda830c2 feat(ssa): Complete Phase V-A editable analysis plan variables
Features:
- Add editable variable selection in workflow plan (SingleVarSelect + MultiVarTags)
- Implement 3-layer flexible interception (warning bar + icon + blocking dialog)
- Add tool_param_constraints.json for 12 statistical tools parameter validation
- Add PATCH /workflow/:id/params API with Zod structural validation
- Implement synchronous parameter sync before execution (Promise chaining)
- Fix LLM hallucination by strict system prompt constraints
- Fix DynamicReport object-based rows compatibility (R baseline_table)
- Fix Word export row.map error with same normalization logic
- Restore inferGroupingVar for smart default variable selection
- Add ReactMarkdown rendering in SSAChatPane
- Update SSA module status document to v3.5

Modified files:
- backend: workflow.routes, ChatHandlerService, SystemPromptService, FlowTemplateService
- frontend: WorkflowTimeline, SSAWorkspacePane, DynamicReport, SSAChatPane, ssaStore, ssa.css
- config: tool_param_constraints.json (new)
- docs: SSA status doc, team review reports

Tested: Cohort study end-to-end execution + report export verified
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 13:08:29 +08:00

6.6 KiB
Raw Blame History

🔬 分布式 Fan-out 指南 (v1.2) 破壁级审查与修订报告

审查人: 资深架构师 & Node.js 底层专家

审查对象: 《分布式 Fan-out 任务模式开发指南 v1.2》

审查深度: V8 引擎内存栈、Postgres 底层驱动、pg-boss 命名空间

核心结论: 业务逻辑已彻底闭环!但在JSON 字符串强截断、长期闲置连接保活、全局唯一键防重这三个底层物理机制上,存在 3 个会直接导致进程崩溃或静默失效的高危隐患。

🚨 破壁级漏洞 1NOTIFY 强截断导致的 JSON.parse 爆栈崩溃

逐行审查发现的问题(位于 模式 6SSE 跨实例广播)

指南中为了防止超过 PostgreSQL 的 8000 bytes 限制,写了如下安全截断代码:

const payloadStr = JSON.stringify({ taskId, type: 'log', data: logEntry });
const safePayload = payloadStr.length > 7000
? payloadStr.substring(0, 7000) + '..."}' // 👈 致命漏洞
: payloadStr;

🔥 灾难时序爆发:

  1. 假设 logEntry 的内容包含大量报错栈,导致 payloadStr 长达 9000 字节。
  2. 代码直接在第 7000 个字符处一刀切,然后粗暴地拼上 ..."}。
  3. 如果第 7000 个字符刚好切在一个中文字符的中间(导致 Unicode 乱码),或者切在了 JSON 的某个 key 名字中间(如 {"ms拼接后的字符串将变成绝对非法的 JSON
  4. 爆炸点: 在 API 接收端代码是这样写的const { taskId, type, data } = JSON.parse(msg.payload);
  5. 当非法的 JSON 被 JSON.parse 解析时Node.js 会抛出 SyntaxError: Unexpected token。由于 pgClient.on('notification') 内部没有写 try-catch这个异常会直接击穿 Event Loop导致整个 API Pod 进程崩溃重启 (Crash)

架构师修正方案 (内部字段安全截断)

绝对不能对 JSON.stringify 后的字符串进行切片!必须切片原始对象内的长文本字段:

// 发送端:在 Stringify 之前,截断真正导致超长的 message 字段
if (logEntry.message && logEntry.message.length > 3000) {
logEntry.message = logEntry.message.substring(0, 3000) + '...[Truncated]';
}
const payloadStr = JSON.stringify({ taskId, type: 'log', data: logEntry });
await prisma.$executeRaw`SELECT pg_notify('sse_channel', ${payloadStr})`;

// 接收端:必须加上防御性 try-catch防止毒数据炸毁整个实例
pgClient.on('notification', (msg) => {
try {
const { taskId, type, data } = JSON.parse(msg.payload);
// ... 推送逻辑
} catch (error) {
logger.error('Failed to parse SSE notification payload', { payload: msg.payload });
}
});

🚨 破壁级漏洞 2LISTEN 监听器的“静默死亡” (Silent Connection Drop)

逐行审查发现的问题(位于 模式 6SSE 跨实例广播)

指南中的 API 接收端初始化代码如下:

const pgClient = new Client({ connectionString: DATABASE_URL });
await pgClient.connect();
await pgClient.query('LISTEN sse_channel');

🔥 灾难时序爆发:

  1. 这是一个一直挂在后台的长期长连接Long-lived Connection
  2. 在云端环境由于底层网络波动、PgBouncer 代理的闲置超时掐断、或者数据库主备切换,这根 TCP 连接一定会在几天内断开一次
  3. pg 原生库的设计是:Client 连接断开后,不会自动重连!
  4. 后果: 没有任何报错,服务依然在跑,但这个 Pod 永远也收不到任何 NOTIFY 消息了。前端 SSE 终端彻底变成一潭死水。

架构师修正方案 (加入心跳重连与错误监听)

必须为这个裸 Client 加上底层的生命周期守护:

// 封装为健壮的监听器启动函数
async function setupSSEListener() {
const pgClient = new Client({ connectionString: DATABASE_URL });

// 核心补丁:监听错误与断开,强制重启监听!
pgClient.on('error', (err) => {
logger.error('PG Listen Client Error, reconnecting...', err);
pgClient.end().catch(console.error);
setTimeout(setupSSEListener, 5000); // 5 秒后自动重连
});

pgClient.on('end', () => {
logger.warn('PG Listen Client Ended, reconnecting...');
setTimeout(setupSSEListener, 5000);
});

await pgClient.connect();
await pgClient.query('LISTEN sse_channel');
pgClient.on('notification', (msg) => { /* ... */ });
}
setupSSEListener();

🚨 破壁级漏洞 3singletonKey 作用域跨界的“狸猫换太子”

逐行审查发现的问题(位于 五、pg-boss 配置速查)

指南中建议这样写防重复 Key

await pgBoss.send('module_task_child', { taskId, itemId }, {
singletonKey: `child-${itemId}`, // ← 派发防重
});

🔥 灾难时序爆发:

  1. 假设 itemId 指的是源数据的 ID例如 PKB里的 Document A
  2. 医生张三在“项目 1”里提取了 Document A。pg-boss 生成了 singletonKey: child-DocA正在缓慢处理。
  3. 同时,医生李四在“项目 2”里也碰巧勾选了同一个 Document A 进行提取任务。
  4. pg-boss 会发现 singletonKey: child-DocA 已经在队列里了。根据幂等性去重规则,pg-boss 会直接丢弃Ignored李四的这个子任务
  5. 后果: 李四的这个任务永远缺少了这一篇文献的进度,父任务 Last Child Wins 机制卡死,系统挂起!

架构师修正方案 (引入绝对隔离域)

singletonKey 在 pg-boss 中是全局数据库唯一的!它绝不能仅仅绑定业务实体 ID必须绑定**“实体 + 本次任务实例”**的联合主键。

如果 itemId 对应的是 AslExtractionResult.id专属于某一次提取任务的单行 ID那是安全的。但为了规范指南必须明确指出

// 核心补丁singletonKey 必须携带父任务 ID 形成绝对隔离!
await pgBoss.send('module_task_child', { taskId, itemId }, {
singletonKey: `task-${taskId}-item-${itemId}`, // 必须绑定 Task 实例级别!
});

🏁 最终判词与交付建议

经过这次“贴脸”级别的底层审查,我们不仅防住了高并发的业务死锁,还防住了网络断线、V8 JSON解析、全局哈希碰撞这三个底层基建级别的灭顶之灾。

请将这 3 个补丁打入《分布式 Fan-out 任务模式开发指南 v1.2》中(升级为 v1.3 定稿版)。有了这套带底层防线的文档,您的团队在面对任何高并发、高可用挑战时,都能写出真正的“大厂级”工业代码!