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:
fmq
2026-08-06 20:51:21 +08:00
parent cd370d88e7
commit d16b3d3cdc
61 changed files with 10268 additions and 5881 deletions
+20 -37
View File
@@ -212,14 +212,9 @@ impl SqliteTaskQueue {
}
};
// 旧版兼容归一化(见 models.rs::normalize_compat 文档):
// 旧 MQ 在途消息可能只含 task_type、无 tlusty_config 字段。serde default
// 会把 tlusty_config.strategies 填为完整默认链 [cold_run, seed_step]
// 导致旧 SeedStep 热启动消息首项被误判为 cold_run。normalize_compat
// 据 task_type 校正首项策略。在 pop 出队后立即调用,保证节点拿到的是
// 语义正确的策略链。对新版消息(已显式设置 tlusty_config)无副作用。
let mut task = task;
task.normalize_compat();
// Phase 6P8):task_type 兼容字段与 normalize_compat 整体废弃。
// 升级前须确认队列为空(docs/database_refactor_design.md §8.8)——残留
// 旧 payload 的 strategies 会按 serde default 填成默认链,无法再校正。
// 记录任务归属:claim 时写入领用方 node_id,供 report 阶段校验,
// 杜绝「节点 A 领用、节点 B 上报」的跨节点伪造结果投毒。
@@ -391,13 +386,10 @@ impl SqliteTaskQueue {
let mut stmt = conn.prepare(
"DELETE FROM task_queue WHERE workflow_name = ?1 AND status = 'pending' RETURNING task_id",
)?;
let rows = stmt.query_map(params![wf_owned], |row| row.get::<_, String>(0))?;
let mut ids = Vec::new();
for r in rows {
if let Ok(id) = r {
ids.push(id);
}
}
let ids: Vec<String> = stmt
.query_map(params![wf_owned], |row| row.get::<_, String>(0))?
.filter_map(Result::ok)
.collect();
Ok(ids)
})
.await??;
@@ -431,7 +423,7 @@ impl SqliteTaskQueue {
#[cfg(test)]
mod tests {
use super::*;
use common::models::{GridPointParams, TaskType};
use common::models::GridPointParams;
use uuid::Uuid;
/// H1 修复:队列库迁移必须把历史遗留的 NULL workflow_name 行回填为 '__legacy__'
@@ -467,7 +459,9 @@ mod tests {
}
// 打开队列库(触发迁移:补列检查 + NULL workflow_name 回填)。
let queue = SqliteTaskQueue::new(&db_path.to_string_lossy()).await.unwrap();
let queue = SqliteTaskQueue::new(&db_path.to_string_lossy())
.await
.unwrap();
let wf: String = {
let conn = queue.pool.get().unwrap();
@@ -503,7 +497,6 @@ mod tests {
logn: (-2.0).into(),
logo: (-2.0).into(),
},
task_type: TaskType::ColdRun,
seed_point_name: None,
timeout_sec: 3600,
workflow_name: Some("test_wf".to_string()),
@@ -552,7 +545,6 @@ mod tests {
task_id,
point_name: params.model_name(),
params: params.clone(),
task_type: TaskType::ColdRun,
seed_point_name: None,
timeout_sec: 60,
workflow_name: None,
@@ -616,7 +608,6 @@ mod tests {
task_id,
point_name: params.model_name(),
params: params.clone(),
task_type: TaskType::ColdRun,
seed_point_name: None,
timeout_sec: 7200,
workflow_name: Some("wf_rq".to_string()),
@@ -650,7 +641,7 @@ mod tests {
.await
.unwrap();
assert_eq!(
claim_a.map(|(p, w)| (p, w)),
claim_a,
Some((params.model_name(), Some("wf_rq".to_string()))),
"requeue 后原节点仍持归属,应放行"
);
@@ -690,7 +681,6 @@ mod tests {
logn: (-2.0).into(),
logo: (-2.0).into(),
},
task_type: TaskType::ColdRun,
seed_point_name: None,
timeout_sec: 60,
workflow_name: Some(wf.to_string()),
@@ -803,20 +793,18 @@ mod tests {
assert!(queue.pop_task("n1").await.unwrap().is_none());
}
/// 旧版 MQ 在途消息兼容(二次审查 critical 修复验证):
/// 旧消息只含 task_type=seed_step、无 tlusty_config 字段。serde default 会把
/// strategies 填为 [cold_run, seed_step],首项 cold_run 与 task_type 不符
/// pop_task 出队时须调用 normalize_compat() 把首项校正为 seed_step
/// 否则旧热启动消息会被节点误当冷启动执行。
/// Phase 6P8):旧版消息兼容(normalize_compat)整体废弃。升级前确认队列为空
/// docs/database_refactor_design.md §8.8)后,旧 payload 的 task_type 键被 serde 忽略,
/// strategies 回落到默认链——出队仍正常,只是不再有 task_type 校正
#[tokio::test]
async fn test_pop_normalizes_legacy_seed_step_message() {
async fn test_pop_legacy_payload_without_task_type_correction() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir.path().join("legacy.db");
let queue = SqliteTaskQueue::new(&db_path.to_string_lossy())
.await
.unwrap();
// 直接写一行旧版 payload(仅 task_type无 tlusty_config/synspec_config 字段)。
// 旧版 payload 仍携带 task_type 键(已被忽略)+ 无 tlusty_config(回落默认链)。
let legacy_task_id = uuid::Uuid::new_v4();
let legacy_payload = serde_json::json!({
"task_id": legacy_task_id.to_string(),
@@ -848,16 +836,11 @@ mod tests {
let popped = queue.pop_task("n1").await.unwrap();
let task = popped.expect("旧版消息应能正常出队");
// normalize_compat 应把 strategies 校正[seed_step](与 task_type 一致)
assert_eq!(
task.tlusty_config.strategies,
vec!["seed_step".to_string()],
"旧 seed_step 消息经 normalize_compat 后首项应为 seed_step"
);
// 无 task_type 校正:strategies 为 serde default 默认链,首项 cold_run
assert_eq!(
task.tlusty_config.current_strategy("cold_run"),
"seed_step",
"current_strategy 应为 seed_step(而非默认链的 cold_run"
"cold_run",
"旧 payload 无显式 strategies,回落到默认链首项"
);
}