核心变更:
1. GridAxisValue 源精度命名
- 新增 GridAxisValue 类型,携带 f64 数值 + YAML 源书写文本(Deref 透明兼容算术)
- config.rs 绕过 serde_yaml 归一化,逐 token 捕获轴值原文(logg: 5.0 → g5.0)
- runner/executor/scheduler 全链路改用 DB TEXT 列权威 point_name,
修复 REAL 列回读丢精度导致的 model_name 错配
2. 工作流执行可观测台
- 新增 stats/progress/points 三组 API(进度时间序列、经验速率 ETA、
停滞预警、逐点明细分页、收敛性热力图数据)
- 新增 workflow_progress_snapshots 表 + tasks/grid_points 耗时列
- runner 携带 last_iter/worst_depth/n_depths 进 conv.json
- 前端新增 hash 路由、工作流详情页(概览/网格点/收敛分析三 Tab)、YAML 编辑器
3. 节点停用/启用管理
- 新增 disabled 状态 + disable/enable API;停用节点保持心跳但停止分发,
worker 空闲待命而非退出;移除 revoke API,token 失效统一走重发覆盖;
移除 host_name 字段
4. 白名单结果归档
- 新增 result_filter 模块,只归档有语义产物,丢弃 Tlusty 中间单元(~2MB/模型)
- executor 原子写入归档 + 200 点 LRU 上限
5. 历史数据导入
- sync_seeds 重写为 import_results:经 /admin/import_seed 标记 converged +
按新版命名迁移产物树
6. 部署与目录重规划
- data/results→seeds、data/archive→result + migrate_data_dirs.sh
- deploy.sh 增强(SSH 复用、Profile、远程 env);Dockerfile 瘦身
7. 文档同步更新 api/database/architecture/deployment
200 lines
7.9 KiB
Rust
200 lines
7.9 KiB
Rust
//! 验证「历史种子导入的工作流名」与「正式工作流名」的隔离关系。
|
||
//!
|
||
//! 用户意图: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_seed:upsert + 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)
|
||
.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)
|
||
.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)
|
||
.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)
|
||
.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 的行保持 converged(ON 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 保留,避免重算");
|
||
}
|