Files
DCTS/crates/server/tests/wf_migration_isolation.rs
T
fmq c8fd24b120 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 入库
2026-08-01 17:01:40 +08:00

200 lines
8.0 KiB
Rust
Raw 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.
//! 验证「历史种子导入的工作流名」与「正式工作流名」的隔离关系。
//!
//! 用户意图:import_results 把旧 Python 计算结果导入,标记为已完成,避免重算。
//! 关键问题:导入到工作流 A,之后正式启动工作流 B(同名/异名),B 能否看到 A 标记的 converged
use common::config::GridConfig;
use common::models::GridPointParams;
use mq::sqlite_queue::SqliteTaskQueue;
use server::{db::Database, scheduler::GridScheduler};
use std::sync::Arc;
fn make_params() -> GridPointParams {
use common::models::GridAxisValue;
GridPointParams {
teff: GridAxisValue::from_value(20000.0),
logg: GridAxisValue::from_value(5.0),
loghe: GridAxisValue::from_value(-2.0),
logc: GridAxisValue::from_value(-4.0),
logn: GridAxisValue::from_value(-4.0),
logo: GridAxisValue::from_value(-4.0),
}
}
/// 构造只含一个网格点(t20000_g5.0_he-2_c-4_n-4_o-4)的 config。
fn make_grid_cfg() -> GridConfig {
let yaml = "grid:\n teff: [20000]\n logg: [5.0]\n loghe: [-2]\n logc: [-4]\n logn: [-4]\n logo: [-4]\n";
GridConfig::from_yaml_str(yaml).unwrap()
}
async fn setup() -> (Database, Arc<GridScheduler>) {
let tmp = tempfile::tempdir().unwrap();
let db = Database::new(&tmp.path().join("db.db").to_string_lossy())
.await
.unwrap();
let queue = Arc::new(
SqliteTaskQueue::new(&tmp.path().join("q.db").to_string_lossy())
.await
.unwrap(),
);
let sched = Arc::new(GridScheduler::new(db.clone(), queue));
(db, sched)
}
/// 场景 1(正确用法):导入到工作流 "sdB_cno",再用同名 config initialize_grid。
/// 期望:initialize_grid 的 ON CONFLICT(workflow_name, name) DO NOTHING 保留 converged 状态。
#[tokio::test]
async fn test_same_workflow_name_preserves_converged() {
let (db, sched) = setup().await;
let name = "t20000_g5.0_he-2_c-4_n-4_o-4";
let p = make_params();
// 模拟 import_seedupsert + mark_imported,工作流名 = sdB_cno
db.upsert_grid_point_named(name, &p, 0, "sdB_cno")
.await
.unwrap();
db.mark_grid_point_imported(name, "sdB_cno", None, "cold_run")
.await
.unwrap();
// 之后正式启动同名工作流:initialize_grid(sdB_cno)
sched
.initialize_grid(&make_grid_cfg(), "sdB_cno")
.await
.unwrap();
let st = db.get_grid_point_status(name, "sdB_cno").await.unwrap();
assert_eq!(
st.unwrap().0,
"converged",
"同名工作流:导入的 converged 应被保留,避免重算"
);
println!("✓ 场景1(同名):status=converged,旧结果被保留,不会重算");
}
/// 场景 2(错误用法):导入到工作流 "imported",之后正式启动 "sdB_cno"。
/// 期望:sdB_cno 分区下是新插入的 pending 行,看不到 imported 分区的 converged。
#[tokio::test]
async fn test_different_workflow_name_causes_recompute() {
let (db, sched) = setup().await;
let name = "t20000_g5.0_he-2_c-4_n-4_o-4";
let p = make_params();
// 模拟 import_seed:导入到 "imported" 工作流
db.upsert_grid_point_named(name, &p, 0, "imported")
.await
.unwrap();
db.mark_grid_point_imported(name, "imported", None, "cold_run")
.await
.unwrap();
// 之后正式启动 "sdB_cno" 工作流
sched
.initialize_grid(&make_grid_cfg(), "sdB_cno")
.await
.unwrap();
// imported 分区:converged(种子库有,但不会被 sdB_cno 调度看到)
let st_imp = db.get_grid_point_status(name, "imported").await.unwrap();
assert_eq!(st_imp.unwrap().0, "converged");
// sdB_cno 分区:pending(重新算!看不到 imported 的 converged
let st_real = db.get_grid_point_status(name, "sdB_cno").await.unwrap();
assert_eq!(
st_real.unwrap().0,
"pending",
"异名工作流:sdB_cno 看不到 imported 的 converged,会重算"
);
println!("✓ 场景2(异名):sdB_cno 分区 status=pending,会重复计算 —— 验证了工作流名必须匹配");
}
/// 场景 3(真实意图验证):旧网格有部分点已算完(导入为 converged),
/// 新网格比旧网格多了若干点。用同名工作流启动后:
/// - 旧点保持 converged(不重算)
/// - 新点是 pending(会被调度计算)
/// 这正是「同步旧结果避免重算」的核心语义。
#[tokio::test]
async fn test_mixed_grid_import_then_init_avoids_recompute() {
let (db, sched) = setup().await;
let p_old = make_params(); // t20000_g5.0_...
// 模拟 import_seed:旧网格里这个点已收敛,导入到 sdB_cno
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, "cold_run")
.await
.unwrap();
// 正式启动 sdB_cno,config 比旧网格多了一个新点(t25000)
let yaml = "grid:\n teff: [20000, 25000]\n logg: [5.0]\n loghe: [-2]\n logc: [-4]\n logn: [-4]\n logo: [-4]\n";
let cfg = GridConfig::from_yaml_str(yaml).unwrap();
sched.initialize_grid(&cfg, "sdB_cno").await.unwrap();
// 旧点:converged(不重算)
let st_old = db
.get_grid_point_status("t20000_g5.0_he-2_c-4_n-4_o-4", "sdB_cno")
.await
.unwrap();
assert_eq!(
st_old.unwrap().0,
"converged",
"旧点应保持 converged 不重算"
);
// 新点:pending(会被调度)
let st_new = db
.get_grid_point_status("t25000_g5.0_he-2_c-4_n-4_o-4", "sdB_cno")
.await
.unwrap();
assert_eq!(st_new.unwrap().0, "pending", "新点应为 pending 等待计算");
println!("✓ 场景3(混合网格):旧点converged保留 + 新点pending待算 —— 完全符合避免重算的意图");
}
/// 场景 4(精度差异命门):旧 conv.json name=g5(无小数),但配置 logg=5.0 → model_name()=g5.0。
/// 导入工具必须把 name 重写为 g5.0 入库,否则 initialize_grid 插入的 g5.0 行与导入的 g5 行
/// 复合唯一键不匹配,导入的 converged 被孤立、g5.0 被重算。
/// 本测试直接模拟「入库的 grid_points.name = g5.0」(即工具重写后的状态),
/// 验证 initialize_grid(sdB_cno) 后该行保持 converged(不重算)。
#[tokio::test]
async fn test_precision_diff_import_then_init_preserves_converged() {
let (db, sched) = setup().await;
// 配置权威名(logg=5.0 → g5.0,保留小数)
let canonical = "t20000_g5.0_he-2_c-4_n-4_o-4";
use common::models::GridAxisValue;
let p = GridPointParams {
teff: GridAxisValue::from_value(20000.0),
logg: GridAxisValue::from_value(5.0),
loghe: GridAxisValue::from_value(-2.0),
logc: GridAxisValue::from_value(-4.0),
logn: GridAxisValue::from_value(-4.0),
logo: GridAxisValue::from_value(-4.0),
};
// 模拟 import_results 重写 name 后入库:grid_points.name = canonical(g5.0)
db.upsert_grid_point_named(canonical, &p, 0, "sdB_cno")
.await
.unwrap();
db.mark_grid_point_imported(canonical, "sdB_cno", None, "cold_run")
.await
.unwrap();
// 启动同名工作流:initialize_grid 用配置 model_name()(=g5.0) 插入
let yaml = "grid:\n teff: [20000]\n logg: [5.0]\n loghe: [-2]\n logc: [-4]\n logn: [-4]\n logo: [-4]\n";
let cfg = GridConfig::from_yaml_str(yaml).unwrap();
sched.initialize_grid(&cfg, "sdB_cno").await.unwrap();
// 关键断言:name=g5.0 的行保持 convergedON CONFLICT DO NOTHING 命中)
let st = db
.get_grid_point_status(canonical, "sdB_cno")
.await
.unwrap();
assert_eq!(
st.unwrap().0,
"converged",
"精度一致(g5.0=g5.0)时导入的 converged 必须保留,不重算"
);
println!("✓ 场景4(精度差异命门):g5.0 入库 + initialize_grid → converged 保留,避免重算");
}