feat(all): 科学计算正确性修复、调度竞态消除、节点优雅退出、安全加固与收敛分析重构
科学计算正确性: - 修复 Fortran 无-E 科学记数法(指数≥100 时 E 被挤掉,如 -1.35+118)导致 发散行被静默跳过、误判收敛的 bug;扩展大气无效检测覆盖 Inf 与 *** 溢出标记 - 种子匹配改为 CNO 有向距离(富金属方向重罚 4×、贫金属方向轻罚 1×), 基于 1191 个真实种子配对回测标定,回测净改善 314 个点 - GridAxisValue 反序列化拒绝非法文本(不再静默 NaN);chmax≤0 显式报错 - ions 行宽列宽对齐真实 fort.5 格式 调度与队列竞态: - 原子选点(IMMEDIATE 事务 SELECT+UPDATE)消除并发调度重复派发 (#5) - 调度互斥锁 + 冷启动优先策略(SeedStep 仅作失败后救援,不再正常路径热启动) - 毒消息 dead_letter 标记防出队死循环;clear_queue 保留 claimed 行 (#6) - 孤儿 running 点回收兜底;stale_sec 默认 7800→21600s(3 倍超时缓冲) 节点生命周期: - SIGTERM+SIGINT 双信号监听(修复 Docker stop 发 SIGTERM 不触发优雅退出) - SlotGuard RAII 防活动 slot 泄漏;子进程超时增加二级 30s wait 防 Fortran hang - SeedStep 种子下载 fail-fast + 沙盒私有副本解耦 LRU 清理竞争 - reqwest Client 增加连接/请求超时;启动清理残留 task_* 沙盒 安全加固: - 节点注册 registration_secret 二次凭据 (H8),恒定时间比对防时序旁路 - token 缓存 generation 机制消除 reissue 后旧 token TOCTOU 复活窗口 - 新增 /api/auth/logout 服务端 session 即时撤销;fail-closed 鉴权启动策略 - 前端 token 迁移 sessionStorage;YAML 高亮改 DOM API 消除 XSS 注入面 - 备份文件权限收紧 0600;点表动态值全面 escapeHtml 前端 Dashboard: - 收敛性分析从热力图重构为 Parallel Sets 平行集合图(6 维+状态轴,手写 SVG 零依赖) - 进度曲线横轴改为真实时间(服务端 now 锚定,停滞期诚实留白);轮询指数退避 - 移除 imported 收敛途径分类,导入点按实际途径 cold_run/seed_step 归类 - 初始化时 /api/auth/check 校验 token;401 toast 提示替代静默 reload 服务端恢复与工具链: - 启动恢复 initializing 态工作流;默认工作流 INSERT-only 不覆盖 API 编辑 - body limit 分层(10MB 不再截断 256MB report);multipart 显式错误处理 - 嵌入二进制原子写(tmp+rename)防半写损坏 - import_results 判定收敛途径透传 success_method;conv.json 格式对齐本项目 - push_import_results.sh 退出码修复 + .bat UTF-8 BOM + scp 上传 - Docker USE_MIRRORS 默认关闭;移除无用 assets 挂载;删除 hosts.ini 入库
This commit is contained in:
@@ -9,7 +9,7 @@ use reqwest::multipart::{Form, Part};
|
||||
use reqwest::Client;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::{info, warn};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// DCTS 历史计算结果导入工具。
|
||||
///
|
||||
@@ -77,6 +77,9 @@ struct CandidateResult {
|
||||
src_dir: PathBuf,
|
||||
/// 权威新名(来自 workflow 配置的小数精度命名)。
|
||||
canonical_name: String,
|
||||
/// 该点当初的收敛途径("cold_run" / "seed_step"),据 stages 是否含 seed_nc 判定,
|
||||
/// 透传给服务端写入 grid_points.success_method,让导入点融入冷启动/种子步进统计。
|
||||
success_method: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -327,12 +330,34 @@ async fn scan_dir_for_results(
|
||||
continue;
|
||||
};
|
||||
|
||||
// 判定该历史点的收敛途径(供服务端写入 success_method,让导入点融入冷启动/种子步进统计):
|
||||
// 旧 run_grid.py 流程——冷启动失败才改用 seed_step 链重跑,种子链首段 label 为 "seed_nc"。
|
||||
// 故 stages 含 seed_nc → seed_step(种子步进救活);否则 → cold_run(冷启动一次收敛)。
|
||||
let has_seed_nc_stage = summary
|
||||
.stages
|
||||
.iter()
|
||||
.any(|s| s.label == "seed_nc");
|
||||
let success_method = if has_seed_nc_stage {
|
||||
"seed_step"
|
||||
} else {
|
||||
"cold_run"
|
||||
};
|
||||
// 辅助诊断:探测同级 <legacy_name>.coldfail 目录(冷启动失败备份),仅记日志不改变判定。
|
||||
let coldfail_dir = dir_path.join(format!("{}.coldfail", summary.name));
|
||||
if coldfail_dir.is_dir() {
|
||||
debug!(
|
||||
"网格点 {} 检测到同级 .coldfail 备份目录,判定途径={}",
|
||||
summary.name, success_method
|
||||
);
|
||||
}
|
||||
|
||||
results.push(CandidateResult {
|
||||
legacy_name: summary.name.clone(),
|
||||
summary_json: content,
|
||||
seed_path: seed_file,
|
||||
src_dir: sub_path,
|
||||
canonical_name: canonical_name.clone(),
|
||||
success_method: success_method.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -626,6 +651,13 @@ async fn upload_results_to_server(
|
||||
Part::bytes(seed_bytes)
|
||||
.file_name(format!("{}.7", c.canonical_name))
|
||||
.mime_str("application/octet-stream")?,
|
||||
)
|
||||
// 收敛途径(cold_run/seed_step):服务端据此写 grid_points.success_method,
|
||||
// 让导入点融入冷启动/种子步进统计而非独立 imported 分类。
|
||||
// Part::text 需 'static 生命周期,克隆一份脱离 candidates 借用。
|
||||
.part(
|
||||
"success_method",
|
||||
Part::text(c.success_method.clone()).mime_str("text/plain")?,
|
||||
);
|
||||
|
||||
let mut req = client.post(&import_url);
|
||||
@@ -678,16 +710,15 @@ fn urlencoding(s: &str) -> String {
|
||||
/// `initialize_grid` 的 `model_name()` 逐字符一致,必须用配置权威名覆盖旧 name。
|
||||
/// 解析为 `serde_json::Value` 后仅替换 name,其余字段(params/stages/收敛状态等)原样保留。
|
||||
fn rewrite_summary_name(summary_json: &str, canonical_name: &str) -> Result<String> {
|
||||
let mut val: serde_json::Value =
|
||||
serde_json::from_str(summary_json).context("解析旧 conv.json 失败")?;
|
||||
let obj = val
|
||||
.as_object_mut()
|
||||
.context("旧 conv.json 顶层不是 JSON 对象,无法重写 name")?;
|
||||
obj.insert(
|
||||
"name".to_string(),
|
||||
serde_json::Value::String(canonical_name.to_string()),
|
||||
);
|
||||
Ok(serde_json::to_string(&val)?)
|
||||
// 解析成 ModelSummary 结构体(与本项目计算生成 conv.json 同源),改 name 后用
|
||||
// to_string_pretty 序列化——字段按 ModelSummary 声明顺序、pretty 缩进,与
|
||||
// runner.rs:597 本项目计算写出的 conv.json 格式完全一致,使导入产物与正常计算产物统一。
|
||||
// 注:ModelSummary 未开 deny_unknown_fields,旧 conv.json 里 ModelSummary 未定义的
|
||||
// 额外字段会被丢弃——这正是"对齐本项目"的预期(本项目计算的 conv.json 本就不含这些字段)。
|
||||
let mut summary: ModelSummary =
|
||||
serde_json::from_str(summary_json).context("解析旧 conv.json 为 ModelSummary 失败")?;
|
||||
summary.name = canonical_name.to_string();
|
||||
Ok(serde_json::to_string_pretty(&summary)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -896,21 +927,36 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_rewrite_summary_name_changes_only_name() {
|
||||
let original = r#"{"name":"t20000_g5_he-2_c-4_n-4_o-4","params":{"teff":20000.0,"logg":5.0,"loghe":-2.0,"logc":-4.0,"logn":-4.0,"logo":-4.0},"converged":true,"final_max_relc":0.001,"atmosphere_has_nan":false}"#;
|
||||
// 夹具含 stages(ModelSummary.stages 为必需字段,无 serde default)。
|
||||
let original = r#"{"name":"t20000_g5_he-2_c-4_n-4_o-4","params":{"teff":20000.0,"logg":5.0,"loghe":-2.0,"logc":-4.0,"logn":-4.0,"logo":-4.0},"stages":[{"label":"nl","lte":"F","converged":true,"elapsed_sec":120.0}],"converged":true,"final_max_relc":0.001,"atmosphere_has_nan":false}"#;
|
||||
let rewritten = rewrite_summary_name(original, "t20000_g5.0_he-2_c-4_n-4_o-4").unwrap();
|
||||
let v: serde_json::Value = serde_json::from_str(&rewritten).unwrap();
|
||||
assert_eq!(v["name"].as_str().unwrap(), "t20000_g5.0_he-2_c-4_n-4_o-4");
|
||||
assert_eq!(v["params"]["logg"].as_f64().unwrap(), 5.0);
|
||||
assert!(v["converged"].as_bool().unwrap());
|
||||
assert_eq!(v["final_max_relc"].as_f64().unwrap(), 0.001);
|
||||
// 格式对齐本项目计算生成的 conv.json(runner.rs:597 to_string_pretty(&ModelSummary)):
|
||||
// pretty 多行缩进 + 字段按 ModelSummary 声明顺序(name 为首字段,非字母序)。
|
||||
assert!(rewritten.contains('\n'), "应为 pretty 多行格式而非紧凑单行");
|
||||
assert!(
|
||||
rewritten.trim_start().starts_with("{\n \"name\""),
|
||||
"首字段应为 name(结构体声明顺序),实际开头: {}",
|
||||
&rewritten[..rewritten.len().min(40)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rewrite_summary_name_rejects_non_object() {
|
||||
// 非对象 JSON(数组/标量)应报错,而非静默放过
|
||||
fn test_rewrite_summary_name_rejects_invalid_summary() {
|
||||
// 非合法 ModelSummary(数组/标量/缺必需字段 stages)应报错,而非静默放过。
|
||||
assert!(rewrite_summary_name("[1,2,3]", "x").is_err());
|
||||
assert!(rewrite_summary_name("42", "x").is_err());
|
||||
assert!(rewrite_summary_name("\"hello\"", "x").is_err());
|
||||
// 缺必需字段 stages → 解析为 ModelSummary 失败
|
||||
assert!(rewrite_summary_name(
|
||||
r#"{"name":"x","params":{"teff":1.0,"logg":1.0,"loghe":0.0,"logc":0.0,"logn":0.0,"logo":0.0},"converged":true,"atmosphere_has_nan":false}"#,
|
||||
"x"
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user