按 ROADMAP §3.1 治理批次推进的后端设计审查整改:
- 批次 0(安全):/api/status/{task_id} 补 JWT 鉴权与任务归属校验;
pythonocc_available 真实探测;bcrypt 超 72 字节显式拒绝;
SECRET_KEY/RUSTFS_* 惰性校验,代码侧弱默认移除
- 批次 1(部署正确性):主处理链路改走 RustFS(分派入参 stp_file_id 化,
worker 按 object_key 下载);AUTO_MIGRATE 开关 + 迁移目录 alembic/→migrations/
修复包遮蔽(自动迁移此前从未真正生效);OCC 镜像改 conda 原生执行 +
基础镜像 tag 锁定;compose 关键项改 ${VAR:?} 强制显式配置
- 批次 2(任务一致性):删除 Redis 进程内存回退,PG 为任务状态单一事实源;
批量元数据入库(processing_tasks.batch_id,迁移 a3f8c2d91e47);
型腔失败任务标 failed 不再静默 completed;事务边界收口
(数据本体写 flush-only、失败先回滚再置 failed、进度更新保留即时 commit)
- 批次 3(API 与代码结构):592 行 advanced_router 拆为 design/cost/machining/
export 四子路由,请求体全量 Pydantic 化;ROUTE_MODULES + route_registry
(/api/health 呈现 degraded,DEBUG fail fast);纯计算端点统一 to_thread;
StorageIntegrationService 按职责三拆;MAX_FILE_SIZE 接线生效、
celery 复用 Settings.redis_url;管理员重置密码改 JSON body(端到端断裂修复);
openapi.json 重导出(76 paths)+ 前端 gen:api
- 批次 4(架构演进):共享 ORM 按模块拆分(shared/models/base.py + identity.py、
moldinsight/models/、inventory/models/,删除三条无使用方的跨模块
relationship,跨模块桥接收敛为裸 FK 硬规则,无兼容 facade);
OCC executor 重建补 cancel_futures=True(消除旧队列被慢恢复线程
并行消化的数据竞争);OCC 吞吐方案设计先行
(docs/topics/performance/OCC_THROUGHPUT.md);顺手清偿 D15
(vite.config.ts 未用参数致 npm run build 失败)
测试基线:125 passed, 2 skipped(pytest + sqlite+aiosqlite;归属边界、
路由契约、配置治理、鉴权回归等随批新增)
文档同步:STATUS / TECH_DEBT / ROADMAP / ARCHITECTURE / API_CONTRACT /
OPERATIONS / AGENTS
Co-Authored-By: Claude Code <noreply@anthropic.com>
6.1 KiB
OCC 处理吞吐与隔离方案设计(OCC_THROUGHPUT)
文档定位:OCC(PythonOCC)处理吞吐与故障隔离的专题设计文档。 本文回答"OCC 串行瓶颈与超时线程泄漏的根治路线";现状事实以 ../../../STATUS.md 为准,债务归属 ../../TECH_DEBT.md D10。 2026-09-17 随批次 4 产出:方案设计先行,短期项(方案 A)零代码可用,中期的接口演进与进程池实施留待后续批次。
1. 现状与硬约束
1.1 运行时事实
- 所有 OCC 操作(STEP 解析 / 布尔运算 / 三角化 / 倒扣检测等)统一经 processing_service.py 的
run_occ投入 进程内ThreadPoolExecutor(max_workers=1)串行执行——OCC 非线程安全,串行是正确性要求,不是实现偷懒。 - Celery worker 为 prefork 模式,
processing_service是模块级单例:每个 worker 子进程各持一个串行 OCC 通道。因此 OCC 并行度 = worker 子进程数,与 web 进程数无关(web 侧run_occ仅服务于轻量同步调用,如倒扣检测)。 - 型腔生成超时后
_reset_occ_executor重建 executor;已在运行的 C++ 线程在 Python 层不可杀,每次超时滞留 1 个线程(2026-09-17 起排队任务随cancel_futures=True丢弃,见 §4)。
1.2 硬约束(决定方案边界)
| 约束 | 含义 |
|---|---|
| OCC 非线程安全 | 任何方案中,一个进程内 OCC 操作必须串行;并行只能靠多进程 |
| C++ 栈不可中断 | 线程级超时只能"抛弃"不能"击杀";只有进程级 kill 是干净的故障恢复 |
run_occ(fn, *args) 传闭包/绑定方法 |
函数对象不可跨进程 pickle——进程化方案必须改接口为"操作名 + 可序列化参数" |
| STEP 重载成本 | 进程间不共享 OCC 形状对象;跨进程方案每次调用需重新读文件/传 BRep(几秒级) |
2. 方案对比
方案 A:Celery prefork 并行伸缩(短期,零新代码)
做法:承认"每子进程一个串行 OCC 通道"的既有事实,把 OCC 吞吐问题转化为 worker 进程数问题:celery -A celery_app worker --concurrency=N,N = 期望的并行分析数(受 CPU 核数与每进程内存约束)。
- 优点:零代码改动;进程边界天然兜住线程泄漏——泄漏线程随子进程存亡,配合
--max-tasks-per-child=M(子进程处理 M 个任务后重启回收)可把滞留线程的存续时间限制在一个批次内。 - 代价:每个子进程常驻完整 Python + OCC 运行时(数百 MB),N 不能无脑调大;DB 连接按 celery 角色池(pool_size=5)随子进程倍增,PG
max_connections需要相应预算。 - 不解决:单任务超时后该子进程内的线程滞留(被 max-tasks-per-child 兜底回收);单任务无加速(串行本质不变)。
结论:立即可用的推荐做法。部署侧调整(concurrency / max-tasks-per-child)随下次镜像与 compose 评审落地,先在 OPERATIONS.md 记录启动参数建议。
方案 B:常驻 OCC 进程池 + kill-on-timeout(中期,推荐演进方向)
做法:在 run_occ 接口之下替换执行器——不再是 ThreadPoolExecutor,而是常驻的单线程 OCC 工作进程池(每进程一个事件循环:接任务 → 执行 → 回报)。超时由主进程 terminate() 工作进程并更换新进程补位。
- 接口演进:
run_occ(fn, *args)→run_occ(op_name: str, payload: dict),操作名注册表映射到模块级函数(STEP 文件路径进、JSON/BRep 文件出,杜绝 pickle 大对象);各调用点(解析、型腔、倒扣、导出三角化……)逐一迁移。 - 优点:超时 = 杀进程,故障恢复干净彻底(D10 残留泄漏根治);OCC 崩溃(segfault)不再波及 API/worker 主进程;进程池大小与 celery 并发解耦。
- 代价:一次明确的接口迁移(所有
run_occ调用点 + 结果序列化);进程池自管理(补位、健康检查、启动预热——spawn 下 import OCC 秒级,需常驻而非按任务拉起);跨进程只传文件路径 + JSON,现有"传形状对象"的内部调用要改为落盘中转。 - 风险:自建进程池的运维复杂度;Windows 开发环境 spawn 语义与 Linux fork 差异需测试覆盖。
方案 C:OCC sidecar 服务(长期,视伸缩需求)
做法:OCC 能力独立成进程/容器(HTTP 或 gRPC),API 与 worker 都是客户端;STEP 按路径/对象键传入,返回 JSON 摘要 + 产物对象键。
- 优点:隔离最彻底;OCC 可独立伸缩、独立发布、独立扩容 GPU/内存型节点;多语言可复用。
- 代价:新增一个部署单元与序列化边界(大网格/形状数据传输设计);超出当前"单 compose 栈"的部署叙事,需与 DEPLOYMENT 文档体系一起演进。
结论:除非出现独立伸缩/隔离性硬需求,暂不启动。
3. 决策与路线
| 阶段 | 动作 | 状态 |
|---|---|---|
| 短期 | 方案 A:--concurrency 伸缩 + --max-tasks-per-child 兜底回收;cancel_futures=True 修复重建并发风险 |
✅ 代码侧 2026-09-17 完成;部署参数随下次 compose/镜像评审落地 |
| 中期 | 方案 B:run_occ(op_name, payload) 接口演进 + 常驻进程池,kill-on-timeout 根治泄漏 |
待排期(独立批次,工作量集中在调用点迁移与序列化设计) |
| 长期 | 方案 C:sidecar,仅在出现独立伸缩需求时启动 | 暂不启动 |
4. 本次已落地的缓解(2026-09-17,批次 4)
_reset_occ_executor 的 shutdown(wait=False) 补 cancel_futures=True。这不只是卫生问题:旧实现下旧 executor 的排队任务不会消失——若挂死线程后来"慢恢复",旧线程会继续消化旧队列,与新 executor 并发操作非线程安全的 OCC(数据竞争 / 崩溃风险)。补参后排队任务即被丢弃,残留问题收敛为"运行中线程滞留 1 个",由方案 A 的进程回收兜底。