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:
@@ -221,7 +221,7 @@ async fn test_l2_node_token_issue_reissue_flow() {
|
||||
node_id: "node-l2-test".to_string(),
|
||||
max_slots: 4,
|
||||
};
|
||||
db.register_node(®).await.unwrap();
|
||||
let (_, _, secret) = db.register_node(®).await.unwrap();
|
||||
|
||||
// 2. 颁发专属 token,返回明文
|
||||
let token = db.issue_node_token("node-l2-test").await.unwrap();
|
||||
@@ -229,16 +229,23 @@ async fn test_l2_node_token_issue_reissue_flow() {
|
||||
|
||||
// 2b. 一次性取走暂存明文(take_pending_node_token):首次取到与颁发一致的明文,
|
||||
// 再次取为 None(取走即焚)。验证 #4 简化为单一 UPDATE...RETURNING 后行为一致。
|
||||
let pending = db.take_pending_node_token("node-l2-test").await.unwrap();
|
||||
// H8:须提供注册时下发的 registration_secret。
|
||||
let pending = db
|
||||
.take_pending_node_token("node-l2-test", secret.as_deref())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(pending.as_deref(), Some(token.as_str()));
|
||||
let pending2 = db.take_pending_node_token("node-l2-test").await.unwrap();
|
||||
let pending2 = db
|
||||
.take_pending_node_token("node-l2-test", secret.as_deref())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
pending2.is_none(),
|
||||
"取走即焚:第二次 take_pending 必须返回 None"
|
||||
);
|
||||
// 不存在的 node take 也应返回 None(不报错)
|
||||
assert!(db
|
||||
.take_pending_node_token("node-not-exist")
|
||||
.take_pending_node_token("node-not-exist", None)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
@@ -774,9 +781,15 @@ async fn test_node_approval_workflow() {
|
||||
.unwrap();
|
||||
let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||
assert_eq!(json["status"], "pending_approval");
|
||||
// H8:注册响应下发一次性 registration_secret,后续 check_status 取 token 须回传。
|
||||
let reg_secret = json["registration_secret"].as_str().map(|s| s.to_string());
|
||||
assert!(reg_secret.is_some(), "注册响应应包含 registration_secret");
|
||||
|
||||
// 2. Node 端轮询查状态 ➔ status: pending_approval
|
||||
let check_body = serde_json::json!({ "node_id": "node-pending-01" });
|
||||
let check_body = serde_json::json!({
|
||||
"node_id": "node-pending-01",
|
||||
"registration_secret": reg_secret,
|
||||
});
|
||||
let res = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
@@ -1291,6 +1304,7 @@ fn make_import_multipart(
|
||||
report_json: &str,
|
||||
seed_bytes: &[u8],
|
||||
seed_name: &str,
|
||||
success_method: &str,
|
||||
) -> Vec<u8> {
|
||||
let mut body = Vec::new();
|
||||
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
|
||||
@@ -1309,6 +1323,12 @@ fn make_import_multipart(
|
||||
body.extend_from_slice(b"Content-Type: application/octet-stream\r\n\r\n");
|
||||
body.extend_from_slice(seed_bytes);
|
||||
body.extend_from_slice(b"\r\n");
|
||||
// 收敛途径字段(cold_run/seed_step):模拟 import_results 工具判定后透传的途径。
|
||||
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
|
||||
body.extend_from_slice(b"Content-Disposition: form-data; name=\"success_method\"\r\n");
|
||||
body.extend_from_slice(b"Content-Type: text/plain\r\n\r\n");
|
||||
body.extend_from_slice(success_method.as_bytes());
|
||||
body.extend_from_slice(b"\r\n");
|
||||
body.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes());
|
||||
body
|
||||
}
|
||||
@@ -1360,6 +1380,7 @@ async fn test_import_seed_admin_endpoint() {
|
||||
&conv,
|
||||
b"FAKE_ATMOS_7",
|
||||
"t20000_g5.0_he-2_c-4_n-4_o-4.7",
|
||||
"cold_run",
|
||||
);
|
||||
let res = app
|
||||
.clone()
|
||||
@@ -1381,6 +1402,7 @@ async fn test_import_seed_admin_endpoint() {
|
||||
&conv,
|
||||
b"FAKE_ATMOS_7",
|
||||
"t20000_g5.0_he-2_c-4_n-4_o-4.7",
|
||||
"cold_run",
|
||||
);
|
||||
let res = app
|
||||
.clone()
|
||||
@@ -1419,6 +1441,7 @@ async fn test_import_seed_admin_endpoint() {
|
||||
&conv,
|
||||
b"FAKE_ATMOS_7_AGAIN",
|
||||
"t20000_g5.0_he-2_c-4_n-4_o-4.7",
|
||||
"cold_run",
|
||||
);
|
||||
let res = app
|
||||
.clone()
|
||||
@@ -1443,7 +1466,7 @@ async fn test_import_seed_admin_endpoint() {
|
||||
|
||||
// 4. 未收敛点 → 200,但不写 .7、grid_points 维持 pending(未建 converged)。
|
||||
let conv_fail = make_legacy_conv_json("t20000_g5.0_he-2_c-4_n-4_o-4_fail", false);
|
||||
let body_bytes = make_import_multipart("boundary4", &conv_fail, b"WONT_BE_USED", "x.7");
|
||||
let body_bytes = make_import_multipart("boundary4", &conv_fail, b"WONT_BE_USED", "x.7", "cold_run");
|
||||
let res = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -1516,7 +1539,7 @@ async fn test_import_seed_python_legacy_conv_json() {
|
||||
let name = "t20000_g5.0_he-2_c-4_n-4_o-4";
|
||||
let conv = make_python_legacy_conv_json(name);
|
||||
let body_bytes =
|
||||
make_import_multipart("boundaryL", &conv, b"FAKE_ATMOS_7", &format!("{name}.7"));
|
||||
make_import_multipart("boundaryL", &conv, b"FAKE_ATMOS_7", &format!("{name}.7"), "cold_run");
|
||||
let res = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -1535,14 +1558,14 @@ async fn test_import_seed_python_legacy_conv_json() {
|
||||
"旧版嵌套 stages 的 conv.json 应被接受"
|
||||
);
|
||||
|
||||
// grid_points:converged + imported 手段 + 旧版 elapsed_sec 已落库
|
||||
// grid_points:converged + cold_run 手段(旧版 conv.json 无 seed_nc 阶段)+ 旧版 elapsed_sec 已落库
|
||||
let row = db
|
||||
.get_workflow_point_row("wf_legacy", name)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("grid_points 应存在");
|
||||
assert_eq!(row.status, "converged");
|
||||
assert_eq!(row.success_method.as_deref(), Some("imported"));
|
||||
assert_eq!(row.success_method.as_deref(), Some("cold_run"));
|
||||
assert_eq!(
|
||||
row.last_elapsed_sec,
|
||||
Some(715.0),
|
||||
@@ -2036,7 +2059,7 @@ async fn test_wf_stats_endpoint() {
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
db.mark_grid_point_imported(&p_imported.model_name(), "wf_stats", None)
|
||||
db.mark_grid_point_imported(&p_imported.model_name(), "wf_stats", None, "cold_run")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2091,9 +2114,8 @@ async fn test_wf_stats_endpoint() {
|
||||
assert_eq!(data["running"], 1);
|
||||
assert_eq!(data["converged"], 3);
|
||||
assert_eq!(data["failed"], 1);
|
||||
assert_eq!(data["cold_run_converged"], 1);
|
||||
assert_eq!(data["cold_run_converged"], 2);
|
||||
assert_eq!(data["seed_step_converged"], 1);
|
||||
assert_eq!(data["imported_converged"], 1);
|
||||
// wave 分布:3 个波次,wave0 共 3 点全未收敛
|
||||
let waves = data["waves"].as_array().unwrap();
|
||||
assert_eq!(waves.len(), 3);
|
||||
@@ -2225,7 +2247,7 @@ async fn seed_obs_fixture(db: &Database, db_path: &std::path::Path, wf: &str) ->
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
db.mark_grid_point_imported(&p_imported.model_name(), wf, None)
|
||||
db.mark_grid_point_imported(&p_imported.model_name(), wf, None, "cold_run")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2383,7 +2405,7 @@ async fn test_wf_points_endpoint() {
|
||||
assert_eq!(data["total"], 1);
|
||||
assert_eq!(data["points"][0]["name"], n.imported);
|
||||
|
||||
// ---- 7. 分页:limit=2 翻页,total 恒定;limit 钳位 ≤500 ----
|
||||
// ---- 7. 分页:limit=2 翻页,total 恒定;limit 缺省=全量(无上限,联合分析用) ----
|
||||
let (_, page0) = get_points(&app, "/api/workflows/wf_pts/points?limit=2&offset=0").await;
|
||||
let (_, page2) = get_points(&app, "/api/workflows/wf_pts/points?limit=2&offset=4").await;
|
||||
assert_eq!(page0["total"], 6);
|
||||
@@ -2391,6 +2413,9 @@ async fn test_wf_points_endpoint() {
|
||||
assert_eq!(page2["points"].as_array().unwrap().len(), 2);
|
||||
let (_, all) = get_points(&app, "/api/workflows/wf_pts/points?limit=9999").await;
|
||||
assert_eq!(all["points"].as_array().unwrap().len(), 6);
|
||||
// limit 完全缺省 → 全量(None 路径,不拼 LIMIT 子句)
|
||||
let (_, full) = get_points(&app, "/api/workflows/wf_pts/points").await;
|
||||
assert_eq!(full["points"].as_array().unwrap().len(), 6);
|
||||
|
||||
// ---- 8. 时间排序:最近完成的点(rescued 的 seed_step 尝试最新)排首位 ----
|
||||
let (st, data) = get_points(
|
||||
|
||||
@@ -53,7 +53,7 @@ async fn test_same_workflow_name_preserves_converged() {
|
||||
db.upsert_grid_point_named(name, &p, 0, "sdB_cno")
|
||||
.await
|
||||
.unwrap();
|
||||
db.mark_grid_point_imported(name, "sdB_cno", None)
|
||||
db.mark_grid_point_imported(name, "sdB_cno", None, "cold_run")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -84,7 +84,7 @@ async fn test_different_workflow_name_causes_recompute() {
|
||||
db.upsert_grid_point_named(name, &p, 0, "imported")
|
||||
.await
|
||||
.unwrap();
|
||||
db.mark_grid_point_imported(name, "imported", None)
|
||||
db.mark_grid_point_imported(name, "imported", None, "cold_run")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -122,7 +122,7 @@ async fn test_mixed_grid_import_then_init_avoids_recompute() {
|
||||
db.upsert_grid_point_named("t20000_g5.0_he-2_c-4_n-4_o-4", &p_old, 0, "sdB_cno")
|
||||
.await
|
||||
.unwrap();
|
||||
db.mark_grid_point_imported("t20000_g5.0_he-2_c-4_n-4_o-4", "sdB_cno", None)
|
||||
db.mark_grid_point_imported("t20000_g5.0_he-2_c-4_n-4_o-4", "sdB_cno", None, "cold_run")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -176,7 +176,7 @@ async fn test_precision_diff_import_then_init_preserves_converged() {
|
||||
db.upsert_grid_point_named(canonical, &p, 0, "sdB_cno")
|
||||
.await
|
||||
.unwrap();
|
||||
db.mark_grid_point_imported(canonical, "sdB_cno", None)
|
||||
db.mark_grid_point_imported(canonical, "sdB_cno", None, "cold_run")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user