x
This commit is contained in:
@@ -2,12 +2,14 @@
|
||||
"""STP 文件处理流程编排器 — 协调解析、网格生成、型腔生成、保存、验证"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from collections import OrderedDict
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -40,12 +42,35 @@ class ProcessingService:
|
||||
self.storage_service = StorageIntegrationService()
|
||||
self.multi_scheme_planner = MultiSchemeMoldPlanner()
|
||||
self.cad_exporter = CADExporter()
|
||||
self._export_shapes_cache: Dict[str, Dict[str, Dict[str, Any]]] = {}
|
||||
# TopoDS_Shape 为 C++ 原生内存对象,LRU 上限防止长期运行内存只涨不降
|
||||
self._export_shapes_cache: "OrderedDict[str, Dict[str, Dict[str, Any]]]" = OrderedDict()
|
||||
self._export_shapes_cache_max = 32
|
||||
# OCC 非线程安全,max_workers=1 保证所有 OCC 操作序列化执行,避免偶发崩溃
|
||||
self._occ_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="occ")
|
||||
|
||||
# ─── 对外入口 ───
|
||||
|
||||
def _reset_occ_executor(self):
|
||||
"""超时后重建 OCC executor。
|
||||
|
||||
asyncio.wait_for 只能取消协程,正在执行 OCC 布尔运算的线程无法中断;
|
||||
单 worker executor 中一个挂死线程会让后续任务永久排队直至重启。
|
||||
代价是泄漏 1 个线程,收益是恢复服务可用性。
|
||||
"""
|
||||
old = self._occ_executor
|
||||
self._occ_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="occ")
|
||||
old.shutdown(wait=False)
|
||||
logger.warning("OCC executor 已因处理超时重建(放弃等待旧线程,可能泄漏 1 个线程)")
|
||||
|
||||
async def run_occ(self, fn, *args):
|
||||
"""在 OCC 单线程 executor 中执行同步几何操作。
|
||||
|
||||
OCC 非线程安全,所有几何计算(解析/布尔/三角化)统一经由本入口串行执行,
|
||||
避免各调用方自行创建线程池造成并发崩溃。
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(self._occ_executor, fn, *args)
|
||||
|
||||
async def process_file_with_storage(
|
||||
self,
|
||||
task_id: str,
|
||||
@@ -79,6 +104,8 @@ class ProcessingService:
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"处理超时: {task_id}")
|
||||
# OCC 线程无法取消:抛弃整个 executor,避免挂死线程堵死后续所有任务
|
||||
self._reset_occ_executor()
|
||||
raise Exception(f"处理超时,超过{timeout_seconds}秒未完成")
|
||||
|
||||
except Exception as e:
|
||||
@@ -338,12 +365,12 @@ class ProcessingService:
|
||||
},
|
||||
)
|
||||
|
||||
# 更新任务缓存状态(仅保留轻量摘要,完整数据由PG+RustFS持久化)
|
||||
# 更新任务缓存状态(仅保留轻量摘要,完整数据由PG+RustFS持久化;
|
||||
# 完成态视图由 TaskQueryService 从 PG+RustFS 组装,Redis 不再存
|
||||
# geometry_data / analysis_result 等 MB 级大对象)
|
||||
await redis_task_manager.update_task(task_id, {
|
||||
"status": ProcessingStatus.COMPLETED,
|
||||
"completed_at": str(datetime.now()),
|
||||
"geometry_data": geometry_data,
|
||||
"analysis_result": analysis_result,
|
||||
"key_info": best_key_info,
|
||||
"best_scheme_id": detailed_cavity_json.get("best_scheme_id"),
|
||||
"material": requested_material,
|
||||
@@ -471,6 +498,10 @@ class ProcessingService:
|
||||
|
||||
def _cache_export_shapes(self, task_id: str, export_shapes: Dict[str, Dict[str, Any]]):
|
||||
self._export_shapes_cache[task_id] = export_shapes
|
||||
self._export_shapes_cache.move_to_end(task_id)
|
||||
# 逐出最旧任务的形状缓存(连原生 OCC shape 引用一起释放)
|
||||
while len(self._export_shapes_cache) > self._export_shapes_cache_max:
|
||||
self._export_shapes_cache.popitem(last=False)
|
||||
|
||||
def _persist_step_exports(
|
||||
self,
|
||||
@@ -492,10 +523,11 @@ class ProcessingService:
|
||||
|
||||
for scheme_id, cavity_data in export_shapes.items():
|
||||
try:
|
||||
result = self.cad_exporter.export_mold_results(
|
||||
# 持久化装配体 STEP + 逐组件 STEP(后者是重启后按需
|
||||
# 重导出其他格式的几何来源,见 regenerate_export_from_persisted)
|
||||
result = self.cad_exporter.export_persisted_steps(
|
||||
cavity_data=cavity_data,
|
||||
base_filename=base_filename,
|
||||
formats=["step"],
|
||||
components=components,
|
||||
task_id=task_id,
|
||||
scheme_id=scheme_id,
|
||||
@@ -522,13 +554,88 @@ class ProcessingService:
|
||||
return manifest
|
||||
|
||||
def get_export_shapes(self, task_id: str, scheme_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
scheme_map = self._export_shapes_cache.get(task_id, {})
|
||||
scheme_map = self._export_shapes_cache.get(task_id)
|
||||
if not scheme_map:
|
||||
return None
|
||||
self._export_shapes_cache.move_to_end(task_id) # LRU 热点保活
|
||||
if scheme_id:
|
||||
return scheme_map.get(scheme_id)
|
||||
return next(iter(scheme_map.values()), None)
|
||||
|
||||
async def regenerate_export_from_persisted(
|
||||
self,
|
||||
task_id: str,
|
||||
scheme_id: str,
|
||||
formats: Optional[List[str]],
|
||||
components: List[str],
|
||||
base_filename: str,
|
||||
scheme_files: List[Dict[str, Any]],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""内存导出缓存失效(如服务重启)后,从持久化 STEP 重建导出文件。
|
||||
|
||||
分析期已为每个方案持久化装配体 + 逐组件 STEP;
|
||||
缺失格式(STL/IGES/BRep)读回单组件 STEP 现场转换,
|
||||
用户无需重新分析。所有组件均不可用时返回 None。
|
||||
"""
|
||||
format_list = list(dict.fromkeys(formats or ["step", "stl"]))
|
||||
step_files = {
|
||||
f.get("component"): f
|
||||
for f in scheme_files or []
|
||||
if f.get("format") == "step" and f.get("component")
|
||||
}
|
||||
if not step_files:
|
||||
return None
|
||||
|
||||
files: List[Dict[str, Any]] = []
|
||||
errors: List[str] = []
|
||||
|
||||
if "step" in format_list:
|
||||
assembly = step_files.get("assembly")
|
||||
if assembly:
|
||||
files.append(assembly)
|
||||
else:
|
||||
errors.append("模具装配体 (step) 不可用")
|
||||
|
||||
for comp in components:
|
||||
comp_file = step_files.get(comp)
|
||||
if comp_file is None:
|
||||
errors.append(f"组件 {comp} 的持久化 STEP 不可用")
|
||||
continue
|
||||
for fmt in format_list:
|
||||
if fmt == "step":
|
||||
files.append(comp_file)
|
||||
continue
|
||||
step_path = os.path.join(
|
||||
self.cad_exporter.output_dir,
|
||||
str(comp_file.get("relative_path") or "").replace("/", os.sep),
|
||||
)
|
||||
out_path = os.path.join(
|
||||
os.path.dirname(step_path), f"{base_filename}_{comp}.{fmt}"
|
||||
)
|
||||
ok = await self.run_occ(
|
||||
self.cad_exporter.convert_component_step, step_path, out_path, fmt
|
||||
)
|
||||
if ok:
|
||||
files.append(
|
||||
self.cad_exporter.build_file_entry(comp, fmt, out_path)
|
||||
)
|
||||
else:
|
||||
errors.append(f"组件 {comp} ({fmt}) 转换失败")
|
||||
|
||||
if not files:
|
||||
return None
|
||||
|
||||
return {
|
||||
"base_filename": base_filename,
|
||||
"task_id": task_id,
|
||||
"scheme_id": scheme_id,
|
||||
"files": files,
|
||||
"errors": errors,
|
||||
"total_files": len(files),
|
||||
"total_errors": len(errors),
|
||||
"source": "regenerated",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_process_params(process_params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
payload = dict(process_params or {})
|
||||
|
||||
Reference in New Issue
Block a user