Files
geMoldInsight/src/moldinsight/services/occ_process_pool.py
T
cjw e728dcd226 批次4后续专项完成:D11清偿 + OCC方案B实施 + 部署参数 + D2诚实标注 + CI门禁
① D11 HTML 报告 RustFS 单源化(TECH_DEBT P2 清偿):可视化产物写任务临时目录后
   裸传报告键 html/reports/{filename}(文件名寻址),/html StaticFiles 挂载删除,
   新增 html_report_router 根路径代理(报告键→遗留 JSON 包装→本地卷兜底→404,
   防穿越);URL 形状 /html/{filename} 不变,持久化引用零迁移;celery 摘除
   html_data 卷,镜像不再烤入陈旧报告;顺带删除 get_stp_file_with_data 死数据块
② OCC 方案 B(D10 清偿):run_occ(op_name, payload) 契约 + 常驻工作进程池
   (occ_process_pool + occ_worker 操作注册表),超时/崩溃 terminate 换新补位、
   任务级超时 recover 整体重建,残留线程泄漏根治;TopoDS 不跨进程(generate_cavity
   分模 + 方案 STEP 持久化全在子进程内,返回 export_manifest);删除内存形状缓存链、
   CADExporter.export_mold_results、shape_loader(→ stp_materializer)
③ OCC 方案 A 部署参数:CELERY_CONCURRENCY / CELERY_MAX_TASKS_PER_CHILD 进
   Dockerfile.celery + compose + .env.example
④ D2 诚实标注:铝价响应带 source: "simulated",前端按来源渲染标注(原硬编码
   "上海期货交易所"属虚假声明),死代码 getAluminumPrice 删除
⑤ CI 门禁:.gitea/workflows/ci.yml 三 job(pytest / 前端构建含 vue-tsc /
   openapi 漂移检测)

接口变更三件套随批完成(openapi 76→77 paths + gen:api + 前端构建通过;方案 B
接口面零变化)。测试基线 143 passed, 0 skipped(新增 16 项)。文档六处同步。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 17:22:01 +08:00

145 lines
5.8 KiB
Python
Raw 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.
# services/occ_process_pool.py
"""常驻 OCC 工作进程池(方案 B,见 docs/topics/performance/OCC_THROUGHPUT.md)。
替代原进程内 `ThreadPoolExecutor(max_workers=1)`:
- 每个工作进程是一个独立 OCC 通道(OCC 非线程安全,通道内串行),常驻不随任务拉起
(spawn 下 import OCC 秒级,按任务拉起会把开销摊到每个任务上)
- 超时/崩溃 = terminate() 换新进程补位——进程边界干净回收(线程级无法击杀 C++ 栈,
旧方案每次超时滞留 1 个线程)
- 输入输出走文件路径 + 普通字典,杜绝 pickle OCC 对象(见 core/occ_worker.py)
"""
import asyncio
import multiprocessing
from typing import Any, Dict, Optional
from moldinsight.core.occ_worker import worker_main
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class _OccWorker:
"""单个 OCC 工作进程的父进程侧封装。"""
def __init__(self, process, conn):
self.process = process
self.conn = conn
self.lock = asyncio.Lock() # 通道串行:同一进程同时只有一个操作在途
async def run(self, op_name: str, payload: Dict[str, Any], timeout: float):
# 阻塞式管道收发放 asyncio.to_thread,不卡事件循环;
# 超时后父进程 terminate() 子进程 → 管道 EOF → 该线程 recv 立即返回,无泄漏
result = await asyncio.wait_for(
asyncio.to_thread(self._run_blocking, op_name, payload),
timeout=timeout,
)
return result
def _run_blocking(self, op_name: str, payload: Dict[str, Any]):
self.conn.send((op_name, payload))
status, data = self.conn.recv()
if status == "error":
raise RuntimeError(data.get("error") or "OCC 子进程操作失败")
return data
class OccProcessPool:
"""OCC 工作进程池(默认 1 进程 = 1 串行通道,与旧单线程语义一致)。
池大小与 celery 并发解耦(celery 并发走多 worker 子进程,每个持自己的池)。
"""
def __init__(self, size: int = 1):
if size < 1:
raise ValueError("size 必须 >= 1")
self._size = size
self._ctx = multiprocessing.get_context("spawn")
self._workers: list[_OccWorker] = []
self._rr = 0
# 保护 workers 列表与轮转指针;操作执行期不持锁
self._pool_lock = asyncio.Lock()
# 任务级整体超时(process_file_with_storage 外层 wait_for)时的在途 worker 追踪
self._busy: Optional[_OccWorker] = None
async def run(self, op_name: str, payload: Dict[str, Any], timeout: float = 600):
for attempt in range(3):
async with self._pool_lock:
await self._ensure_started()
worker = self._workers[self._rr]
self._rr = (self._rr + 1) % len(self._workers)
self._busy = worker
try:
async with worker.lock:
return await worker.run(op_name, payload, timeout)
except asyncio.TimeoutError:
await self._replace(worker)
raise
except Exception as exc:
if not worker.process.is_alive():
# OCC segfault 等进程死亡:换新补位后重试该操作
logger.warning(f"OCC 工作进程异常退出,重试操作 {op_name}: {exc}")
await self._replace(worker)
continue
raise
finally:
if self._busy is worker:
self._busy = None
raise RuntimeError(f"OCC 工作进程连续异常,操作 {op_name} 未能完成")
async def recover(self):
"""任务级整体超时恢复:重建整个池,丢弃可能正卡在挂死 OCC 操作上的进程。
单通道池重建代价可忽略;重建后下次 run 自动按需补拉。
"""
async with self._pool_lock:
for worker in self._workers:
await self._terminate(worker)
self._workers = []
self._busy = None
logger.warning("OCC 进程池已整体重建(任务级超时恢复)")
async def shutdown(self):
async with self._pool_lock:
for worker in self._workers:
await self._terminate(worker)
self._workers = []
self._busy = None
async def _ensure_started(self):
if not self._workers:
for _ in range(self._size):
self._workers.append(self._spawn_one())
logger.info(f"OCC 进程池已启动: {self._size} 个常驻工作进程")
def _spawn_one(self) -> _OccWorker:
parent_conn, child_conn = self._ctx.Pipe(duplex=True)
proc = self._ctx.Process(target=worker_main, args=(child_conn,), daemon=True)
proc.start()
child_conn.close() # 父进程侧关闭子端,只留 parent_conn
return _OccWorker(proc, parent_conn)
async def _replace(self, worker: _OccWorker):
async with self._pool_lock:
await self._terminate(worker)
new_worker = self._spawn_one()
try:
idx = self._workers.index(worker)
except ValueError:
# 已被并发重建移除,新进程追加补位
self._workers.append(new_worker)
else:
self._workers[idx] = new_worker
logger.warning("OCC 工作进程已重建补位")
@staticmethod
async def _terminate(worker: _OccWorker):
try:
worker.process.terminate()
worker.process.join(timeout=5)
except Exception as exc:
logger.warning(f"终止 OCC 工作进程异常: {exc}")
try:
worker.conn.close()
except Exception:
pass