feat(all): 数据库模块化拆分与版本化迁移、任务引擎命名体系收敛、物理输出校验加固与用户配置接通
- server/db: 拆 4929 行 db.rs 单体为 db/ 目录,migrations.rs 引入 PRAGMA user_version
版本化迁移运行器(M1~M13)
- 任务引擎 Phase 6/7b/7c 改名收敛:EngineStageConfig→PhaseConfig、StagePolicy→ResumePolicy、
Converged→Completed、删除 task_type 列、success_method 拆 tlusty_/synspec_ 双列、
新增 tlusty_status/synspec_status 半失败阶段守卫
- 科学正确性加固:conv_check 任意行 NaN/Inf/溢出判无效(0 行容忍)、新增 spec_is_valid
校验 SYNSPEC 脏谱、itek_history 逐次迭代全量保真、fmt_abn powf 溢出饱和
- 用户配置真正接通:tlusty_chain/tlusty_input 由死字段经 调度器→TaskSpec→executor→runner
透传生效;config 加载期 validate + deny_unknown_fields + 解析失败记 warn
- 调度修复:H1 活锁(pending_strategies 跳过已失败策略)、种子查找错误不再静默降级冷启动
- dashboard: 阶段配置面板 tlusty_stage/synspec_stage、"已完成"标签、迭代诊断展示
- docs: 新增 database_refactor_design.md,同步 database/api/PIPELINE/workflow_detail
This commit is contained in:
+132
-20
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use common::config::SynspecConfig;
|
||||
use common::config::{ChainStep, SynspecInput, TlustyInput};
|
||||
use common::embedded::{ensure_specific_data_files, RuntimePaths};
|
||||
use common::models::{ModelSummary, TaskSpec};
|
||||
use common::result_filter::is_result_worthy;
|
||||
@@ -157,22 +157,49 @@ pub async fn execute_task(
|
||||
// 3. (slot_work_dir 已在种子下载前提前创建,种子私有副本亦已落盘于沙盒内。)
|
||||
|
||||
// 反序列化工作流携带的 SYNSPEC 数值参数(波长范围等)。None → runner 用硬编码默认。
|
||||
let synspec_cfg: Option<SynspecConfig> = task
|
||||
let synspec_cfg: Option<SynspecInput> = task
|
||||
.synspec_params
|
||||
.as_ref()
|
||||
.and_then(|v| serde_json::from_value::<SynspecConfig>(v.clone()).ok());
|
||||
.and_then(|v| serde_json::from_value::<SynspecInput>(v.clone()).ok());
|
||||
|
||||
let runner = ExecutionRunner::new(runtime, slot_work_dir.clone());
|
||||
// 执行链来源(优先级):
|
||||
// 1. TaskSpec.tlusty_chain_params(用户在 YAML `tlusty_chain:` 配置的多阶段 ChainStep
|
||||
// 数组,由 scheduler 序列化注入)——非空时优先使用,使用户能细粒度控制 niter/chmax/
|
||||
// metals 等阶段参数。
|
||||
// 2. default_chain_for_strategy(current_strategy) 兜底——按策略名(cold_run/seed_step)
|
||||
// 选预设默认链(runner.rs 的 default_cold_chain / default_seed_chain)。
|
||||
// 历史:Phase 6 起仅用 default 链(用户 config.chain 被忽略,是死字段);本次接通后
|
||||
// 用户配置真正生效,default 链降级为兜底。旧 MQ payload(无 tlusty_chain_params 字段)
|
||||
// 反序列化为 None → 回退 default 链,行为与旧版完全一致(向后兼容)。
|
||||
let chain = resolve_execution_chain(
|
||||
current_strategy,
|
||||
&task.tlusty_chain_params,
|
||||
&task.task_id.to_string(),
|
||||
);
|
||||
// TLUSTY 输入文件全局参数(NFREAD/ions 表/nst extra_keys 等)。
|
||||
// None → runner 用代码内硬编码默认(向后兼容)。
|
||||
let tlusty_input =
|
||||
task.tlusty_input_params.as_ref().and_then(|v| {
|
||||
match serde_json::from_value::<TlustyInput>(v.clone()) {
|
||||
Ok(t) => Some(t),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"任务 {} 的 tlusty_input_params 反序列化失败,回退默认输入: {}",
|
||||
task.task_id, e
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
let summary = runner
|
||||
.run_model_with_timeout(
|
||||
&task.params,
|
||||
// 用权威的 point_name(DB grid_points.name 列,源精度正确)作为模型名,
|
||||
// 而非 task.params.model_name()(后者经 DB REAL 列回读已丢精度 "5.0"→"5")。
|
||||
&task.point_name,
|
||||
task.task_type.clone(),
|
||||
// custom_chain 恒为 None:执行链由 task_type 决定(ColdRun→default_cold_chain、
|
||||
// SeedStep→default_seed_chain),节点端不再做「缺种子回退冷启动链」的降级。
|
||||
None,
|
||||
current_strategy,
|
||||
Some(chain),
|
||||
seed_atmos_path.as_deref(),
|
||||
synspec_cfg.as_ref(),
|
||||
// 阶段独立配置开关(见 docs/task_engine_decoupling_design.md §5)。
|
||||
@@ -180,17 +207,18 @@ pub async fn execute_task(
|
||||
task.synspec_config.enabled,
|
||||
task.timeout_sec,
|
||||
shutdown,
|
||||
tlusty_input.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!(
|
||||
"完成计算任务 {} (网格点: {}, 收敛状态: {})",
|
||||
task.task_id, task.point_name, summary.converged
|
||||
"完成计算任务 {} (网格点: {}, 结果可用: {})",
|
||||
task.task_id, task.point_name, summary.result_valid
|
||||
);
|
||||
|
||||
// Read seed bytes if converged and clean
|
||||
// Read seed bytes if result usable and clean
|
||||
let mut seed_bytes: Option<Vec<u8>> = None;
|
||||
if summary.converged && !summary.atmosphere_has_nan {
|
||||
if summary.result_valid && !summary.atmosphere_has_nan {
|
||||
let model_sub_dir = slot_work_dir.join(&summary.name);
|
||||
let candidates = [
|
||||
model_sub_dir.join(format!("{}.7", summary.name)),
|
||||
@@ -350,15 +378,23 @@ pub async fn save_result_artifacts(result_dir: &Path, slot_work_dir: &Path, name
|
||||
);
|
||||
}
|
||||
|
||||
/// 归档目录不做数量上限治理:所有已算网格点的完整产物(.spec/.cont/.iden/各阶段
|
||||
/// 快照/日志/种子二进制等)一律永久保留,避免 LRU 淘汰导致科学产物丢失
|
||||
/// (2026-08-02 修正:撤销 1bfa240 引入的 MAX_RESULT_MODELS=200 LRU 上限)。
|
||||
// 归档目录不做数量上限治理:所有已算网格点的完整产物(.spec/.cont/.iden/各阶段
|
||||
// 快照/日志/种子二进制等)一律永久保留,避免 LRU 淘汰导致科学产物丢失
|
||||
// (2026-08-02 修正:撤销 1bfa240 引入的 MAX_RESULT_MODELS=200 LRU 上限)。
|
||||
|
||||
/// `.seed_cache/` 内保留的 `.seed.7` 文件上限。超过则按 mtime 删除最旧的。
|
||||
/// 典型网格内活跃种子点数量有限,8 足以覆盖常用邻域且把磁盘占用控制在 ~8 个种子文件。
|
||||
const MAX_SEED_CACHE_FILES: usize = 8;
|
||||
///
|
||||
/// 审查修复 #N5:上限可经环境变量 `DCTS_SEED_CACHE_MAX` 覆盖(默认 8)。多工作流或密集
|
||||
/// 网格下常用邻域种子可能超过 8 个,硬编码上限会导致反复从 server 下载,增加负载。
|
||||
fn seed_cache_max_files() -> usize {
|
||||
std::env::var("DCTS_SEED_CACHE_MAX")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.filter(|n: &usize| *n > 0)
|
||||
.unwrap_or(8)
|
||||
}
|
||||
|
||||
/// LRU 清理种子缓存目录:当 `.seed.7` 文件数超过 `MAX_SEED_CACHE_FILES` 时,
|
||||
/// LRU 清理种子缓存目录:当 `.seed.7` 文件数超过上限(`DCTS_SEED_CACHE_MAX`,默认 8)时,
|
||||
/// 按 mtime 升序删除最旧的若干个,直到不超过上限。仅统计 `.seed.7`,忽略 `.tmp` 中间文件。
|
||||
/// 任何 IO 错误均降级为 warn,不阻断主流程。
|
||||
pub async fn cleanup_seed_cache(seed_dir: &Path) {
|
||||
@@ -391,13 +427,14 @@ pub async fn cleanup_seed_cache(seed_dir: &Path) {
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if entries.len() <= MAX_SEED_CACHE_FILES {
|
||||
let max_files = seed_cache_max_files();
|
||||
if entries.len() <= max_files {
|
||||
return;
|
||||
}
|
||||
|
||||
// 按 mtime 升序(最旧在前),删除超出上限的最旧文件
|
||||
entries.sort_by_key(|(mtime, _)| *mtime);
|
||||
let to_remove = entries.len().saturating_sub(MAX_SEED_CACHE_FILES);
|
||||
let to_remove = entries.len().saturating_sub(max_files);
|
||||
for (_, path) in entries.into_iter().take(to_remove) {
|
||||
match tokio::fs::remove_file(&path).await {
|
||||
Ok(()) => info!("LRU 清理种子缓存文件: {}", path.display()),
|
||||
@@ -411,6 +448,43 @@ pub async fn cleanup_seed_cache(seed_dir: &Path) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析任务要执行的大气链(冷启动链 / 种子热启动链)。
|
||||
///
|
||||
/// 优先级(修复回归):
|
||||
/// - `current_strategy == "seed_step"` → 强制 `default_seed_chain()`(seed_nc→nl)。
|
||||
/// 自定义 `tlusty_chain` 是冷启动链:首步 lte 的 `ltgray=T` 会删除 fort.8、丢弃已下载的
|
||||
/// 热启动种子(runner.rs 阶段 fort.8 准备逻辑)。scheduler 在派发与回退两条路径都注入
|
||||
/// 同一个 `tlusty_chain`,若 seed_step 也沿用自定义链,会把种子回退退化成本地冷启动,
|
||||
/// 丢失热启动语义。故种子链固定走内置默认,仅在 cold_run 等冷策略下信任用户自定义链。
|
||||
/// - 其余策略 → 优先 TaskSpec.tlusty_chain_params(用户 YAML `tlusty_chain:` 配置),非空即用;
|
||||
/// 为空/反序列化失败 → `default_chain_for_strategy(current_strategy)` 兜底。
|
||||
fn resolve_execution_chain(
|
||||
current_strategy: &str,
|
||||
tlusty_chain_params: &Option<serde_json::Value>,
|
||||
task_id: &str,
|
||||
) -> Vec<ChainStep> {
|
||||
if current_strategy == "seed_step" {
|
||||
common::runner::default_seed_chain()
|
||||
} else {
|
||||
tlusty_chain_params
|
||||
.as_ref()
|
||||
.and_then(
|
||||
|v| match serde_json::from_value::<Vec<ChainStep>>(v.clone()) {
|
||||
Ok(c) => Some(c),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"任务 {} 的 tlusty_chain_params 反序列化失败,回退 default 链: {}",
|
||||
task_id, e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.filter(|c| !c.is_empty())
|
||||
.unwrap_or_else(|| common::runner::default_chain_for_strategy(current_strategy))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -429,7 +503,7 @@ mod tests {
|
||||
name: params.model_name(),
|
||||
params,
|
||||
stages: vec![],
|
||||
converged: true,
|
||||
result_valid: true,
|
||||
final_max_relc: Some(0.0005),
|
||||
final_chmax: None,
|
||||
seed: None,
|
||||
@@ -442,6 +516,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn labels(chain: &[ChainStep]) -> Vec<String> {
|
||||
chain.iter().map(|s| s.label.clone()).collect()
|
||||
}
|
||||
|
||||
/// P1 回归防护:seed_step 即使注入自定义冷启动链,也必须强制走种子热启动默认链
|
||||
/// (seed_nc→nl),否则首步 lte(ltgray=T) 会删 fort.8、丢弃已下载种子,回退退化成本地冷启动。
|
||||
#[test]
|
||||
fn seed_step_ignores_custom_cold_chain() {
|
||||
let custom = serde_json::json!([
|
||||
{"label": "lte", "lte": "T", "ltgray": "T", "ilvlin": 0, "niter": 0},
|
||||
{"label": "nc", "lte": "F", "ltgray": "F", "ilvlin": 0, "niter": 10},
|
||||
{"label": "nl", "lte": "F", "ltgray": "F", "ilvlin": 100, "niter": 100},
|
||||
]);
|
||||
|
||||
let chain = resolve_execution_chain("seed_step", &Some(custom), "t1");
|
||||
assert_eq!(labels(&chain), vec!["seed_nc", "nl"]);
|
||||
// 首步必须是非灰 LTE(ltgray=F),否则会删 fort.8 丢弃种子。
|
||||
assert_eq!(chain[0].ltgray, "F");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cold_run_uses_custom_chain_when_provided() {
|
||||
let custom = serde_json::json!([
|
||||
{"label": "lte", "lte": "T", "ltgray": "T", "ilvlin": 0, "niter": 0},
|
||||
{"label": "nc", "lte": "F", "ltgray": "F", "ilvlin": 0, "niter": 10},
|
||||
{"label": "nl", "lte": "F", "ltgray": "F", "ilvlin": 100, "niter": 100},
|
||||
]);
|
||||
|
||||
let chain = resolve_execution_chain("cold_run", &Some(custom), "t2");
|
||||
assert_eq!(labels(&chain), vec!["lte", "nc", "nl"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cold_run_falls_back_to_default_when_no_custom_chain() {
|
||||
let chain = resolve_execution_chain("cold_run", &None, "t3");
|
||||
assert_eq!(labels(&chain), vec!["lte", "nc", "nl"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cleanup_slot_work_dir() {
|
||||
let temp_dir =
|
||||
|
||||
Reference in New Issue
Block a user