docs(asl): Upgrade Tool 3 architecture from Fan-out to Scatter+Aggregator (v2.0)
Architecture transformation: - Replace Fan-out (Manager->Child->Last Child Wins) with Scatter+Aggregator pattern - API layer directly dispatches N independent jobs (no Manager) - Worker only writes its own Result row, never touches Task table (zero row-lock) - Aggregator polls groupBy for completion + zombie cleanup (replaces Sweeper) - Reduce red lines from 13 to 9, eliminate distributed complexity Documents updated (10 files): - 08-Tool3 main architecture doc: v2.0 rewrite (schema, Task 2.3/2.4, red lines, risks) - 08d-Code patterns: rewrite sections 4.1-4.6 (API dispatch, SingleWorker, Aggregator) - 08a-M1 sprint: rewrite M1-3 core (Worker+Aggregator), red lines, acceptance criteria - 08b-M2 sprint: simplify SSE (NOTIFY/LISTEN downgraded to P2 optional) - 08c-M3 sprint: milestone table wording update - New: Scatter+Polling Aggregator pattern guide v1.1 (Level 2 cookbook) - New: V2.0 architecture deep review and gap-fix report - Updated: ASL module status, system status, capability layer index Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
145
docs/03-业务模块/ASL-AI智能文献/06-技术文档/工具3 V2.0(散装架构)深度审查与断层修复报告.md
Normal file
145
docs/03-业务模块/ASL-AI智能文献/06-技术文档/工具3 V2.0(散装架构)深度审查与断层修复报告.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# **🔍 深度审查与断层修复报告:工具 3 全文提取 (V2.0 散装架构版)**
|
||||
|
||||
**审查人:** 资深架构师
|
||||
|
||||
**审查对象:** 08 总纲及 08a/08b/08c/08d 拆分计划文档
|
||||
|
||||
**审查基准:** 《散装派发与轮询收口任务模式指南 v1.0》
|
||||
|
||||
**总体评估:** 战略方向极其正确,但**文档间存在严重的自相矛盾**。旧版 Fan-out 的代码(Manager、原子递增、独立清道夫)未被彻底清除,导致架构撕裂。必须立即修复 08a、08b 和 08d 文档。
|
||||
|
||||
## **🚨 致命断层 1:Worker 仍在修改父任务(行锁地雷未除)**
|
||||
|
||||
### **❌ 发现的问题 (位于 08d 代码手册 §4.3)**
|
||||
|
||||
在 08 总纲中,您明确写道:“Worker 绝不碰 Task 表,进度由 Aggregator groupBy 聚合”。
|
||||
|
||||
但是!在 08d 的 ExtractionChildWorker 代码示例中,依然保留了旧版 Fan-out 的致命逻辑:
|
||||
|
||||
// 08d 错误残留代码:
|
||||
const \[\_resultUpdate, taskAfterUpdate\] \= await prisma.$transaction(\[
|
||||
prisma.aslExtractionResult.update(...),
|
||||
prisma.aslExtractionTask.update({ ... data: { successCount: { increment: 1 } } }) // ❌ 严重违规!
|
||||
\]);
|
||||
// ...
|
||||
if (taskAfterUpdate.successCount \+ ... \>= totalCount) { ... } // ❌ 严重违规 (Last Child Wins 残留)
|
||||
|
||||
**危害:** 如果开发照抄这段代码,散装架构最核心的优势“无锁并发”将彻底丧失,依然会引发 Lock wait timeout 和死锁!
|
||||
|
||||
### **✅ 修复指令 (修改 08d §4.3)**
|
||||
|
||||
彻底删除 $transaction、删除 increment,删除 Last Child Wins 判定。Worker 的代码必须极简:
|
||||
|
||||
// 修正后的 08d §4.3:纯粹的散装 Worker
|
||||
// ... 幽灵重试守卫保持不变 ...
|
||||
const extractResult \= await this.extractionService.extractOne(resultId, taskId);
|
||||
|
||||
// ✅ 核心:只管更新自己的 Result,绝不碰 Task!
|
||||
await prisma.aslExtractionResult.update({
|
||||
where: { id: resultId },
|
||||
data: { status: 'completed', extractedData: extractResult.data, processedAt: new Date() }
|
||||
});
|
||||
|
||||
// ✅ SSE 广播 (可选)
|
||||
await broadcastLog(taskId, { source: 'system', message: \`✅ ${extractResult.filename} extracted\` });
|
||||
// 结束!没有任何收口逻辑!
|
||||
|
||||
## **🚨 致命断层 2:Manager 的幽灵依然存在**
|
||||
|
||||
### **❌ 发现的问题 (位于 08a M1-3 和 08d §4.2)**
|
||||
|
||||
* 08 总纲明确声明:“消灭的组件:ExtractionManagerWorker”,“API 层直接散装派发 N 个独立 Job”。
|
||||
* 然而,08a (M1-3) 依然安排了 1 天工期去开发 ExtractionManagerWorker.ts。
|
||||
* 08d (§4.2) 依然详细保留了 Manager Worker 的完整代码。
|
||||
|
||||
**危害:** 架构撕裂。如果 API 层发了一个 Manager Job,Manager 又去发 Child Job,这就退回到了极其笨重的 Fan-out 模式,且徒增了一次重试崩溃的风险。
|
||||
|
||||
### **✅ 修复指令**
|
||||
|
||||
1. **修改 08a M1-3**:划掉 ExtractionManagerWorker.ts 的开发任务。
|
||||
2. **修改 08d §4.2**:将此节更名为 **§4.2 API 层散装派发 (ExtractionController)**,并替换为以下代码:
|
||||
|
||||
// 修正后的 08d §4.2:API 直接派发
|
||||
async function createTask(req, reply) {
|
||||
// ... DB 幂等拦截 (idempotencyKey) ...
|
||||
|
||||
// 1\. 获取 PKB 快照元数据并冻结到 DB (移至 API 层执行)
|
||||
const pkbDocs \= await pkbBridge.getDocumentsDetail(documentIds);
|
||||
const resultsData \= pkbDocs.map(doc \=\> ({
|
||||
taskId: task.id,
|
||||
pkbDocumentId: doc.documentId,
|
||||
snapshotStorageKey: doc.storageKey,
|
||||
status: 'pending'
|
||||
}));
|
||||
await prisma.aslExtractionResult.createMany({ data: resultsData });
|
||||
|
||||
const createdResults \= await prisma.aslExtractionResult.findMany({ where: { taskId: task.id } });
|
||||
|
||||
// 2\. 🚀 极速散装派发
|
||||
const jobs \= createdResults.map(result \=\> ({
|
||||
name: 'asl\_extract\_single',
|
||||
data: { resultId: result.id, taskId: task.id, pkbDocumentId: result.pkbDocumentId },
|
||||
options: { singletonKey: \`extract-${result.id}\`, expireInMinutes: 30 }
|
||||
}));
|
||||
await jobQueue.insert(jobs); // 批量压入 pg-boss
|
||||
|
||||
return reply.send({ taskId: task.id });
|
||||
}
|
||||
|
||||
## **🚨 致命断层 3:Sweeper(清道夫) 与 Aggregator(收口器) 未合并**
|
||||
|
||||
### **❌ 发现的问题 (位于 08a M1-3 和 08d §4.6)**
|
||||
|
||||
在《散装模式指南》中,你们团队提出了一个绝妙的优化:“Aggregator 兼职 Sweeper,一个组件两个职责”。
|
||||
|
||||
但在 08a 中,依然要求单独注册 asl\_extraction\_sweeper;在 08d 中也保留了独立的 Sweeper 代码。
|
||||
|
||||
### **✅ 修复指令 (修改 08a 和 08d)**
|
||||
|
||||
1. **修改 08a**:删除 Step D (Sweeper 注册)。新增任务:“开发 ExtractionAggregator.ts,实现僵尸清理与轮询收口”。
|
||||
2. **修改 08d**:删除 §4.6 的独立 Sweeper。补充 ExtractionAggregator 的标准代码:
|
||||
|
||||
// 修正后的 08d §4.6:ExtractionAggregator 轮询包工头
|
||||
jobQueue.work('asl\_extraction\_aggregator', async () \=\> {
|
||||
const tasks \= await prisma.aslExtractionTask.findMany({ where: { status: 'processing' } });
|
||||
|
||||
for (const task of tasks) {
|
||||
// 1\. 顺手清理僵尸 (Sweeper 职责)
|
||||
await prisma.aslExtractionResult.updateMany({
|
||||
where: { taskId: task.id, status: 'extracting', updatedAt: { lt: new Date(Date.now() \- 30 \* 60 \* 1000\) } },
|
||||
data: { status: 'error', errorMessage: '\[Aggregator\] Timeout, likely worker crash.' }
|
||||
});
|
||||
|
||||
// 2\. 聚合统计
|
||||
const stats \= await prisma.aslExtractionResult.groupBy({ by: \['status'\], where: { taskId: task.id }, \_count: true });
|
||||
const pending \= stats.find(s \=\> s.status \=== 'pending')?.\_count || 0;
|
||||
const extracting \= stats.find(s \=\> s.status \=== 'extracting')?.\_count || 0;
|
||||
|
||||
// 3\. 收口
|
||||
if (pending \=== 0 && extracting \=== 0\) {
|
||||
await prisma.aslExtractionTask.update({ where: { id: task.id }, data: { status: 'completed' } });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
## **⚠️ 体验断层 4:M2-3 对 NOTIFY/LISTEN 的执念**
|
||||
|
||||
### **❌ 发现的问题 (位于 08b M2-3)**
|
||||
|
||||
08 总纲明确将 NOTIFY/LISTEN 跨 Pod 广播降级为“已被 V2.0 纯轮询替代/可选增强”。但在 M2 冲刺清单(08b M2-3)中,依然将 SseNotifyBridge.ts 和独立 PgClient 的开发列为了“必须做的额外任务”。
|
||||
|
||||
### **✅ 修复指令 (修改 08b)**
|
||||
|
||||
为了保证 M2 的按时交付,请将 08b 中的 **"🆕 v1.5 额外任务:SSE 跨 Pod 广播"** 直接**删除**或标注为 **\[P2 可选\]**。
|
||||
|
||||
在散装架构下,既然进度条和跳页全靠 React Query 轮询,终端日志由于没有严重的业务关联,前期完全可以用“本 Pod 内存事件”将就,没必要在 M2 阶段去死磕复杂的 PG 独立监听连接。
|
||||
|
||||
## **🏁 架构师总结建议**
|
||||
|
||||
这个断层非常典型。因为我们前期对 Fan-out 模式进行了多达 6 轮的极限推演和打磨,那些代码(乐观锁、原子递增、Manager)已经深入人心,导致在重写 V2.0 文档时,大家潜意识里舍不得删掉它们。
|
||||
|
||||
**请项目经理做最后一步:**
|
||||
|
||||
打开 08a、08b 和 08d。凡是带了 Manager、successCount: { increment: 1 }、Last Child Wins、asl\_extraction\_sweeper 字眼的代码和任务,**全部无情删掉**,替换为上述的散装代码。
|
||||
|
||||
删完之后,你们的系统代码量将减少 30%,但稳定性将提升 200%!恭喜团队,你们即将拥有一套极其优雅的底层流水线,放心开干吧!
|
||||
Reference in New Issue
Block a user