From e728dcd226c640fe2dee56cd967fb1d0c909d5a5 Mon Sep 17 00:00:00 2001 From: chenjw28 <792430652@qq.com> Date: Fri, 18 Sep 2026 17:22:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=B9=E6=AC=A14=E5=90=8E=E7=BB=AD=E4=B8=93?= =?UTF-8?q?=E9=A1=B9=E5=AE=8C=E6=88=90=EF=BC=9AD11=E6=B8=85=E5=81=BF=20+?= =?UTF-8?q?=20OCC=E6=96=B9=E6=A1=88B=E5=AE=9E=E6=96=BD=20+=20=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E5=8F=82=E6=95=B0=20+=20D2=E8=AF=9A=E5=AE=9E=E6=A0=87?= =?UTF-8?q?=E6=B3=A8=20+=20CI=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ① 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 --- .env.example | 5 + .gitea/workflows/ci.yml | 68 ++++ AGENTS.md | 15 +- deploy/Dockerfile.celery | 10 +- deploy/Dockerfile.moldinsight | 1 - docker-compose.yml | 12 +- docs/API_CONTRACT.md | 3 +- docs/OPERATIONS.md | 6 +- docs/ROADMAP.md | 4 +- docs/STATUS.md | 2 + docs/TECH_DEBT.md | 35 +- docs/topics/performance/OCC_THROUGHPUT.md | 28 +- frontend/src/modules/home/HomeView.vue | 18 +- frontend/src/shared/api-client.ts | 4 - frontend/src/types/api.ts | 48 +++ openapi.json | 39 ++- src/entrypoints/inventory.py | 1 - src/entrypoints/moldinsight.py | 9 +- src/entrypoints/unified.py | 9 +- src/moldinsight/api/core_modules.py | 3 +- src/moldinsight/api/design_router.py | 30 +- src/moldinsight/api/export_router.py | 101 ++---- src/moldinsight/api/html_report_router.py | 139 ++++++++ src/moldinsight/core/cad_exporter.py | 110 ------ src/moldinsight/core/occ_worker.py | 251 ++++++++++++++ .../services/aluminum_price_service.py | 4 + .../services/analysis_storage_service.py | 56 ++- src/moldinsight/services/occ_process_pool.py | 144 ++++++++ .../services/processing_service.py | 325 ++++++++---------- src/moldinsight/services/shape_loader.py | 88 ----- src/moldinsight/services/stp_materializer.py | 76 ++++ src/moldinsight/storage/rustfs_storage.py | 67 ++++ src/shared/app_factory.py | 16 +- tests/test_aluminum_price_source.py | 20 ++ tests/test_html_report_router.py | 176 ++++++++++ tests/test_occ_process_pool.py | 116 +++++++ 36 files changed, 1474 insertions(+), 565 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 src/moldinsight/api/html_report_router.py create mode 100644 src/moldinsight/core/occ_worker.py create mode 100644 src/moldinsight/services/occ_process_pool.py delete mode 100644 src/moldinsight/services/shape_loader.py create mode 100644 src/moldinsight/services/stp_materializer.py create mode 100644 tests/test_aluminum_price_source.py create mode 100644 tests/test_html_report_router.py create mode 100644 tests/test_occ_process_pool.py diff --git a/.env.example b/.env.example index 3d73076..39471ac 100644 --- a/.env.example +++ b/.env.example @@ -88,3 +88,8 @@ LLM_API_KEY=sk-your-api-key LLM_MODEL=gpt-4o-mini LLM_TIMEOUT=60 LLM_MAX_TOKENS=2000 + +# Celery/OCC 吞吐调优(可选,默认值见 deploy/Dockerfile.celery; +# concurrency 即 OCC 并行分析数,见 docs/topics/performance/OCC_THROUGHPUT.md) +# CELERY_CONCURRENCY=2 +# CELERY_MAX_TASKS_PER_CHILD=50 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..a29905e --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,68 @@ +# CI 门禁(Gitea Actions,语法与 GitHub Actions 兼容) +# +# 三个 job: +# 1. backend-tests —— pytest 全量(sqlite+aiosqlite,无外部服务依赖; +# OCC 契约测试在无 pythonocc 的 pip 环境自动 skip) +# 2. frontend-build —— npm ci + npm run build(含 vue-tsc -b 类型检查,D15 已修复) +# 3. openapi-drift —— 用 OCC 环境重导出 openapi.json 与仓库版本比对, +# 防止接口变更三件套(AGENTS §2)被遗漏导致前后端漂移 +# +# 运行前提:Gitea 实例启用 Actions 且注册了 runner; +# `ubuntu-latest` label 需映射到带 node/git 的镜像(gitea runner 默认映射满足)。 +# 已验证事实:pytest 与 openapi 导出均不依赖 .env(settings 惰性校验)。 + +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + backend-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + # requirements.txt 自带阿里云 pip 镜像配置 + run: pip install -r requirements.txt + - name: Run tests + run: python -m pytest tests/ -q + + frontend-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install dependencies + working-directory: frontend + run: npm ci + - name: Type check and build + working-directory: frontend + run: npm run build + + openapi-drift: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Miniforge and pythonocc + # pythonocc-core 仅经 conda-forge 提供(与 deploy/Dockerfile.moldinsight 同版本约束) + # 默认走 TUNA 镜像(Gitea 服务器多为国内网络);海外环境可换回官方源 + run: | + wget -q https://mirrors.tuna.tsinghua.edu.cn/github-release/conda-forge/miniforge/LatestRelease/Miniforge3-Linux-x86_64.sh -O miniforge.sh + bash miniforge.sh -b -p "$HOME/miniforge" + "$HOME/miniforge/bin/conda" create -n ci -c https://mirrors.tuna.tsinghua.edu.cn/conda-forge -y python=3.12 pythonocc-core=7.9.0 + - name: Install python dependencies + run: "$HOME/miniforge/envs/ci/bin/pip" install -r requirements.txt + - name: Export openapi.json and compare with committed version + run: | + "$HOME/miniforge/envs/ci/bin/python" -c "import sys; sys.path.insert(0, 'src'); from entrypoints.unified import app; import json; print(json.dumps(app.openapi(), ensure_ascii=False, indent=2))" > /tmp/openapi.json + git diff --exit-code --no-index /tmp/openapi.json openapi.json || { + echo "::error::openapi.json 与代码不一致——请按 docs/API_CONTRACT.md §4 重导出并执行 npm run gen:api" + exit 1 + } diff --git a/AGENTS.md b/AGENTS.md index d10bcc5..62db35a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,7 +53,8 @@ src/ machining_router.py # 加工类接口:/design-cam /check-collision /optimize-toolpath /design-electrodes /simulate-machining export_router.py # 导出类接口:/export-mold /export-download /export-recommendations core_modules.py # 核心计算模块惰性装载器(设计/加工路由共用,装载失败 503) - aluminum_price_routes.py # /api/aluminum-price/* 铝价(当前为模拟/参考数据,见 TECH_DEBT D2) + aluminum_price_routes.py # /api/aluminum-price/* 铝价(模拟数据,响应带 source: "simulated",见 TECH_DEBT D2) + html_report_router.py # GET /html/{filename} 报告代理(根路径挂载:RustFS 报告键 → 遗留 JSON 包装 → 本地卷兜底;include_into 由入口调用) debug_router.py # /api/debug/tasks 全量任务 dump(仅 DEBUG 模式注册,仍需登录) core/ # 几何与方案核心算法(OCC 重依赖区) stp_parser.py # STEP/STP 解析 @@ -71,16 +72,18 @@ src/ cavity_layout_optimizer.py # 型腔布局优化 mold_machining.py / mold_cam.py # 加工与 CAM mold_quality_inspector.py # 质量检查 - cad_exporter.py # CAD 导出 + cad_exporter.py # CAD 导出(export_mold_results 随方案 B 已删;export_step 等供 OCC 子进程持久化/转换) + occ_worker.py # OCC 常驻工作进程入口:操作注册表(parse_stp/generate_mesh/generate_cavity/analyze_mold_design/detect_undercuts/convert_component_step/ping/sleep/warmup)+ worker_main 消息循环 services/ # 业务服务层 task_dispatcher.py # 后台任务统一分派(勿绕过它 fire-and-forget) task_query_service.py # 任务状态查询聚合 - processing_service.py # 分析处理编排 + processing_service.py # 分析处理编排(run_occ 经常驻 OCC 进程池调度,见 occ_process_pool.py / OCC_THROUGHPUT.md) + occ_process_pool.py # OCC 常驻工作进程池(方案 B):超时/崩溃 terminate 换新补位;任务级超时 recover 整体重建 calculation_service.py # 计算服务 cost_estimate_service.py # 成本估算 cam_bundle_service.py # CAM 结果打包 verification_service.py # FreeCAD 验证(可选) - shape_loader.py # shape 加载(含 OCC 超时后 executor 重建逻辑) + stp_materializer.py # 按 task_id 把 STP 原件落盘临时文件(OCC 解析在子进程内,形状不跨进程) material_service.py # 物料价格服务 aluminum_price_service.py # 铝价服务(模拟数据) llm_service.py # LLM 增强分析(可选,OpenAI 兼容) @@ -98,7 +101,7 @@ src/ models/ # inventory 域 ORM(catalog / warehouse / trading / finance 四文件,共 15 表) utils.py shared/ # 【共享平台层:只放真正跨模块复用的基础能力,勿堆业务】 - app_factory.py # create_app:request_id 日志中间件 / auth_router / /health / SPA fallback / /html mount + app_factory.py # create_app:request_id 日志中间件 / auth_router / /health / SPA fallback / connect_rustfs 开关(D11 后 /html 由 moldinsight 代理路由提供,不再挂本地 StaticFiles) config/settings.py # Settings 单例:dotenv + os.getenv;DB_*/SECRET_KEY 惰性校验无默认 database/database.py # async engine / session / get_db_session database/init_db.py # 建表与管理员种子 @@ -110,7 +113,7 @@ src/ services/redis_task_manager.py # Redis 任务状态(Hash 字段级原子更新,兼容旧 string) utils/logger.py # 结构化日志(json/text)+ request_id utils/file_handler.py # 上传文件处理 - utils/html_generator.py # /html 静态分析报告生成 + utils/html_generator.py # 可视化报告生成(HTML/摘要/数据 JSON;产物写任务临时目录,由 moldinsight 上传 RustFS 报告键) celery_app.py # Celery app(Redis broker,task_acks_late) celery_tasks.py # moldinsight 异步分析任务 frontend/ # Vue 3 独立工程:src/modules 按域组织(moldinsight/inventory/users/login/home);src/types/api.ts 为 openapi 生成物,勿手改 diff --git a/deploy/Dockerfile.celery b/deploy/Dockerfile.celery index 6357a09..e05a629 100644 --- a/deploy/Dockerfile.celery +++ b/deploy/Dockerfile.celery @@ -3,4 +3,12 @@ # gemold-backend:latest 不一致,干净环境下 celery 镜像构建必然失败。 FROM gemold-backend:latest -CMD ["celery", "-A", "celery_app", "worker", "--workdir=/app/src", "--concurrency=2", "--loglevel=info"] +# OCC 并行度伸缩(OCC_THROUGHPUT 方案 A,见 docs/topics/performance/OCC_THROUGHPUT.md): +# 每个 prefork 子进程各持一个串行 OCC 通道,concurrency 即并行分析数 +# (调大时预算好每子进程内存与 PG 连接数);max-tasks-per-child 让子进程 +# 定期重启,兜底回收 OCC 超时后滞留的线程。可在 compose/.env 覆盖。 +ENV CELERY_CONCURRENCY=2 \ + CELERY_MAX_TASKS_PER_CHILD=50 + +# sh -c + exec:既支持环境变量替换,又让 celery exec 接管 PID 1 正确接收 SIGTERM +CMD ["sh", "-c", "exec celery -A celery_app worker --workdir=/app/src --concurrency=${CELERY_CONCURRENCY:-2} --max-tasks-per-child=${CELERY_MAX_TASKS_PER_CHILD:-50} --loglevel=info"] diff --git a/deploy/Dockerfile.moldinsight b/deploy/Dockerfile.moldinsight index a06c205..fd43ec6 100644 --- a/deploy/Dockerfile.moldinsight +++ b/deploy/Dockerfile.moldinsight @@ -34,7 +34,6 @@ COPY migrations/ /app/migrations/ COPY alembic.ini /app/alembic.ini COPY uploads/ /app/uploads/ -COPY html_output/ /app/html_output/ ENV PYTHONPATH=/app/src diff --git a/docker-compose.yml b/docker-compose.yml index 7fab610..9f2b870 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,8 +65,8 @@ services: LLM_TIMEOUT: ${LLM_TIMEOUT:-60} LLM_MAX_TOKENS: ${LLM_MAX_TOKENS:-2000} AUTO_MIGRATE: ${AUTO_MIGRATE:-true} - # 共享卷过渡兜底(D6/D11):主链路已改走 RustFS,本地卷仅为 - # RustFS 异常时的本地路径回退与 HTML 产物互通保留,后续批次移除 + # 共享卷过渡兜底(D6/D11):主链路已改走 RustFS。uploads 供 RustFS 异常时 + # 本地路径回退;html_output 仅作 /html 报告代理的存量兜底读(新产物不落本地) volumes: - uploads_data:/app/uploads - html_data:/app/html_output @@ -115,10 +115,13 @@ services: LLM_MODEL: ${LLM_MODEL:-gpt-4o-mini} LLM_TIMEOUT: ${LLM_TIMEOUT:-60} LLM_MAX_TOKENS: ${LLM_MAX_TOKENS:-2000} - # 与 backend 共享本地卷(过渡兜底,见 D6/D11):worker 下载回退与 HTML 产物写读 + # Celery/OCC 吞吐调优(OCC_THROUGHPUT 方案 A,默认值在 Dockerfile.celery) + CELERY_CONCURRENCY: ${CELERY_CONCURRENCY:-2} + CELERY_MAX_TASKS_PER_CHILD: ${CELERY_MAX_TASKS_PER_CHILD:-50} + # uploads_data 共享卷(D6 过渡兜底):RustFS 异常时 worker 回退本地路径下载。 + # D11 后 worker 不再写 HTML 产物(直传 RustFS 报告键),无需 html_data 卷 volumes: - uploads_data:/app/uploads - - html_data:/app/html_output depends_on: - backend restart: unless-stopped @@ -177,6 +180,7 @@ services: LLM_TIMEOUT: ${LLM_TIMEOUT:-60} LLM_MAX_TOKENS: ${LLM_MAX_TOKENS:-2000} AUTO_MIGRATE: ${AUTO_MIGRATE:-true} + # html_output 仅作 /html 报告代理的存量兜底读(D11,新产物不落本地) volumes: - uploads_data:/app/uploads - html_data:/app/html_output diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index d62d754..d786331 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -24,7 +24,7 @@ - **鉴权**:JWT Bearer(`Authorization: Bearer `)。登录:`POST /api/auth/login`(表单)/ `POST /api/auth/login/json`(JSON);受保护路由通过 FastAPI 依赖 `get_current_active_user` 注入当前用户([shared/services/auth_service.py](../src/shared/services/auth_service.py))。`SECRET_KEY` 跨进程必须一致。 - **响应形态**:现状**无统一信封包装**——各端点直接返回业务 JSON;schema 以 `openapi.json` 的 components 为准。新增接口不建议另起信封风格,保持与所在模块一致。 - **错误**:FastAPI 标准 `HTTPException` 状态码语义;业务校验优先 Pydantic 请求模型自动 422。 -- **业务路由前缀**:全部业务端点在 `/api` 下;顶层仅 `/health`(探活)、`/login`、`/users`(历史遗留入口,前端主链路用 `/api/auth/*`)。 +- **业务路由前缀**:全部业务端点在 `/api` 下;顶层仅 `/health`(探活)、`/html/{filename}`(可视化报告代理)、`/login`、`/users`(历史遗留入口,前端主链路用 `/api/auth/*`)。 ## 3. 端点总览(按域分组) @@ -47,6 +47,7 @@ | 批量分析 | `/api/batch-upload`、`/api/batch/{batch_id}`(聚合状态以 PG 为准;响应含 `current_step`;他人批次 403、不存在 404) | batch_router.py | | 任务状态 | `/api/status/{task_id}`(需登录;仅任务所有者可访问,他人/无主任务 403,不存在 404) | task_router.py | | 历史结果 | `/api/history`、`/api/history/{filename}` | history_router.py | +| HTML 报告 | `/html/{filename}`(根路径,非 `/api` 前缀;D11 代理:RustFS 报告键 `html/reports/{filename}` 直取 → 遗留 JSON 包装对象 → 本地 `html_output/` 存量兜底 → 404。已知约束:不做认证——iframe 无法携带 Authorization 头,沿用 StaticFiles 时代既定姿态) | html_report_router.py | | CAM | `/api/cam/plan`(Pydantic 请求模型;未提供的偏好回落任务持久化偏好再回落默认) | cam_router.py | | 设计 | `/api/optimize-layout`、`/api/design-cooling`、`/api/design-gating`、`/api/design-mold-system`、`/api/detect-undercuts` | design_router.py | | 成本估算 | `/api/cost-estimate` | cost_router.py | diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 1f9c2b5..2b9303e 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -49,8 +49,8 @@ Celery worker(moldinsight 异步分析链路;本地从 `src` 目录跑,与 cd src && celery -A celery_app worker --concurrency=2 --loglevel=info ``` -- `--concurrency=N` 即 OCC 并行分析数(每个 prefork 子进程持一个串行 OCC 通道,见 [topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md) 方案 A);调大时预算好每子进程内存与 PG 连接数。 -- 建议生产加 `--max-tasks-per-child=M`(如 50):子进程定期重启,兜底回收 OCC 超时后滞留的线程。 +- `--concurrency=N` 即 OCC 并行分析数:每个 prefork 子进程持一个常驻 OCC 工作进程(方案 B,见 [topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md))——每个 OCC 工作进程是独立的 Python + OCC 运行时,**N 增大时按「worker 子进程 + OCC 子进程」双份预算内存**,并预留 PG 连接数(按 celery 角色池随子进程倍增)。 +- `--max-tasks-per-child=M`(如 50):worker 子进程定期重启,连带回收其 OCC 子进程(进程级兜底,方案 A)。 前端: @@ -78,7 +78,7 @@ docker compose --profile inventory up -d # inventory 单模块栈 - **生产环境必须显式设置**:`SECRET_KEY`、`ADMIN_PASSWORD`、`DB_*`、`CORS_ORIGINS`、`RUSTFS_*`、`REDIS_PASSWORD`、`DEBUG=false`、`LOG_FORMAT=json`。 - **单数据库**:moldinsight 与 inventory 共享同一 PostgreSQL(刻意设计,不拆库)。 - **后台任务一律走 `task_dispatcher`** 与 Celery,不要在路由里 fire-and-forget。 -- **uploads/ 与 html_output/ 为运行时产物目录**,不提交、不作为配置源头。 +- **uploads/ 与 html_output/ 为运行时产物目录**,不提交、不作为配置源头。`html_output/` 自 D11 起仅作 `/html` 报告代理的**存量兜底读**(新产物直传 RustFS 报告键 `html/reports/`,worker 不再写本地卷)。 - `scripts/` 下的一次性脚本执行前先确认目标环境(多为不可逆数据迁移)。 ## 6. 排障指针 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a40ce63..72e3b91 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -100,7 +100,7 @@ geMoldInsight 已从历史单体逐步演进为“双业务模块 + 共享平台 ### P3:专项能力继续规范化 -- 铝价模拟数据增加显式 `source: "simulated"` +- ~~铝价模拟数据增加显式 `source: "simulated"`~~(2026-09-18 完成:后端响应带 `source` 字段,前端按来源渲染标注,不再硬编码交易所名) - 补专题文档的定位/边界说明 - 清理历史 checklist / tasks / report 文档的展示层级 @@ -121,7 +121,7 @@ geMoldInsight 已从历史单体逐步演进为“双业务模块 + 共享平台 **执行顺序建议**:批次 0 与批次 1 的 D6(RustFS 主链路)先行——前者是确认的安全漏洞,后者是部署根本性缺陷,两者互不依赖、改动可控。其余按批次顺序推进,每批完成同步 STATUS / TECH_DEBT / API_CONTRACT。 -> 进度:批次 0 / 1 / 2 已于 2026-09-16 完成、批次 3 / 4 已于 2026-09-17 完成,§3.1 批次计划**全部执行完毕**(遗留:D13 的 pip 全量锁文件随下次镜像构建补齐;D11 留待后续批次,正确性已由批次 1 共享卷兜底;批次 4 遗留中期项——OCC 进程池方案 B 实施待独立排期,见 [TECH_DEBT.md](TECH_DEBT.md) D10 与 [topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md))。完成明细见 [STATUS.md](STATUS.md) 与 [TECH_DEBT.md](TECH_DEBT.md) §2.5–2.8。后续优先项回到 §3 P2 / P3 与主线方向。 +> 进度:批次 0 / 1 / 2 已于 2026-09-16 完成、批次 3 / 4 已于 2026-09-17 完成,§3.1 批次计划**全部执行完毕**;批次 4 后续专项于 2026-09-18 完成——D11(HTML 报告 RustFS 单源 + `/html` 代理路由)与 **OCC 方案 B(`run_occ` 契约进程化 + 常驻进程池 kill-on-timeout)已清偿**(部署参数方案 A 一并落地,见 [TECH_DEBT.md](TECH_DEBT.md) D10 与 [topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md))。遗留:D13 的 pip 全量锁文件随下次镜像构建补齐。完成明细见 [STATUS.md](STATUS.md) 与 [TECH_DEBT.md](TECH_DEBT.md) §2.5–2.8。后续优先项回到 §3 P2 / P3 与主线方向。 --- diff --git a/docs/STATUS.md b/docs/STATUS.md index 390aaec..0d5d56f 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -3,6 +3,8 @@ > 文档定位:**唯一的「现在到哪了」**。README / AGENTS / 各主文档只链接到这里,不复制状态内容。 > 维护规则:每完整完成一个需求,**倒序在本文顶部加一条**(日期 + 主题 + 关键事实);其余主文档(架构 / 规划 / 技术债 / 部署)维护各自的"当前有效说法",本文只记录"什么时候做到了哪一步"。维护规则出处见根目录 [AGENTS.md](../AGENTS.md)。 +> 2026-09-18(**批次 4 后续专项五项完成:D11 清偿 + 部署参数 + D2 诚实标注 + CI 门禁 + OCC 方案 B 实施**:① **D11 清偿**(TECH_DEBT P2)——可视化报告 RustFS 单源化:写侧 HTMLGenerator 每任务写临时目录,`.html`/`_summary.json`/`_data.json` 三件统一裸传 RustFS 报告键 `html/reports/{filename}`(文件名寻址),`/html` StaticFiles 本地挂载删除,新增 [html_report_router.py](../src/moldinsight/api/html_report_router.py) 根路径代理(报告键直取 → 遗留 `html/{hash}.json` JSON 包装解析 → 本地卷存量兜底 → 404;URL 形状 `/html/{filename}` 不变,持久化 cavity JSON 与前端 iframe 引用零迁移);celery 服务摘除 `html_data` 卷,Dockerfile.moldinsight 删除 `COPY html_output/`(构建机陈旧报告不再进镜像);已知约束:报告路由不做认证(iframe 无法携带 Authorization 头,沿用 StaticFiles 时代既定姿态,文档已声明);顺带删除 `get_stp_file_with_data` 的死数据块(`html_content` 组装零消费方,此前每次完成任务查询白下载数 MB 正文)。② **OCC 方案 A 参数落地**——`CELERY_CONCURRENCY`/`CELERY_MAX_TASKS_PER_CHILD` 进 [Dockerfile.celery](../deploy/Dockerfile.celery) ENV + compose 透传 + `.env.example`。③ **D2 清偿**——铝价响应带 `source: "simulated"`,[HomeView.vue](../frontend/src/modules/home/HomeView.vue) 按来源渲染"模拟数据 · 参考走势"标注(原硬编码"上海期货交易所"属虚假声明),死代码 `getAluminumPrice` 删除。④ **CI 门禁**——[.gitea/workflows/ci.yml](.gitea/workflows/ci.yml) 三 job:pytest 全量 / 前端构建(含 vue-tsc)/ openapi 漂移检测(conda pythonocc 环境重导出比对;已实测 pytest 与导出均不依赖 .env)。⑤ **OCC 方案 B 实施**(TECH_DEBT D10 清偿)——`run_occ(fn, *args)` → `run_occ(op_name, payload)`,执行器由线程池替换为常驻工作进程池 [occ_process_pool.py](../src/moldinsight/services/occ_process_pool.py) + 操作注册表 [occ_worker.py](../src/moldinsight/core/occ_worker.py):超时/崩溃 terminate 换新补位、任务级超时 recover 整体重建,**残留线程泄漏根治**(进程边界回收 C++ 栈);TopoDS 形状不跨进程(`generate_cavity` 分模 + 方案 STEP 持久化导出全在子进程内,返回 export_manifest);调用点全量迁移(解析/网格/型腔/分析/倒扣/STEP 转换),删除内存形状缓存链(`_cache_export_shapes`/`get_export_shapes`/`_persist_step_exports`)、`CADExporter.export_mold_results`(零调用方)、shape_loader(→ [stp_materializer.py](../src/moldinsight/services/stp_materializer.py));回归测试 [test_occ_process_pool.py](../tests/test_occ_process_pool.py)(OCC-gated,6 例含真实盒体 STP 解析/分模端到端)。**接口变更三件套随批完成**:openapi.json 重导出(76→77 paths,新增 `/html/{filename}`)+ 前端 `gen:api` 再生 + 前端构建通过(方案 B 接口面零变化,无路由/schema 变更)。**测试基线**:**143 passed, 0 skipped**(D11 8 项 + 铝价 2 项 + OCC 进程池 6 项;基线 129 中原 2 个 skip 已随本地环境补齐 celery/alembic 转为执行)。**下一步**:回到 §3 主线 P2/P3 长期方向——D3 剩余收敛(app_factory 参数收敛、identity/platform 语义)、inventory 服务下沉、D13 pip 锁文件随下次镜像构建补齐,见 [ROADMAP.md](ROADMAP.md) §3。) + > 2026-09-17(**批次 4(架构演进)完成,§3.1 治理批次全部执行完毕**:① D3 主体清偿——891 行的旧 `shared/models/database.py`(31 模型类三类同居,已删除)按归属拆为 [shared/models/base.py](../src/shared/models/base.py)(唯一 Base)+ [shared/models/identity.py](../src/shared/models/identity.py)(身份权限 7 表)+ [moldinsight/models/](../src/moldinsight/models/)(分析域 9 表)+ [inventory/models/](../src/inventory/models/)(进销存 15 表,catalog/warehouse/trading/finance 四文件);**三条跨模块 ORM relationship(`User.stp_files` / `STPFile.user` / `STPFile.product`)经全仓核实零使用,直接删除**——跨模块桥接收敛为裸 FK 硬规则([ARCHITECTURE.md](ARCHITECTURE.md) §5.1),单模块部署 mapper 可独立配置;约 45 处 import 全量改写(含 migrations/env.py 全量注册、scripts/ 两个一次性脚本),旧模块物理删除无兼容 facade;零调用方死方法 `db_manager.create_tables` 一并删除(拆分后会静默建残缺 schema);② D10 治理——`_reset_occ_executor` 补 `cancel_futures=True`(旧实现下"慢恢复"的旧线程会继续消化旧队列,与新 executor **并发操作非线程安全的 OCC**,属数据竞争而非单纯泄漏);吞吐方案设计先行定稿 [topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md)(短期 A:celery `--concurrency` 伸缩 + `--max-tasks-per-child` 兜底,启动参数已记 [OPERATIONS.md](OPERATIONS.md) §3;中期 B:`run_occ` 契约进程化 + kill-on-timeout,待独立排期);③ 新增 [tests/test_model_ownership.py](../tests/test_model_ownership.py) 锁定归属边界(31 表全量注册 / 单模块独立 mapper 配置 / 旧模块无 facade);④ 顺手清偿 D15——[vite.config.ts](../frontend/vite.config.ts) 删除未用的 `mode` 参数,`vue-tsc -b` 恢复通过,前端生产构建链路解除阻断。**接口面零变化**(无路由与 schema 变更,openapi.json 不触发重导出)。**测试基线**:**125 passed, 2 skipped**(基线 122 + 新增归属测试 3 项)。**下一步**:治理批次收尾后回到主线方向;遗留项 D11(HTML RustFS 单源)、D13(pip 锁文件)、OCC 方案 B 独立批次。) > 2026-09-17(**批次 3(API 与代码结构)完成**:① D1 清偿——592 行 advanced_router 拆为 [design_router](../src/moldinsight/api/design_router.py) / [cost_router](../src/moldinsight/api/cost_router.py) / [machining_router](../src/moldinsight/api/machining_router.py) / [export_router](../src/moldinsight/api/export_router.py) 四个子路由(端点路径不变),请求体全量 Pydantic 模型化(`request.json()` 手动解析退役,校验统一 422);② 路由装载失败显式化:`ROUTE_MODULES` 清单 + [route_registry](../src/moldinsight/api/route_registry.py),失败经 `/api/health` 呈现 `degraded` 并列出清单(`pythonocc` 改真实探测),DEBUG 下 fail fast——此前失败仅 WARNING 后静默跳过,进程带病启动不可感知;③ 纯 Python 重计算端点(设计/加工/CAM 打包)统一 `asyncio.to_thread` 投放线程池,不再阻塞事件循环(OCC 仍走单线程 executor,D10 留批次 4);④ `StorageIntegrationService`(867 行)按职责拆为 [task_storage](../src/moldinsight/services/task_storage_service.py) / [analysis_storage](../src/moldinsight/services/analysis_storage_service.py) / [file_history](../src/moldinsight/services/file_history_service.py) 三服务,无调用方死代码 `log_user_activity` 删除;⑤ D14 收尾清偿——`MAX_FILE_SIZE` 接线生效(默认上限 50MB→100MB,以 .env 为准)、celery_app 复用 `Settings.redis_url`(连接串唯一拼装点)。**连带修复**:管理员重置密码改 JSON body `{ new_password }`(原裸 str 参数被解析为 query param,前端两个调用点均发 body,功能端到端断裂)+ [UsersView.vue](../frontend/src/modules/users/UsersView.vue) 同步;Dockerfile.celery 的 FROM 对齐 `gemold-backend:latest`(此前引用不存在的 tag,干净环境 celery 镜像必构建失败)。**接口变更三件套随批完成**:openapi.json 重导出(76 paths)+ 前端 `gen:api` 再生。**连带发现**:`npm run build` 因 vite.config.ts 既有 TS6133 失败(与本项目改动无关,登记 D15)。**测试基线**:**122 passed, 2 skipped**(新增 4 个测试文件共 17 项:[test_advanced_split_contract](../tests/test_advanced_split_contract.py) / [test_route_load_status](../tests/test_route_load_status.py) / [test_config_governance](../tests/test_config_governance.py) / [test_auth_password_reset](../tests/test_auth_password_reset.py);skips 为 alembic / celery 缺失环境)。**下一步**:批次 4(架构演进:共享 ORM 拆分、OCC 吞吐方案,见 [ROADMAP.md](ROADMAP.md) §3.1)。) diff --git a/docs/TECH_DEBT.md b/docs/TECH_DEBT.md index 9445c3f..e267e7e 100644 --- a/docs/TECH_DEBT.md +++ b/docs/TECH_DEBT.md @@ -173,30 +173,33 @@ ~~原现状 / 影响~~:各存储方法内部自行 commit,型腔保存失败留半成品数据且任务仍 completed。 -### D10. OCC 全局单线程串行 + 超时重建泄漏线程 —— 泄漏治理已落地,吞吐方案设计先行(2026-09-17,批次 4) +### D10. OCC 全局单线程串行 + 超时重建泄漏线程 —— 已清偿(2026-09-18,方案 B 实施) -已完成: -- `_reset_occ_executor` 补 `cancel_futures=True`:排队任务随重建丢弃——旧实现下"慢恢复"的旧线程会继续消化旧队列,与新 executor 并发操作非线程安全的 OCC;修复后残留收敛为"运行中线程滞留 1 个"(C++ 栈 Python 层不可杀,属客观边界) -- 吞吐与隔离方案定稿:[topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md)——短期方案 A(celery `--concurrency` 伸缩 + `--max-tasks-per-child` 进程回收兜底,零新代码,启动参数见 [OPERATIONS.md](OPERATIONS.md) §3);中期方案 B(`run_occ` 改操作名+payload 契约、常驻 OCC 进程池 kill-on-timeout 根治泄漏,待独立批次) +**方案 B(常驻 OCC 进程池,kill-on-timeout 根治泄漏)已实施**(2026-09-18): +- `run_occ(fn, *args)` → `run_occ(op_name, payload)`;执行器由进程内线程池替换为常驻工作进程池 [occ_process_pool.py](../src/moldinsight/services/occ_process_pool.py) + 操作注册表 [occ_worker.py](../src/moldinsight/core/occ_worker.py)(新增;详见 [topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md) §5) +- 超时/崩溃 = terminate() 换新补位——**残留线程泄漏根治**(C++ 栈由进程边界回收);OCC segfault 不再波及 API/worker 主进程 +- 调用点全部迁移(解析/网格/型腔/分析/倒扣/STEP 转换),TopoDS 形状不跨进程(`generate_cavity` 的方案形状 STEP 导出改在子进程内持久化,返回 export_manifest) +- 顺带删除:内存形状缓存链(`_cache_export_shapes` / `get_export_shapes` / `_persist_step_exports`)、`CADExporter.export_mold_results`(零调用方)、shape_loader(→ [stp_materializer.py](../src/moldinsight/services/stp_materializer.py)) +- 回归测试:[tests/test_occ_process_pool.py](../tests/test_occ_process_pool.py)(OCC-gated,6 例含真实盒体 STP 端到端) + +**方案 A 部署参数落地**(2026-09-18):`CELERY_CONCURRENCY` / `CELERY_MAX_TASKS_PER_CHILD` 进 [Dockerfile.celery](../deploy/Dockerfile.celery) + compose + `.env.example`(`--max-tasks-per-child` 仍保留为进程回收兜底)。 保留为已知约束(非待修缺陷): - 单进程内 OCC 串行是正确性要求(OCC 非线程安全),吞吐扩展走多进程(方案 A/B) -- 线程级超时的滞留线程由进程边界回收,根治依赖方案 B 落地 +- 每个操作从 STP 原件重新加载形状(STEP 重载成本秒级)——进程隔离的设计取舍,见 OCC_THROUGHPUT §1.2/§5 -优先级:**P3**(中期方案 B 实施前维持观察) +优先级:~~**P3**~~ **已清偿** -### D11. HTML 报告本地磁盘与 RustFS 双写双读 +### D11. HTML 报告本地磁盘与 RustFS 双写双读 —— 已清偿(2026-09-18) -现状: -- 可视化 HTML/摘要同时写本地 `html_output/`(/html 静态挂载)与 RustFS +修复内容: +- **写侧**:可视化产物不再落节点本地 `html_output/`——HTMLGenerator 每任务写临时目录([processing_service.py](../src/moldinsight/services/processing_service.py)),`.html` / `_summary.json` / `_data.json` 三件统一裸传 RustFS 报告键 `html/reports/{filename}`(文件名寻址,同源同秒重复分析即覆盖刷新);`HTMLFile` 表仅存元数据 +- **读侧**:`/html` StaticFiles 挂载删除,新增代理路由 [html_report_router.py](../src/moldinsight/api/html_report_router.py)(挂根路径保持 URL 形状——持久化 cavity JSON 与前端 iframe 均引用 `/html/{filename}`):RustFS 报告键直取 → 遗留 `html/{hash}.json` JSON 包装解析 → 本地卷存量兜底 → 404,防路径穿越(单段文件名校验) +- **部署**:celery 服务摘除 `html_data` 卷(不再写本地);Dockerfile.moldinsight 删除 `COPY html_output/`(构建机陈旧报告不再烤进镜像) +- **已知约束**(沿用 StaticFiles 时代既定姿态,非新引入):报告路由不做认证——iframe 无法携带 Authorization 头 +- 回归测试:[tests/test_html_report_router.py](../tests/test_html_report_router.py)(8 例:四链路命中、新旧格式记录区分、媒体类型、404、穿越拒绝) -影响: -- 多副本下 /html 命中结果取决于负载均衡,跨副本文件不共享;同一份报告两套来源 - -建议: -- 统一 RustFS 为唯一来源,本地仅作按需缓存 - -优先级:**P2** +~~原现状 / 影响~~:可视化 HTML/摘要同时写本地与 RustFS,多副本下 /html 命中结果取决于负载均衡,跨副本文件不共享。 ### D12. 应用启动时自动执行 alembic 迁移 diff --git a/docs/topics/performance/OCC_THROUGHPUT.md b/docs/topics/performance/OCC_THROUGHPUT.md index fbb745e..27023b2 100644 --- a/docs/topics/performance/OCC_THROUGHPUT.md +++ b/docs/topics/performance/OCC_THROUGHPUT.md @@ -10,9 +10,10 @@ ### 1.1 运行时事实 -- 所有 OCC 操作(STEP 解析 / 布尔运算 / 三角化 / 倒扣检测等)统一经 [processing_service.py](../../../src/moldinsight/services/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)。 +- 所有 OCC 操作(STEP 解析 / 布尔运算 / 三角化 / 倒扣检测 / STEP 转换等)统一经 [processing_service.py](../../../src/moldinsight/services/processing_service.py) 的 `run_occ(op_name, payload)` 投入 **常驻 OCC 工作进程池**([occ_process_pool.py](../../../src/moldinsight/services/occ_process_pool.py),默认 1 进程 = 1 串行通道)——OCC 非线程安全,通道内串行是正确性要求,不是实现偷懒。 +- 操作在 [occ_worker.py](../../../src/moldinsight/core/occ_worker.py) 以注册表形式实现(parse_stp / generate_mesh / generate_cavity / analyze_mold_design / detect_undercuts / convert_component_step 等);输入输出全部是**文件路径 + 普通字典**,TopoDS_Shape 不跨进程传输(方案 B 硬约束,见 §1.2)。 +- Celery worker 为 prefork 模式,`processing_service` 是模块级单例:**每个 worker 子进程各持一个常驻 OCC 工作进程**。因此 OCC 并行度 = worker 子进程数,与 web 进程数无关(web 侧 `run_occ` 仅服务于轻量同步调用,如倒扣检测)。 +- 超时/崩溃 = `terminate()` 该工作进程并换新补位——进程边界干净回收,无线程滞留。 ### 1.2 硬约束(决定方案边界) @@ -61,10 +62,25 @@ | 阶段 | 动作 | 状态 | |---|---|---| -| 短期 | 方案 A:`--concurrency` 伸缩 + `--max-tasks-per-child` 兜底回收;`cancel_futures=True` 修复重建并发风险 | ✅ 代码侧 2026-09-17 完成;部署参数随下次 compose/镜像评审落地 | -| 中期 | 方案 B:`run_occ(op_name, payload)` 接口演进 + 常驻进程池,kill-on-timeout 根治泄漏 | 待排期(独立批次,工作量集中在调用点迁移与序列化设计) | +| 短期 | 方案 A:`--concurrency` 伸缩 + `--max-tasks-per-child` 兜底回收;`cancel_futures=True` 修复重建并发风险 | ✅ 部署参数 2026-09-18 落地(`CELERY_CONCURRENCY` / `CELERY_MAX_TASKS_PER_CHILD` 进 Dockerfile.celery + compose + .env.example) | +| 中期 | 方案 B:`run_occ(op_name, payload)` 接口演进 + 常驻进程池,kill-on-timeout 根治泄漏 | ✅ 2026-09-18 实施完成(见 §5;回归测试 [tests/test_occ_process_pool.py](../../../tests/test_occ_process_pool.py)) | | 长期 | 方案 C:sidecar,仅在出现独立伸缩需求时启动 | 暂不启动 | -## 4. 本次已落地的缓解(2026-09-17,批次 4) +## 4. 已落地的缓解(2026-09-17,批次 4) `_reset_occ_executor` 的 `shutdown(wait=False)` 补 `cancel_futures=True`。这不只是卫生问题:旧实现下旧 executor 的**排队任务不会消失**——若挂死线程后来"慢恢复",旧线程会继续消化旧队列,与新 executor **并发操作非线程安全的 OCC**(数据竞争 / 崩溃风险)。补参后排队任务即被丢弃,残留问题收敛为"运行中线程滞留 1 个",由方案 A 的进程回收兜底。 + +## 5. 方案 B 实施记录(2026-09-18) + +**接口**:`run_occ(fn, *args)` → `run_occ(op_name, payload)`;执行器由进程内线程池替换为常驻进程池。 + +- **新增** [occ_process_pool.py](../../../src/moldinsight/services/occ_process_pool.py):`OccProcessPool`(默认 size=1)。每个 `_OccWorker` 是一个 spawn 出的常驻子进程 + 双工管道 + 独立 `asyncio.Lock`(通道串行);阻塞收发经 `asyncio.to_thread` 不卡事件循环。操作超时或进程死亡 → `terminate()` + 换新补位;任务级整体超时(`process_file_with_storage` 外层 wait_for)→ `recover()` 整体重建。`shutdown()` 供应用退出/测试清理。 +- **新增** [occ_worker.py](../../../src/moldinsight/core/occ_worker.py):操作注册表 + `worker_main` 消息循环。OCC 模块在 handler 内惰性导入(父进程 pip 环境无 OCC 也可 import),进程内单例缓存(parser/planner/analyzer/mesh_gen 等)。全部操作输入输出为**文件路径 + 普通字典**,TopoDS 不跨进程。 +- **调用点迁移**(processing_service): + - `parse_stp` = 原 load_step_file + analyze_geometry 两步合一(形状在子进程内即生即用) + - `generate_mesh` / `analyze_mold_design` / `detect_undercuts` / `convert_component_step` 同名对位 + - `generate_cavity` = 分模 + 方案形状持久化 STEP 导出全在子进程内;`_export_shapes`(TopoDS)不再回主进程,返回 export_manifest(与旧 `_persist_step_exports` 结构一致,主进程原样存 export_artifacts) + - 旧 `_cache_export_shapes` / `get_export_shapes` / `_persist_step_exports` / `_export_shapes_cache` 及线程 executor / `_reset_occ_executor` 全部删除(跨进程本就不存在内存形状缓存,export_router 相应移除 `export_mold_results` 内存分支) + - [shape_loader.py](../../../src/moldinsight/services/shape_loader.py) → [stp_materializer.py](../../../src/moldinsight/services/stp_materializer.py):只把 STP 原件落盘临时文件,OCC 解析交给子进程操作 +- **成本确认**:每个 spawn 子进程首次操作需 import OCC(秒级);进程常驻后后续操作复用缓存实例。每个操作从 STP 原件重新加载形状(STEP 重载成本,见 §1.2)——原线程方案跨步骤共享 shape 的内存优势让位于进程隔离,符合方案 B 设计取舍。 +- **测试**:[tests/test_occ_process_pool.py](../../../tests/test_occ_process_pool.py)(OCC-gated,6 例):spawn+管道往返 / 未知操作错误回传 / 子进程异常浮出 / 超时换新补位 / 真实盒体 STP 解析端到端 / generate_cavity 分模+STEP 落盘端到端。 diff --git a/frontend/src/modules/home/HomeView.vue b/frontend/src/modules/home/HomeView.vue index ba2ca58..8d37d60 100644 --- a/frontend/src/modules/home/HomeView.vue +++ b/frontend/src/modules/home/HomeView.vue @@ -9,9 +9,9 @@
- 🪙 铝金属价格 (SHFE) + 🪙 铝金属价格
- 数据来源: 上海期货交易所 | 更新于 {{ aluminum.date }} + {{ aluminumSourceLabel }}
@@ -78,7 +78,7 @@