Files
DCTS/docs/tlusty&synspec收敛性判断.md
fmq cd370d88e7 feat(all): 任务引擎双阶段解耦、僵尸涡旋修复、动态 CPU 配额与前端详情页重构
将 TLUSTY/SYNSPEC 拆为各自独立的 enabled/policy/strategies 阶段,
以策略链自动弹栈取代单级 seed_step 布尔回退;定向修复 2026-08-02
僵尸任务涡旋事故;新增节点并发配额热调;前端详情页从 1412 行巨型
视图拆为薄控制器 + detail 子模块,并补齐工具层与单测。

引擎与调度(task_engine_decoupling_design.md)
- models.rs: 新增 StagePolicy / EngineStageConfig / TaskSpec 阶段字段、
  normalize_compat() 校正旧版在途消息策略链、failed_stage 归因
- scheduler.rs: resolve_dispatchable_chain 派发门控、
  trigger_strategy_fallback 按 failed_stage 精确弹栈;启动期
  force_recompute/skip_converged(默认)/skip_failed 三策略
- db.rs: tasks 表 +7 列持久化阶段配置;终态守卫
  (mark_grid_point_running 仅 pending/queued→running;
  record_task_report 拒绝迟到失败翻黑 converged);策略弹栈快照

僵尸涡旋修复(runbook-20260802-zombie-vortex-fix.md)
- 全链路跨库活性交叉校验:派发/claim/孤儿回收/回退统一查 MQ 队列活性,
  活则放行、死则清僵尸,结构性消除"每点重复派发"
- stop/重启卫生:清队列同步 delete_tasks_by_ids,杜绝遗留 pending 行
- report_task: 幂等吸收 + 409 区分迟到冗余结果,仅 state_changed 时回退
- MQ: NULL workflow_name 回填 __legacy__、requeue 后迟到上报被 403 竞态修复

动态 CPU 配额(dynamic_cpu_slots_design.md)
- admin.rs: POST /admin/nodes/:id/quota(Option<Option<i32>> 区分
  缺字段/显式 null);nodes 表 +admin_max_slots
- worker.rs: effective_max_slots = min(admin, physical),心跳下发原子生效

科学产物保全(tlusty_result_artifacts.md)
- runner.rs: SYNSPEC 启动前快照 fort.12/fort.14 → .bfac/.emflux 防覆盖
- 半失败点(大气收敛+光谱失败)改判 Failed 并写入 note;仅 SYNSPEC
  场景不再恒判失败;撤销归档 LRU 200 上限改为永久保留
- executor.rs: 透传 synspec_params 数值参数(此前固定 None)

前端(dashboard/)
- workflowDetail.js 1412→328 行,拆出 views/detail/{ctx,overview,
  pointsTable,parSets,pointPanel}.js,AbortController 治理监听/请求生命周期
- 删除 wfActions.js,新增 wfEnginePanel.js(双阶段三维配置编辑面板)
- 新增 utils/{errors,format,icons,polling,yamlStage}.js 纯函数模块
- 路由级动态 import 代码分割;节点配额三点菜单 + Modal 管理
- 首次引入 node:test 单测(format/polling/yamlStage/psCache,644 行)
- 系统性补齐 a11y:skip-link、ARIA、Tab 键盘漫游、toast 关闭、退出动画

文档与工具
- 新增 6 篇设计/调研:引擎解耦、动态配额、涡旋 runbook、
  光谱正确性分析、收敛判断、产物归档
- PIPELINE/design/api/database 等协同重写为分布式 C/S 架构口径
- scripts/fetch_results.sh 跨节点产物备份;import_results 按 cno 升序导入
- workflows/sdB_cno.yaml: 新增 tlusty/synspec_stage 配置块,修正 wstart 笔误
2026-08-04 23:40:52 +08:00

361 lines
23 KiB
Markdown
Raw Permalink 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.
# 任务失败判定逻辑调研:Tlusty&Synspec收敛性判断LUSTY / SYNSPEC 退出码可靠性与兜底机制
> 调研日期:2026-08-04
> 范围:`crates/common/src/runner.rs`、`crates/node/src/reporter.rs`、`crates/common/src/conv_check.rs`
> 的任务成败判定逻辑,对照 TLUSTY 208 / SYNSPEC 54 源码(`tlusty/tlusty208.f`、`synspec/synspec54.f`
> 与 gfortran 运行时退出码行为。
> 方法:源码级静态分析 + gfortran 实测(退出码、运行时错误)+ 三方独立复核。
> 本文仅记录调研结论,不含修复实现。
---
## 0. TL;DR
调度系统通过子进程退出码 `rc` 判定 TLUSTY / SYNSPEC 成败,但 **gfortran 下裸 `STOP` 默认返回 rc=0**,而两个 Fortran 程序的几乎所有错误路径用的都是裸 `STOP``call quit`(内部亦为裸 `stop`)。因此 **rc 本身不可靠**
当前判定能"基本不出错",靠的是 rc 之外的兜底:
- **TLUSTY**`rc==0 && fort.7 存在` 才进入 `check_fort9`max_relc < chmax+ `atmosphere_has_nan` 双重否决。这三重条件对"显式发散 STOP"和"硬崩溃(信号)"覆盖可靠。
- **SYNSPEC****仅靠 `synspec_rc==0`,下游零内容校验**,是当前系统最大的科学正确性风险。
复核(含 gfortran 运行时错误实测)发现两类此前未点出的风险:
1. **gfortran 运行时错误默认返回 rc=0 且静默**(实数 IEEE 异常、数组越界),生产编译未启用 `-fcheck`/`-ffpe-trap`
2. **fort.9 缺失分支无条件判收敛**,且写入虚构的 `best_max_relc=0.0`,会污染种子选择并向相邻网格点扩散。
修复优先级收敛为:SYNSPEC `.spec` 内容校验 > 收紧 `atmosphere_has_nan` 阈值 > 修正 fort.9 缺失分支 > 补齐 SOLVES/RYBSOL 发散路径归因。
---
## 1. 现状:失败判定的四层结构
node 端任务失败判定从底向上分四层:
```
execute_task (executor.rs) ← 前置 IO 失败 → 直接返回 Err
└─ run_model_with_timeout ← 计算失败 → 产出 ModelSummary(converged 字段)
└─ derive_report_status ← 半失败判定 → Completed / Failed
└─ infer_failed_stage ← 失败归因 → tlusty / synspec
```
### 第一层:`execute_task`executor.rs:11)—— 前置 IO 失败
直接返回 `Err`、进不到计算:
| 失败情形 | 处理 | 行号 |
| -------------------------------------- | ----------------------------------- | ------------------- |
| 原子数据文件拉取失败 | `warn` 但**继续**(非致命) | executor.rs:46-50 |
| 种子大气下载失败(HTTP 非 2xx / 网络) | `bail!` 返回 Err | executor.rs:114-137 |
| 沙盒目录创建失败 | `?` 返回 Err | executor.rs:57 |
返回 Err 时,worker.rs:456 转为 `String`reporter 走 `Err(e)` 分支(reporter.rs:78-89):
`status=Failed, converged=false, error_message=e`
### 第二层:`run_model_with_timeout`runner.rs:232)—— 计算过程收敛判定
核心,`final_converged` 在此累积,分三块:
**① TLUSTY 阶段链收敛判定(runner.rs:402-469**
每个阶段执行后,只有 `rc==0 && fort.7 存在` 才进入收敛检查,否则该阶段 `converged=false`
```rust
// runner.rs:402
if rc == 0 && fort7.is_file() {
if fort9.is_file() {
let res = check_fort9(&fort9, eff_chmax);
stage_summary.converged = res.converged; // max_relc < chmax
} else {
// NITER=0 grey start without fort.9 ← 见 §4 漏洞 2
stage_summary.converged = true;
stage_summary.best_max_relc = Some(0.0);
}
} else {
stage_summary.note = Some(format!("tlusty rc={} or missing fort.7", rc));
}
```
`check_fort9` 的收敛判据(conv_check.rs:163):
```
converged = max_relc.is_finite() && max_relc < chmax
```
其中 `max_relc` 是 fort.9 **最后一次迭代**中所有深度点里**最大的相对修正绝对值**。关键防御:
- `chmax ≤ 0 或非有限` → 直接判失败并报错(conv_check.rs:58,防误配)
- fort.9 无有效迭代数据 → `max_relc=Infinity`,判失败(conv_check.rs:127
- 发散值(NaN/Inf/无-E 记数法 `-1.35+118`)→ `is_finite()=false` → 判失败(conv_check.rs:160
- fort.9 缺失但 rc=0 → 视为"NITER=0 grey start"**判收敛**runner.rs:419)—— 见 §4 漏洞 2
阶段间联动:若某阶段 `require_converged=true` 且未收敛 → `break`,中止后续所有阶段(runner.rs:463)。
`final_converged` 取最后执行阶段的值。
**② 大气 NaN 强制否决(runner.rs:488-491**
```rust
let atmo_has_nan = atmosphere_has_nan(&final_7);
if atmo_has_nan { final_converged = false; }
```
`atmosphere_has_nan`conv_check.rs:190)检测大气文件里 `NaN`/`Inf`/`********`Fortran 字段溢出),
**超 10% 行命中**即判无效。此步会推翻前面收敛结论 —— 见 §4 漏洞 3 的阈值盲区。
**③ SYNSPEC 阶段(runner.rs:498-603**
SYNSPEC 不直接改 `final_converged`,记录到 `synspec_rc` / `synspec_error`
- `fort.8`(大气输入)复制失败 → 记 `synspec_error`,跳过(runner.rs:508
- `fort.55`(控制卡)写入失败 → 记 `synspec_error`,跳过(runner.rs:531
- synspec 进程 rc≠0 或超时(上限 `min(600, timeout_sec)`)→ 记 `synspec_rc`runner.rs:568-575
**仅 SYNSPEC 场景**(tlusty 关闭)的收敛重判(runner.rs:611-616):
`final_converged = (synspec_rc == 0)`
### 第三层:`derive_report_status`reporter.rs:38)—— 半失败 = 失败
```rust
let synspec_failed = synspec_error.is_some() || matches!(synspec_rc, Some(rc) if rc != 0);
if s.converged && !synspec_failed { TaskStatus::Completed } else { TaskStatus::Failed }
```
旧逻辑 `if converged { Completed }` 只看大气收敛,TLUSTY 收敛但 SYNSPEC 失败时 `converged` 仍为 true
→ 误报成功 → 服务端吸收为终态。新逻辑:**大气好 + 光谱坏 = 整体失败**,让服务端弹 synspec 链重试。
`converged` 字段仍按大气真实状态上报(为 true 时服务端仍存 .7 作种子)。
### 第四层:`infer_failed_stage`reporter.rs:15)—— 失败归因
| 条件 | 归因 |
| ---------------------------------------- | -------------------- |
| `converged=false` + tlusty 启用 | `tlusty`(含级联) |
| `converged=false` + tlusty 关闭 | `synspec` |
| `converged=true` 但 synspec 出错/rc≠0 | `synspec` |
| 全部成功 | `None` |
---
## 2. `rc==0` 的真实含义与不可靠性
### 2.1 gfortran 退出码实测(实证)
`/tmp` 用最小 Fortran 程序实测(gfortran 15.2.0`-fno-automatic -O3` 等价于生产编译环境):
| Fortran 语句 / 错误类型 | 触发方式 | 退出码 rc | runner 能否抓住 |
| ---------------------------------- | ------------- | --------------------------- | ----------------------- |
| 裸`STOP` | `stop` | **0** | ❌ 靠 fort.9 兜 |
| `STOP 'msg'` | `stop 'x'` | **0** | ❌ 靠 fort.9 兜 |
| `STOP 1`(带数字) | `stop 1` | 1 | ✅(TLUSTY 未用此形式) |
| 自然`END` | 主程序结束 | 0 | — |
| 实数除零`1.0/0.0` | IEEE 默认 | **0** | ❌ 产 Inf,无陷阱 |
| `0.0/0.0` | IEEE 默认 | **0** | ❌ 产 NaN |
| 实数溢出`1e30*1e30` | IEEE 默认 | **0** | ❌ 产 Inf |
| `sqrt(-1)` / `log(0)` | IEEE 默认 | **0** | ❌ 产 NaN/-Inf |
| **整数除零** `1/0` | SIGFPE | **136**128+8 | ✅ |
| **数组越界(默认)** | 无`-fcheck` | **0,静默写越界内存** | ❌ |
| 数组越界(`-fcheck=bounds`) | 启用检查 | 2 | ✅ |
| READ 遇 EOF(无`end=` 守卫) | 运行时 | **2** | ✅ |
| READ 类型不匹配(无`err=` 守卫) | 运行时 | **2** | ✅ |
| 栈溢出 / 段错误 | SIGSEGV | **139**128+11 | ✅ |
> 生产编译命令(deployment.md:86,90)为 `gfortran -fno-automatic -O3`**未启用** `-fcheck=bounds` 与
> `-ffpe-trap=invalid,zero,overflow`。因此表中所有 rc=0 的静默失败在生产二进制中真实存在。
### 2.2 核心矛盾
**Fortran 的 `STOP` 语句默认返回 0**,而 TLUSTY/SYNSPEC 几乎所有错误处理用的都是裸 `STOP``call quit`(内部裸 `stop`)。导致**进程异常终止的退出码 = 正常完成的退出码 = 0**。
- TLUSTY `subroutine quit`tlusty208.f:29946-29955)末行裸 `stop`
- SYNSPEC `subroutine quit`synspec54.f:12014-12021)末行裸 `stop`
- 两者全文 80+ 处 `STOP`/`call quit`,错误路径几乎全走裸 `STOP`
---
## 3. 源码级失败路径分析
### 3.1 TLUSTY:三重兜底对"显式发散"覆盖可靠
**主循环结构(tlusty208.f:1-62):**
```fortran
10 ITER=ITER+1
CALL RESOLV 形式解;内部 CALL OUTPUT(:3807/:3828) 每次迭代写 fort.7
IF(LFIN) GO TO 20 上一轮设了 LFIN 就跳出
IF(IACC.GT.0) CALL ACCEL2
IF(IFRYB.EQ.0) THEN
IF(NN.GT.MSMX) THEN
CALL SOLVE 含发散 STOP(:14703)
ELSE
CALL SOLVES 含发散 STOP(:15050)
END IF
ELSE
CALL RYBSOL 含发散 STOP(:47585-47590) 修正点:第三条路径
END IF
GO TO 10
20 STOP 主程序正常结束,裸 STOP rc=0
END
```
**关键事实:**
- **fort.7 每次迭代都写**`CALL OUTPUT`tlusty208.f:3807, 3828)在 RESOLV 内,每次迭代执行。
- **发散 STOP 在三处平行求解器内**(不止 SOLVE):
- `SOLVE` :14701-14706`CHMX > 1.D16` → 裸 STOP
- `SOLVES` :15047-15051(同逻辑)
- `RYBSOL`/RYBCHN :47585-47590(同逻辑,`IFRYB≠0` 时走此路)
三处发散检查都在 `PRCHAN`/RYBCHN 写完 fort.9 **之后**,故发散 STOP 时 fort.7 与 fort.9 均已落盘。
- **达到 NITER 未收敛**`LFIN=ABS(CHMX).LE.CHMAX.OR.ITER.GE.NITER`(:14728),走主程序正常 ENDrc=0fort.9 含未收敛的 max_relc。
**TLUSTY 失败场景对照:**
| 场景 | Fortran 处理 | rc | 写 fort.7 | 写 fort.9 | runner 判定 | 是否合适 |
| --------------------- | --------------------------------- | ----------- | --------- | --------- | ---------------------------------------- | -------- |
| 正常收敛 | 主程序 END | 0 | ✅ | ✅ | check_fort9 → converged | ✅ |
| 达到 NITER 未收敛 | `LFIN`END | 0 | ✅ | ✅ | check_fort9 → max_relc≥chmax → 未收敛 | ✅ |
| 数值发散 CHMX>1e16 | SOLVE/SOLVES/RYBSOL 内裸 STOP | **0** | ✅ | ✅ | check_fort9 → 发散值未收敛 | ✅ |
| 输入错误(call quit | 裸 stop | 0 | ❌ | ❌ | rc=0 但 fort.7 缺失 → 失败 | ✅ |
| temp 越界 | `stop 'partf; temp...'`(:44624) | **0** | ❌ | ❌ | fort.7 缺失 → 失败 | ✅ |
**结论**rc 虽不可靠,但 `rc==0 + fort.7 存在 + check_fort9 + atmosphere_has_nan` 四重条件对"显式 STOP 类发散"和"硬崩溃(信号)"覆盖可靠 —— 这是一个**巧合但有效**的设计。
### 3.2 SYNSPEC:仅靠 rc,基本裸奔
**主程序结构(synspec54.f:1-174):** 主循环 `:10` 标签到 `:150 IF(IBLANK.LT.NBLANK) GO TO 10`
`CALL OUTPRI`:143)写谱输出(unit 7 谱 / 17 连续谱 / 16 部分等宽 / 6 诊断)且**在循环内、多次追加写**(无 REWIND)。
主程序以自然 `END`(:174)结束,正常路径 rc=0。
**call quit / STOP 相对 OUTPRI 的时序分类(复核产出):**
| 归类 | 数量 | 代表行号 | 风险 |
| ------------------------------------------------------------------ | -------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------ |
| **OUTPRI 之前**(START 阶段输入校验) | 多数 | :726, :808, :937, :1062, :1856, :2229, :12168, :12224, :12252, :12323, :12324, :16745, :19455, :19601 | 低 —— 谱未生成,靠"文件缺失"兜得住 |
| **OUTPRI 之后 / 之间**(主循环内 RESOLV/RESOLW/OPAC 调用链) | **6 处** | **:4203, :8208, :9935, :18863, :18865, :19891** | 高 —— 脏数据风险 |
| 死代码 / 坏码 | 3 处 | :17414`stop23`(坏码)、:17851 CHCKAB(调用被注释)、:18383 MOLSET(从未调用) | 无 |
**脏数据风险的精确触发条件**:仅在 **NBLANK>1 或分子/集合重处理**synspec54.f:152-162)导致前序迭代已写 fort.7 之后,后续迭代在 INISET/OPAC/RESOLW/SETRAY 等处中途 `call quit` → 残缺谱已落盘 + rc=0 → 被误判成功。**单集合单次运行(NBLANK=1)所有 quit 都在首次 OUTPRI 之前,无脏数据**。
**SYNSPEC 失败场景对照:**
| 场景 | Fortran 处理 | rc | runner 判定 | 是否合适 |
| ---------------------------------------- | ------------------ | ----------- | --------------------------------------------------------------- | --------------- |
| 正常完成 | 主程序 END:174 | 0 | synspec_rc=0 → 成功 | ✅ |
| 输入数据错误(OUTPRI 前 quit | 裸 stop | **0** | 谱未生成 → .spec 缺失(runner.rs:579 is_file 不成立)→ 未产谱 | ⚠️ 靠文件缺失 |
| 计算中途错误(OUTPRI 后 quit,多轮场景) | 裸 stop | **0** | synspec_rc=0 →**误判成功** | ❌ 严重 |
---
## 4. 复核发现的漏洞(按严重度排序)
> 以下为源码级静态分析与 gfortran 实测、经三方独立复核确认的风险。
### 漏洞 1(最严重):SYNSPEC 产出的 `.spec` 全链路零内容校验
runner 对 SYNSPEC 产出(runner.rs:578-599**只有 `is_file()` 存在性检查 + rename/copy**,无任何内容校验:
```rust
if model_dir.join("fort.7").is_file() {
let _ = tokio::fs::rename(..., ... ".spec").await; // 不查内容
}
```
下游全部不拦截:
- `executor.rs:191-215`:只读 `.7`(种子),从不碰 `.spec`
- `result_filter.rs:64-110`:纯文件名后缀白名单,`spec` 在白名单内(result_filter.rs:28),0 字节 `.spec` 与干净的 50MB `.spec` **同等归档**
- `derive_report_status`reporter.rs:38-45):只看 `synspec_rc``synspec_error`,不看产物。
完整穿透链:SYNSPEC `call quit`rc=0+ 写出含 NaN/Inf/0 字节的 `.spec``is_file()` 成立 → rename 成 `.spec`
`final_converged = rc==0`runner.rs:612)→ `synspec_failed=false`reporter.rs:39)→ `Completed`
脏谱归档、服务端吸收为终态、不重试。**没有任何一道防线检查 `.spec` 内容。**
### 漏洞 2(严重):fort.9 缺失分支无条件判收敛 + 虚构 best_max_relc=0.0
runner.rs:417-421,当 `rc==0 && fort.7 存在` 但 fort.9 **不存在**时:
```rust
} else {
// NITER=0 grey start without fort.9
stage_summary.converged = true; // 无条件判收敛
stage_summary.best_max_relc = Some(0.0); // 虚构"完美收敛"值
stage_summary.note = Some("NITER=0 grey start".to_string());
}
```
- 注释"NITER=0 grey start"是**断言而非检查**,代码未校验本次 stage 的 `niter` 配置是否真为 0。
- `best_max_relc = Some(0.0)` 进入 `final_max_relc`runner.rs:458-459)→ 上报(reporter.rs:73)。若服务端用 max_relc 选种子,会把误判模型当"完美收敛"优先选作下一轮种子,**污染向相邻网格点扩散**。
- 合法 grey start`lte`/`nc` 阶段,`require_converged=false`runner.rs:23/38)与致命崩溃共用同一段无校验代码,无法区分。
**触发条件**rc==0gfortran 裸 STOP 或运行时静默错误)+ fort.7 已写 + fort.9 缺失。严重度比漏洞 1 低,因为多数发散情况 fort.9 会写出含发散值的行,反而触发 `check_fort9``is_finite()=false` 判不收敛。
### 漏洞 3(严重):gfortran 运行时错误默认 rc=0 且静默
见 §2.1 实测表。在生产编译(无 `-fcheck`/`-ffpe-trap`)下:
- **实数 IEEE 异常**(除零、溢出、NaN 产生)→ rc=0,无陷阱,静默产出 NaN/Inf。
- **数组越界**(无 `-fcheck=bounds`)→ rc=0,**静默写越界内存**,可能产出物理上错误但无 NaN 标记的大气。
这两类若发生在 RESOLV 写完 fort.7tlusty208.f:3828)之后、SOLVE 写 fort.9 之前 → fort.7 在、fort.9 不在 → 命中漏洞 2 → 误判收敛。`atmosphere_has_nan` 能否兜住取决于越界是否恰好产生 NaN/Inf 字面值;若只是静默改写相邻内存,此防线无效。
### 漏洞 4(中):`atmosphere_has_nan` 的 10% 阈值对物理产物过松
conv_check.rs:212`(bad_lines as f64) > (total_lines as f64 * 0.1)`,严格大于。
典型 `.7` 文件 60-100 个深度行,阈值 6-10 行:
| 场景 | bad/total | 判定 | 物理正确性 |
| --------------------------- | --------- | ----------------------- | -------------- |
| 全 NaN(彻底发散) | 100% | true → 否决 | 正确 |
| 6/100 行 NaN(表层发散) | 6% | **false → 放过** | **错误** |
| 5/60 行 NaN(典型深度网格) | 8.3% | **false → 放过** | **错误** |
| 单行 NaN | 1% | **false → 放过** | **错误** |
物理上**任意一个深度点的温度/密度是 NaN,整个大气就不可用**(积分会传播 NaN)。10% 是统计学语义,与大气可用性的布尔语义不匹配。表层(深度浅、温度高)最易数值发散,往往只有最外几层出 NaN —— 正好落在盲区。
补充盲区:regex `(?i)(\bnan\b|\binf(?:inity)?\b|\*{3,})`conv_check.rs:199)只匹配字面 `NaN`/`Inf`/`***`;若 Fortran 把发散值写成接近 f64 上限但未溢出的普通科学记数法(如 `1.0E+308`),不命中任何模式。
### 漏洞 5(低):`infer_failed_stage` / 日志归因对 RYBSOL 路径不完整
发散 STOP 在 SOLVE / SOLVES / RYBSOL 三处(§3.1),但失败 note 统一记 `tlusty rc=0 or missing fort.7`runner.rs:429),丢失"为何未收敛"的线索。对走 RYBSOL`IFRYB≠0`)的发散,归因信息不完整。
### 漏洞 6(低):仅 SYNSPEC 场景 final_converged 重判只看 rc
runner.rs:611-616TLUSTY 关闭的纯复算场景,`final_converged = (synspec_rc == 0)`,与漏洞 1 同源,场景更窄。
---
## 5. 被排除的伪风险
**超时 / shutdown 后半成品 fort.7 进 check_fort9 误判收敛 —— 不存在。**
`run_child_async_with_timeout` 在超时/shutdown 时返回 `Err`runner.rs:151, 156)→ 上层 match `rc = -1`
runner.rs:380-383 / 568-574)。于是 `rc==0 && fort.7 存在` 前置门不成立 → 进 `else` 分支 →
`converged` 保持 false → `check_fort9` 根本不调用。SYNSPEC 超时同理 → `synspec_rc=-1` → 判失败。
**超时路径是可靠的。**
---
## 6. 修复优先级(仅建议,不含实现)
| 优先级 | 漏洞 | 建议 | 改动范围 |
| ------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| P0 | 漏洞 1 | SYNSPEC`.spec` 内容校验:NaN/Inf/`***` + 行数下限 + 非全零,命中则置 `synspec_rc` 为非零或设 `synspec_error` | runner.rsSYNSPEC 产出后,runner.rs:585 附近) |
| P1 | 漏洞 4 | 收紧物理产物 NaN 阈值:产物准入用 0 行容忍(任意一行 NaN 即否决),而非 10% | conv_check.rs:212 或新增严格版函数 |
| P1 | 漏洞 2 | runner.rs:417-421:至少把`best_max_relc``Some(0.0)` 改为 `None`(消除污染扩散);理想是校验 stage 的 `niter` 配置才走该分支 | runner.rs:417-421 |
| P2 | 漏洞 5 | 补齐 SOLVES / RYBSOL 发散路径的归因,失败 note 区分求解器类型 | runner.rs:429 |
| P3 | 漏洞 3 | 生产编译加`-fcheck=bounds -ffpe-trap=invalid,zero,overflow`,让数组越界与 IEEE 异常返回 rc≠0(性能换正确性) | deployment.md 编译命令 |
---
## 7. 参考资料
- 源码:
- `tlusty/tlusty208.f`(主程序 :1-62;发散 STOP :14703/:15050/:47585quit :29946-29955
PRCHAN :22876-22944RYBCHN :47445-47598RESOLV :3724-3903OUTPUT :14055 起)
- `synspec/synspec54.f`(主程序 :1-174OUTPRI :3343-3458quit :12014-12021
脏数据风险点 :4203/:8208/:9935/:18863/:18865/:19891
- 判定逻辑:
- `crates/common/src/runner.rs`run_model_with_timeout :232-682;漏洞 1/2/5/6 所在)
- `crates/node/src/reporter.rs`derive_report_status :38-45infer_failed_stage :15-27
- `crates/common/src/conv_check.rs`check_fort9 :54-175atmosphere_has_nan :190-213
- 编译配置:`docs/deployment.md:86,90`(确认生产编译无 `-fcheck`/`-ffpe-trap`
- 相关设计:`docs/task_engine_decoupling_design.md`(阶段独立配置、半失败判定语义)