feat(all): seed_step_stab 稳定化种子链与同族种子轮换、ladder/自适应 Teff/丰度轴延拓三级回退与梯级种子入库复用、热启动首末比豁免、workflow 完成翻转回退链阻塞修复、大气收敛权威统计与 M14 回填
收敛攻坚(81/400 失败点根因与实测,见 docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md): - runner: 新增 seed_step_stab 策略链——同物理族(同 Teff/logg/logHe)CNO 邻居种子 + DPSILG=3.0 λ 算子欠松弛 + POPZER=1E-10 微布居置零(联动 POPZR2/RADZER 同值 + NITZER=1),针对 20kK He 富大气 He I/II 电离前沿布居极限环 - runner: 三级链内延拓回退(主链与 nl_direct 全败后自动触发): · 固定 ladder 步进——plan_ladder_steps 按归一化间隔选轴,Δlogg≤0.25/ΔTeff≤2.5kK,≤4 步 · 自适应 Teff 延拓——步长 1250K 起、成功 ×1.5 恢复、失败二分至 25K 折叠墙,≤48 步 · C/N 丰度轴延拓——高温域(Teff>30kK)专用,严格同族种子沿 C(优先)/N 轴 0.2 dex 起步 延拓阶段 NITER 下限提至 300(慢收敛 waypoint 迭代饥饿误判修复) - runner: 稳定化多档回退(DPSILG/POPZER 三档互补,联合回收 41/65); 域门控 Teff≤30kK——高温高金属域实测旋钮致散(17 拍爆至 4e16),域外跳过 - 梯级种子持久化: 收敛中间模型登记 ladder_seeds 随上报落 server seeds 表 (任务失败也上传,合成名 _ladder 与真实网格点零冲突),簇内相邻失败点自动复用 调度与执行: - scheduler: 策略解析新增 seed_step_stab 臂——find_exact_family_seed_from_db 严格 同物理族判定(不做 global 退化,防 ladder 中间种子 ΔTeff≤5000K 误命中), 排除本点历史已用种子实现重试轮换;链在 stab 耗尽时轮换未试过同族邻居重派 - executor: seed_step_stab 补种子下载(漏列曾致 78 任务假失败,seed_nc 无 fort.8 崩溃); Teff>30kK 域外自动降级普通种子链 收敛判据: - conv_check: 热启动豁免——首拍 max_relc<1(种子已近解)时首末比 1e3 判据数学上 不可达,豁免后交五重物理硬门槛裁决(修复 nl_ladder 0.038→6e-4 物理全过被误杀); 冷启动仍受判据门控 workflow 生命周期: - workflows/tasks: 完成 flip 增加「未消费回退链」阻塞子句——failed 点策略链未耗尽 或链尾 seed_step_stab 尚有未试过同族种子时不得置 completed(修复最后活跃点 cold 失败上报抢先 flip、still_running 守卫拦截后续策略永不派发);按 failed_stage 归因 (synspec 失败行只看 synspec 链,防 stale 审计副本永久卡死)+ json_valid 脏行防护 统计与前端: - 统计新增权威口径 tlusty_converged(不按策略拆)与 seed_step_stab_converged 分项, 前端详情页色带/概览卡消费权威总数并新增稳定化青色段(修复 stab 收敛点漏计, 生产 9137/9216 差额);M14 迁移回填历史 completed 点的 tlusty_status 文档: - 新增 failed81 POPZER/DPSILG 制胜配方根因分析、Windows 节点经跳板 RDP 运维手册; failed400 增补 ladder 生产化实现与第二轮 121 残点实测矩阵
This commit is contained in:
@@ -81,6 +81,8 @@ pub async fn report_task(
|
||||
) -> Result<impl IntoResponse, crate::api::AppError> {
|
||||
let mut report_json: Option<TaskReport> = None;
|
||||
let mut seed_file_data: Option<Vec<u8>> = None;
|
||||
// ladder 中间梯级种子(可重复字段):文件名(去 .7)即合成种子名,参数从名称解析。
|
||||
let mut ladder_seed_files: Vec<(String, Vec<u8>)> = Vec::new();
|
||||
let mut multipart_error = false;
|
||||
|
||||
// 遍历全部 multipart 字段。旧实现 `while let Ok(Some(field))` 在首个字段读取错误时
|
||||
@@ -114,6 +116,24 @@ pub async fn report_task(
|
||||
multipart_error = true;
|
||||
}
|
||||
}
|
||||
} else if field_name == "ladder_seed" {
|
||||
// 文件名即合成种子名(<model_name>_ladder.7)
|
||||
let file_name = field
|
||||
.file_name()
|
||||
.unwrap_or("")
|
||||
.trim_end_matches(".7")
|
||||
.to_string();
|
||||
match field.bytes().await {
|
||||
Ok(bytes) => {
|
||||
if !file_name.is_empty() {
|
||||
ladder_seed_files.push((file_name, bytes.to_vec()));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("读取 ladder_seed 字段失败: {}", e);
|
||||
multipart_error = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => break,
|
||||
@@ -329,6 +349,40 @@ pub async fn report_task(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ladder 中间梯级种子持久化(2026-08-17):node 端仅上报「收敛 + 无 NaN」的
|
||||
// 中间模型;此处原子落盘 + 按 _ladder 合成名入 seeds 表(ON CONFLICT 仅刷新
|
||||
// file_path,绝不触碰真实网格点种子行)。名称解析失败 → 丢弃并告警(防脏名)。
|
||||
for (seed_name, bytes) in &ladder_seed_files {
|
||||
let Some(lp) = common::models::GridPointParams::parse_point_name(seed_name) else {
|
||||
warn!("ladder 种子名 {} 无法解析参数,丢弃", seed_name);
|
||||
continue;
|
||||
};
|
||||
let ladder_dir = Path::new(&state.seeds_dir).join(seed_name);
|
||||
if fs::create_dir_all(&ladder_dir).await.is_err() {
|
||||
warn!("ladder 种子目录创建失败: {}", ladder_dir.display());
|
||||
continue;
|
||||
}
|
||||
let tmp = ladder_dir.join(format!("{}.7.{}.tmp", seed_name, uuid::Uuid::new_v4().simple()));
|
||||
let path = ladder_dir.join(format!("{}.7", seed_name));
|
||||
if fs::write(&tmp, bytes).await.is_ok() && fs::rename(&tmp, &path).await.is_ok() {
|
||||
if let Err(e) = state
|
||||
.db
|
||||
.insert_seed_named(seed_name, &lp, &path.to_string_lossy())
|
||||
.await
|
||||
{
|
||||
warn!("ladder 种子 {} 入库失败: {}", seed_name, e);
|
||||
} else {
|
||||
info!(
|
||||
"ladder 中间种子入库: {} (t{} g{} he{})",
|
||||
seed_name,
|
||||
lp.teff.value(),
|
||||
lp.logg.value(),
|
||||
lp.loghe.value()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state_changed
|
||||
|
||||
@@ -576,8 +576,10 @@ impl Database {
|
||||
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
|
||||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' THEN 1 ELSE 0 END) AS tlusty_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold_run_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed_step_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step_stab' THEN 1 ELSE 0 END) AS seed_step_stab_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'failed' THEN 1 ELSE 0 END) AS tlusty_failed,
|
||||
SUM(CASE WHEN synspec_status = 'converged' THEN 1 ELSE 0 END) AS synspec_converged,
|
||||
SUM(CASE WHEN synspec_status = 'failed' THEN 1 ELSE 0 END) AS synspec_failed,
|
||||
@@ -586,7 +588,7 @@ impl Database {
|
||||
params![name],
|
||||
|r| {
|
||||
let n = |i: usize| -> i64 { r.get::<_, Option<i64>>(i).unwrap_or(None).unwrap_or(0) };
|
||||
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11)))
|
||||
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11), n(12), n(13)))
|
||||
},
|
||||
),
|
||||
None => conn.query_row(
|
||||
@@ -597,8 +599,10 @@ impl Database {
|
||||
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
|
||||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' THEN 1 ELSE 0 END) AS tlusty_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold_run_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed_step_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step_stab' THEN 1 ELSE 0 END) AS seed_step_stab_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'failed' THEN 1 ELSE 0 END) AS tlusty_failed,
|
||||
SUM(CASE WHEN synspec_status = 'converged' THEN 1 ELSE 0 END) AS synspec_converged,
|
||||
SUM(CASE WHEN synspec_status = 'failed' THEN 1 ELSE 0 END) AS synspec_failed,
|
||||
@@ -607,7 +611,7 @@ impl Database {
|
||||
[],
|
||||
|r| {
|
||||
let n = |i: usize| -> i64 { r.get::<_, Option<i64>>(i).unwrap_or(None).unwrap_or(0) };
|
||||
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11)))
|
||||
Ok((n(0), n(1), n(2), n(3), n(4), n(5), n(6), n(7), n(8), n(9), n(10), n(11), n(12), n(13)))
|
||||
},
|
||||
),
|
||||
}?;
|
||||
@@ -618,8 +622,10 @@ impl Database {
|
||||
running,
|
||||
completed,
|
||||
failed,
|
||||
tlusty_converged,
|
||||
cold_run_converged,
|
||||
seed_step_converged,
|
||||
seed_step_stab_converged,
|
||||
tlusty_failed,
|
||||
synspec_converged,
|
||||
synspec_failed,
|
||||
@@ -633,8 +639,13 @@ impl Database {
|
||||
"running": running,
|
||||
"completed": completed,
|
||||
"failed": failed,
|
||||
// tlusty_converged(2026-08-25):大气收敛的权威总数(不按 method 拆分)。
|
||||
// 前端「N 大气收敛」此前用 cold+seed 之和——seed_step_stab(2026-08-18 引入)
|
||||
// 收敛的 70 点被漏计(生产 9137/9216 差额主因)。
|
||||
"tlusty_converged": tlusty_converged,
|
||||
"cold_run_converged": cold_run_converged,
|
||||
"seed_step_converged": seed_step_converged,
|
||||
"seed_step_stab_converged": seed_step_stab_converged,
|
||||
"tlusty_failed": tlusty_failed,
|
||||
"synspec_converged": synspec_converged,
|
||||
"synspec_failed": synspec_failed,
|
||||
@@ -733,6 +744,8 @@ impl Database {
|
||||
failed,
|
||||
cold_run_converged: g("cold_run_converged"),
|
||||
seed_step_converged: g("seed_step_converged"),
|
||||
tlusty_converged: g("tlusty_converged"),
|
||||
seed_step_stab_converged: g("seed_step_stab_converged"),
|
||||
tlusty_failed: g("tlusty_failed"),
|
||||
synspec_converged: g("synspec_converged"),
|
||||
synspec_failed: g("synspec_failed"),
|
||||
|
||||
@@ -710,6 +710,12 @@ pub struct WorkflowListStats {
|
||||
pub running: i64,
|
||||
pub cold_run_converged: i64,
|
||||
pub seed_step_converged: i64,
|
||||
/// TLUSTY 阶段收敛总数(tlusty_status='converged')——权威口径,语义同
|
||||
/// WorkflowStats.tlusty_converged(2026-08-25 补,见彼处注释)。
|
||||
#[serde(default)]
|
||||
pub tlusty_converged: i64,
|
||||
#[serde(default)]
|
||||
pub seed_step_stab_converged: i64,
|
||||
/// TLUSTY 阶段发散点数(tlusty_status='failed')。
|
||||
pub tlusty_failed: i64,
|
||||
/// SYNSPEC 阶段收敛点数(synspec_status='converged')。
|
||||
@@ -754,6 +760,15 @@ pub struct WorkflowStats {
|
||||
pub failed: i64,
|
||||
pub cold_run_converged: i64,
|
||||
pub seed_step_converged: i64,
|
||||
/// TLUSTY 阶段收敛总数(tlusty_status='converged',不按 method 拆分)——权威口径。
|
||||
/// 前端「N 大气收敛」应消费此字段;cold/seed/stab 为策略细分。2026-08-25 补:
|
||||
/// 此前前端用 cold+seed 之和,seed_step_stab(2026-08-18 引入)收敛点被漏计
|
||||
/// (生产 9137/9216 差额主因)。
|
||||
#[serde(default)]
|
||||
pub tlusty_converged: i64,
|
||||
/// TLUSTY 阶段以 seed_step_stab 策略收敛的点数(稳定化/waypoint 种子链)。
|
||||
#[serde(default)]
|
||||
pub seed_step_stab_converged: i64,
|
||||
/// TLUSTY 阶段发散点数(tlusty_status='failed')。
|
||||
pub tlusty_failed: i64,
|
||||
/// SYNSPEC 阶段收敛点数(synspec_status='converged')。
|
||||
@@ -1199,6 +1214,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
db.upsert_point_summary(&summary.name, "wf_a", &summary, "seed_step")
|
||||
@@ -1215,12 +1231,16 @@ mod tests {
|
||||
assert_eq!(all["completed"], 1);
|
||||
assert_eq!(all["cold_run_converged"], 0);
|
||||
assert_eq!(all["seed_step_converged"], 1);
|
||||
// tlusty_converged(2026-08-25):不按 method 拆分的权威总数。
|
||||
assert_eq!(all["tlusty_converged"], 1);
|
||||
assert_eq!(all["seed_step_stab_converged"], 0);
|
||||
// 单工作流 wf_a:1 pending + 1 seed_step 收敛
|
||||
let a = db.get_grid_summary_stats(Some("wf_a")).await.unwrap();
|
||||
assert_eq!(a["total"], 2);
|
||||
assert_eq!(a["pending"], 1);
|
||||
assert_eq!(a["queued"], 0);
|
||||
assert_eq!(a["seed_step_converged"], 1);
|
||||
assert_eq!(a["tlusty_converged"], 1);
|
||||
// 不存在的工作流:0
|
||||
let none = db
|
||||
.get_grid_summary_stats(Some("nonexistent"))
|
||||
@@ -1264,6 +1284,120 @@ mod tests {
|
||||
assert_eq!(m.unwrap().name, exact.model_name());
|
||||
}
|
||||
|
||||
/// find_exact_family_seed_from_db 严格同物理族判定(2026-08-19 生产修复回归):
|
||||
/// ladder 中间种子(如 teff=22500)即便 CNO 距离为 0,也因 Teff 不同被排除;
|
||||
/// 同 Teff/logg/logHe 的 CNO 邻居正常命中;排除目标自身名。
|
||||
#[tokio::test]
|
||||
async fn test_find_exact_family_seed_strict_same_family() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db = Database::new(&temp_dir.path().join("seed_stab_db.db").to_string_lossy())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let target = GridPointParams {
|
||||
teff: 20000.0.into(),
|
||||
logg: 6.5.into(),
|
||||
loghe: 2.0.into(),
|
||||
logc: (-4.0).into(),
|
||||
logn: (-3.0).into(),
|
||||
logo: (-4.0).into(),
|
||||
};
|
||||
// ladder 中间种子:同 logg/logHe、CNO 完全一致,但 teff=22500 —— 必须排除。
|
||||
let ladder = GridPointParams {
|
||||
teff: 22500.0.into(),
|
||||
logg: 6.5.into(),
|
||||
loghe: 2.0.into(),
|
||||
logc: (-4.0).into(),
|
||||
logn: (-3.0).into(),
|
||||
logo: (-4.0).into(),
|
||||
};
|
||||
// 合法 CNO 邻居:同 Teff/logg/logHe,Δo=1。
|
||||
let cno_neighbor = GridPointParams {
|
||||
teff: 20000.0.into(),
|
||||
logg: 6.5.into(),
|
||||
loghe: 2.0.into(),
|
||||
logc: (-4.0).into(),
|
||||
logn: (-3.0).into(),
|
||||
logo: (-3.0).into(),
|
||||
};
|
||||
db.insert_seed(&ladder, "/tmp/ladder.7").await.unwrap();
|
||||
db.insert_seed(&cno_neighbor, "/tmp/neighbor.7").await.unwrap();
|
||||
|
||||
let m = db
|
||||
.find_exact_family_seed_from_db(&target, &[target.model_name()])
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(m.is_some(), "应命中 CNO 邻居");
|
||||
assert_eq!(
|
||||
m.unwrap().name,
|
||||
cno_neighbor.model_name(),
|
||||
"不得选 teff 不同的 ladder 中间种子(即便其 CNO 距离为 0)"
|
||||
);
|
||||
|
||||
// 无严格同族候选时返回 None(不退化到 global)。
|
||||
let lonely = GridPointParams {
|
||||
teff: 30000.0.into(),
|
||||
logg: 5.0.into(),
|
||||
loghe: (-2.0).into(),
|
||||
logc: (-2.0).into(),
|
||||
logn: (-2.0).into(),
|
||||
logo: (-2.0).into(),
|
||||
};
|
||||
let none = db
|
||||
.find_exact_family_seed_from_db(&lonely, &[lonely.model_name()])
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(none.is_none());
|
||||
}
|
||||
|
||||
|
||||
/// 种子轮换回归(2026-08-20 修复):find_exact_family_seed_from_db 排除列表
|
||||
/// 应使调用方在重试间轮换到下一个未试过的同族 CNO 邻居;全部排除后返回 None。
|
||||
#[tokio::test]
|
||||
async fn test_find_exact_family_seed_rotation() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db = Database::new(&temp_dir.path().join("seed_rot_db.db").to_string_lossy())
|
||||
.await
|
||||
.unwrap();
|
||||
let base = |logc: f64, logn: f64, logo: f64| GridPointParams {
|
||||
teff: 20000.0.into(),
|
||||
logg: 6.0.into(),
|
||||
loghe: 2.0.into(),
|
||||
logc: logc.into(),
|
||||
logn: logn.into(),
|
||||
logo: logo.into(),
|
||||
};
|
||||
let target = base(-3.0, -3.0, -1.0);
|
||||
let n1 = base(-3.0, -2.0, -1.0);
|
||||
let n2 = base(-3.0, -3.0, -2.0);
|
||||
db.insert_seed(&n1, "/tmp/rot_n1.7").await.unwrap();
|
||||
db.insert_seed(&n2, "/tmp/rot_n2.7").await.unwrap();
|
||||
|
||||
let self_name = target.model_name();
|
||||
// 首次:选距离最近的未排除邻居。
|
||||
let first = db
|
||||
.find_exact_family_seed_from_db(&target, &[self_name.clone()])
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("应命中 CNO 邻居");
|
||||
// 排除首个后:轮换到另一个邻居。
|
||||
let second = db
|
||||
.find_exact_family_seed_from_db(&target, &[self_name, first.name.clone()])
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("排除首个邻居后应轮换到第二个邻居");
|
||||
assert_ne!(first.name, second.name);
|
||||
// 全部排除后:None(终态 failed,不死循环)。
|
||||
let none = db
|
||||
.find_exact_family_seed_from_db(
|
||||
&target,
|
||||
&[target.model_name(), first.name, second.name],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(none.is_none(), "邻居全部试过后应返回 None");
|
||||
}
|
||||
|
||||
/// "种子回退仅一次"守卫语义(2026-08-02 涡旋事故定稿):has_seed_step_attempt
|
||||
/// 统计**一切** seed_step 行(含 pending)。pending 行在新架构下只有两种来源:
|
||||
/// (a) 真在途(排队/已领用)——计数它正是对在途回退的去重,挡住救援途中迟到失败
|
||||
@@ -3265,6 +3399,347 @@ mod tests {
|
||||
assert_eq!(item.status, "completed");
|
||||
}
|
||||
|
||||
/// 完成 flip 的「未消费回退链」阻塞回归(2026-08-22 修复):
|
||||
/// 最后一个活跃点失败时,其最新 failed 行策略链弹掉失败首项后仍有顺位
|
||||
/// (长度 >1)→ workflow 不得置 completed(否则 trigger_strategy_fallback
|
||||
/// 的 still_running 守卫拦截,链上 seed_step/seed_step_stab 永不派发;
|
||||
/// 生产实证 sdB_cno/t60000_g5.0_he-4_c-4_n-4_o-1,8-22 04:00:50)。
|
||||
/// 链耗尽(长度 1)后放行翻转。
|
||||
#[tokio::test]
|
||||
async fn test_workflow_flip_blocked_by_unconsumed_fallback_chain() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db = Database::new(
|
||||
&temp_dir
|
||||
.path()
|
||||
.join("flip_block_db.db")
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let params = GridPointParams {
|
||||
teff: 60000.0.into(),
|
||||
logg: 5.0.into(),
|
||||
loghe: (-4.0).into(),
|
||||
logc: (-4.0).into(),
|
||||
logn: (-4.0).into(),
|
||||
logo: (-1.0).into(),
|
||||
};
|
||||
let name = params.model_name();
|
||||
db.upsert_workflow("wf_fb2", None, "config", "idle")
|
||||
.await
|
||||
.unwrap();
|
||||
db.update_workflow_status("wf_fb2", "running").await.unwrap();
|
||||
db.upsert_grid_point(¶ms, 0, "wf_fb2").await.unwrap();
|
||||
// 点终态 failed、无 pending/queued/running——旧条件已满足 flip。
|
||||
db.update_grid_status(&name, GridPointStatus::Failed, "wf_fb2")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 最新 failed 行携带 3 顺位链(cold_run 刚失败,尚余 seed_step/seed_step_stab)。
|
||||
let t1 = uuid::Uuid::new_v4();
|
||||
db.insert_task(&common::models::TaskSpec {
|
||||
task_id: t1,
|
||||
point_name: name.clone(),
|
||||
params: params.clone(),
|
||||
timeout_sec: 7200,
|
||||
workflow_name: Some("wf_fb2".to_string()),
|
||||
wave: 0,
|
||||
tlusty_config: PhaseConfig {
|
||||
strategies: vec![
|
||||
"cold_run".to_string(),
|
||||
"seed_step".to_string(),
|
||||
"seed_step_stab".to_string(),
|
||||
],
|
||||
..PhaseConfig::default_tlusty()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let pool = db.pool.clone();
|
||||
let tid = t1.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = pool.get().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status = 'failed', completed_at = datetime('now') \
|
||||
WHERE task_id = ?1",
|
||||
params![tid],
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
db.sync_all_running_workflows_completion().await.unwrap();
|
||||
let wf = db.get_workflow("wf_fb2").await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
wf.status, "running",
|
||||
"存在未消费回退链的 failed 点时,workflow 不应翻转 completed"
|
||||
);
|
||||
|
||||
// 回退推进到链尾(更新的 failed 行仅剩单顺位)→ 阻塞解除,翻转放行。
|
||||
let t2 = uuid::Uuid::new_v4();
|
||||
db.insert_task(&common::models::TaskSpec {
|
||||
task_id: t2,
|
||||
point_name: name.clone(),
|
||||
params: params.clone(),
|
||||
timeout_sec: 7200,
|
||||
workflow_name: Some("wf_fb2".to_string()),
|
||||
wave: 0,
|
||||
tlusty_config: PhaseConfig {
|
||||
strategies: vec!["seed_step_stab".to_string()],
|
||||
..PhaseConfig::default_tlusty()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let pool = db.pool.clone();
|
||||
let tid = t2.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = pool.get().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status = 'failed', completed_at = datetime('now'), \
|
||||
created_at = datetime('now', '+1 hour') WHERE task_id = ?1",
|
||||
params![tid],
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
db.sync_all_running_workflows_completion().await.unwrap();
|
||||
let wf2 = db.get_workflow("wf_fb2").await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
wf2.status, "completed",
|
||||
"链耗尽(长度 1)后应恢复完成翻转"
|
||||
);
|
||||
}
|
||||
|
||||
/// 完成 flip 的「种子轮换待执行」阻塞回归(2026-08-22 审查补充):
|
||||
/// 链尾 [seed_step_stab] 失败 + 存在未试过的同物理族干净种子 → 不翻转
|
||||
/// (否则 rotation 臂被 still_running 守卫吞掉,waypoint 只有一次机会);
|
||||
/// 同族种子全部用过后放行翻转。
|
||||
#[tokio::test]
|
||||
async fn test_workflow_flip_blocked_by_seed_rotation_pending() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db = Database::new(
|
||||
&temp_dir
|
||||
.path()
|
||||
.join("flip_rot_db.db")
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let params = GridPointParams {
|
||||
teff: 60000.0.into(),
|
||||
logg: 5.0.into(),
|
||||
loghe: (-4.0).into(),
|
||||
logc: (-4.0).into(),
|
||||
logn: (-4.0).into(),
|
||||
logo: (-1.0).into(),
|
||||
};
|
||||
let name = params.model_name();
|
||||
db.upsert_workflow("wf_rot", None, "config", "idle")
|
||||
.await
|
||||
.unwrap();
|
||||
db.update_workflow_status("wf_rot", "running").await.unwrap();
|
||||
db.upsert_grid_point(¶ms, 0, "wf_rot").await.unwrap();
|
||||
db.update_grid_status(&name, GridPointStatus::Failed, "wf_rot")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 链尾 [seed_step_stab] 失败行。
|
||||
let t1 = uuid::Uuid::new_v4();
|
||||
db.insert_task(&common::models::TaskSpec {
|
||||
task_id: t1,
|
||||
point_name: name.clone(),
|
||||
params: params.clone(),
|
||||
timeout_sec: 7200,
|
||||
workflow_name: Some("wf_rot".to_string()),
|
||||
wave: 0,
|
||||
tlusty_config: PhaseConfig {
|
||||
strategies: vec!["seed_step_stab".to_string()],
|
||||
..PhaseConfig::default_tlusty()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let pool = db.pool.clone();
|
||||
let tid = t1.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = pool.get().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status = 'failed', completed_at = datetime('now') \
|
||||
WHERE task_id = ?1",
|
||||
params![tid],
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// 无同族种子 → 无可轮换 → 放行翻转。
|
||||
db.sync_all_running_workflows_completion().await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_workflow("wf_rot").await.unwrap().unwrap().status,
|
||||
"completed",
|
||||
"无同族候选种子时链尾失败应放行翻转"
|
||||
);
|
||||
|
||||
// 重启场景:workflow 回 running,注入一个未用过的同族干净种子。
|
||||
db.update_workflow_status("wf_rot", "running").await.unwrap();
|
||||
let neighbor = GridPointParams {
|
||||
logn: (-3.489798).into(),
|
||||
..params.clone()
|
||||
};
|
||||
db.insert_seed_named(
|
||||
"t60000_g5_he-4_c-4_n-3.489798_o-1_ladder",
|
||||
&neighbor,
|
||||
"/tmp/seed.7",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.sync_all_running_workflows_completion().await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_workflow("wf_rot").await.unwrap().unwrap().status,
|
||||
"running",
|
||||
"存在未试过的同族种子时,链尾 seed_step_stab 失败应阻塞翻转(rotation 待执行)"
|
||||
);
|
||||
|
||||
// 轮换消费该种子(任务行携带 seed_point_name)→ 候选耗尽 → 放行。
|
||||
let t2 = uuid::Uuid::new_v4();
|
||||
db.insert_task(&common::models::TaskSpec {
|
||||
task_id: t2,
|
||||
point_name: name.clone(),
|
||||
params: params.clone(),
|
||||
seed_point_name: Some("t60000_g5_he-4_c-4_n-3.489798_o-1_ladder".to_string()),
|
||||
timeout_sec: 7200,
|
||||
workflow_name: Some("wf_rot".to_string()),
|
||||
wave: 0,
|
||||
tlusty_config: PhaseConfig {
|
||||
strategies: vec!["seed_step_stab".to_string()],
|
||||
..PhaseConfig::default_tlusty()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let pool = db.pool.clone();
|
||||
let tid = t2.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = pool.get().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status = 'failed', completed_at = datetime('now'), \
|
||||
created_at = datetime('now', '+1 hour') WHERE task_id = ?1",
|
||||
params![tid],
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
db.sync_all_running_workflows_completion().await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_workflow("wf_rot").await.unwrap().unwrap().status,
|
||||
"completed",
|
||||
"同族种子全部用过(无新候选)后应放行翻转"
|
||||
);
|
||||
}
|
||||
|
||||
/// 完成 flip 的阶段归因回归(2026-08-22 审查补充):synspec 失败行的
|
||||
/// tlusty_strategies 是完整审计副本(scheduler.rs synspec 回退刻意保留),
|
||||
/// 阻塞判定必须按 failed_stage='synspec' 只看 synspec 链——否则 synspec
|
||||
/// 工作流会因 stale 审计副本永久卡 running。
|
||||
#[tokio::test]
|
||||
async fn test_workflow_flip_stage_attribution_ignores_tlusty_audit_copy() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db = Database::new(
|
||||
&temp_dir
|
||||
.path()
|
||||
.join("flip_syn_db.db")
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let params = GridPointParams {
|
||||
teff: 30000.0.into(),
|
||||
logg: 5.0.into(),
|
||||
loghe: (-2.0).into(),
|
||||
logc: (-2.0).into(),
|
||||
logn: (-2.0).into(),
|
||||
logo: (-2.0).into(),
|
||||
};
|
||||
let name = params.model_name();
|
||||
db.upsert_workflow("wf_syn2", None, "config", "idle")
|
||||
.await
|
||||
.unwrap();
|
||||
db.update_workflow_status("wf_syn2", "running").await.unwrap();
|
||||
db.upsert_grid_point(¶ms, 0, "wf_syn2").await.unwrap();
|
||||
db.update_grid_status(&name, GridPointStatus::Failed, "wf_syn2")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// synspec 失败行:synspec 链已耗尽(长度 1),但 tlusty 链是完整审计
|
||||
// 副本(长度 3)——阶段归因下不应阻塞。
|
||||
let t1 = uuid::Uuid::new_v4();
|
||||
db.insert_task(&common::models::TaskSpec {
|
||||
task_id: t1,
|
||||
point_name: name.clone(),
|
||||
params: params.clone(),
|
||||
timeout_sec: 7200,
|
||||
workflow_name: Some("wf_syn2".to_string()),
|
||||
wave: 0,
|
||||
tlusty_config: PhaseConfig {
|
||||
strategies: vec![
|
||||
"cold_run".to_string(),
|
||||
"seed_step".to_string(),
|
||||
"seed_step_stab".to_string(),
|
||||
],
|
||||
..PhaseConfig::default_tlusty()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let pool = db.pool.clone();
|
||||
let tid = t1.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = pool.get().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status = 'failed', failed_stage = 'synspec', \
|
||||
synspec_strategies = '[\"standard\"]', completed_at = datetime('now') \
|
||||
WHERE task_id = ?1",
|
||||
params![tid],
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
db.sync_all_running_workflows_completion().await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_workflow("wf_syn2").await.unwrap().unwrap().status,
|
||||
"completed",
|
||||
"synspec 链已耗尽的失败行,其 tlusty 审计副本不应阻塞完成翻转"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_take_pending_node_token_atomic() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
@@ -3695,6 +4170,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
let report1 = TaskReport {
|
||||
@@ -3761,6 +4237,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
let report2 = TaskReport {
|
||||
@@ -3927,6 +4404,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
let report1 = TaskReport {
|
||||
@@ -3984,6 +4462,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: Some("synspec 失败".into()),
|
||||
};
|
||||
let report2 = TaskReport {
|
||||
@@ -4108,6 +4587,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
let report1 = TaskReport {
|
||||
@@ -4183,6 +4663,7 @@ mod tests {
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
let report2 = TaskReport {
|
||||
|
||||
@@ -196,4 +196,60 @@ impl Database {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// 仅在 exact_family(同 Teff/logg/logHe、仅 CNO 不同)内找种子,供
|
||||
/// `seed_step_stab` 稳定化策略使用(2026-08-18,docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md)。
|
||||
///
|
||||
/// 与 `find_best_seed_from_db` 的区别:不做 global 退化——稳定化配方的验证前提
|
||||
/// 是种子与目标仅差 CNO 丰度(微扰),跨 Teff/logg 的种子不在其适用域内。
|
||||
/// `exclude` 排除种子名列表(至少含目标点自身名,防止误把自己旧产物当种子)。
|
||||
/// 2026-08-20 种子轮换修复:稳定化配方对种子**逐点敏感**——同 CNO 距离的不同
|
||||
/// 邻居(换 N 还是换 O)收敛性不同。调用方传入本点历史任务已用过的种子名,
|
||||
/// 使每次 seed_step_stab 重试轮换到下一个未试过的同族邻居,而非重复同一组合。
|
||||
pub async fn find_exact_family_seed_from_db(
|
||||
&self,
|
||||
target: &GridPointParams,
|
||||
exclude: &[String],
|
||||
) -> Result<Option<common::seed_finder::SeedMatch>> {
|
||||
let exact_candidates: Vec<SeedCacheItem> = {
|
||||
let idx_lock = self.seed_index.read().await;
|
||||
let keys = SeedBucketKey::from_params(target);
|
||||
let mut out = Vec::new();
|
||||
for key in keys {
|
||||
if let Some(bucket) = idx_lock.get(&key) {
|
||||
out.extend(bucket.iter().cloned());
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
|
||||
let mut best: Option<(String, std::path::PathBuf, f64)> = None;
|
||||
for item in &exact_candidates {
|
||||
if exclude.iter().any(|ex| &item.point_name == ex) {
|
||||
continue;
|
||||
}
|
||||
// 严格同物理族(2026-08-19 生产修复):必须 Teff/logg/logHe 逐值相同、
|
||||
// 仅 CNO 不同。不能用 calculate_seed_distance 的 is_exact——其判定
|
||||
// 容忍 ΔTeff≤5000K(一个网格档),会把 ladder 中间种子(如
|
||||
// t22500_g6.5_...)当成同族并因 CNO 距离 0 排最前,而稳定化配方
|
||||
// (POPZER+DPSILG)的验证前提是种子与目标仅差丰度微扰、同温同重力。
|
||||
let strict_same_family = (item.params.teff.value() - target.teff.value()).abs()
|
||||
< 1e-9
|
||||
&& (item.params.logg.value() - target.logg.value()).abs() < 1e-9
|
||||
&& (item.params.loghe.value() - target.loghe.value()).abs() < 1e-9;
|
||||
if !strict_same_family {
|
||||
continue;
|
||||
}
|
||||
let (_, d) = common::seed_finder::calculate_seed_distance(&item.params, target);
|
||||
if best.is_none() || d < best.as_ref().unwrap().2 {
|
||||
let path = std::path::PathBuf::from(&item.file_path);
|
||||
best = Some((item.point_name.clone(), path, d));
|
||||
}
|
||||
}
|
||||
Ok(best.map(|(name, path, distance)| common::seed_finder::SeedMatch {
|
||||
name,
|
||||
path,
|
||||
distance,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,6 +292,33 @@ impl Database {
|
||||
/// `strategy` 传 Some("seed_step")/Some("cold_run") 时按当前策略过滤(Phase 6 起派生:
|
||||
/// 比较 `json_extract(tlusty_strategies, '$[0]')`,不再依赖已删除的 task_type 列),
|
||||
/// None 不过滤。生产调用仅传 None,Some 分支供测试断言用。
|
||||
/// 本点历史任务已用过的全部种子名(DISTINCT、非 NULL)。
|
||||
/// 2026-08-20 种子轮换修复:`seed_step_stab` 重试时排除这些种子,
|
||||
/// 使 `find_exact_family_seed_from_db` 轮换到未试过的同族 CNO 邻居。
|
||||
pub async fn list_used_seed_names(
|
||||
&self,
|
||||
point_name: &str,
|
||||
workflow_name: &str,
|
||||
) -> Result<Vec<String>> {
|
||||
let pool = self.pool.clone();
|
||||
let point = point_name.to_string();
|
||||
let wf = workflow_name.to_string();
|
||||
tokio::task::spawn_blocking(move || -> Result<Vec<String>> {
|
||||
let conn = pool.get().map_err(|e| anyhow::anyhow!("DB Pool Error: {}", e))?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT DISTINCT seed_point_name FROM tasks
|
||||
WHERE point_name = ?1 AND workflow_name = ?2 AND seed_point_name IS NOT NULL",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![point, wf], |r| r.get::<_, String>(0))?;
|
||||
let mut out = Vec::new();
|
||||
for r in rows {
|
||||
out.push(r?);
|
||||
}
|
||||
Ok(out)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
pub async fn has_pending_tasks_for_point(
|
||||
&self,
|
||||
point_name: &str,
|
||||
@@ -621,8 +648,14 @@ impl Database {
|
||||
// 审查修复 #S1:workflow 完成 flip 并入同一事务(原实现在 tx.commit() 后单独 UPDATE,
|
||||
// 崩溃窗口期 task 已终态但 workflow 卡 running;且 `let _ =` 丢弃 I/O 错误)。
|
||||
// 现在事务内更新,与 task/grid_points 结算原子提交,错误正常传播。
|
||||
//
|
||||
// 2026-08-22 修复:「无 pending/queued/running」之外还须无「未消费回退链」的
|
||||
// failed 点(UNCONSUMED_FALLBACK_BLOCKER_SQL)——否则最后一个活跃点 cold_run
|
||||
// 失败的这份上报会先把 workflow 置 completed,紧随其后的
|
||||
// trigger_strategy_fallback 被 still_running 守卫拦截,链上后续策略永不派发。
|
||||
tx.execute(
|
||||
"UPDATE workflows
|
||||
&format!(
|
||||
"UPDATE workflows
|
||||
SET status = 'completed', updated_at = datetime('now')
|
||||
WHERE name = ?1
|
||||
AND status = 'running'
|
||||
@@ -631,7 +664,10 @@ impl Database {
|
||||
SELECT 1 FROM grid_points
|
||||
WHERE workflow_name = workflows.name
|
||||
AND status IN ('pending', 'queued', 'running')
|
||||
)",
|
||||
)
|
||||
{}",
|
||||
super::workflows::UNCONSUMED_FALLBACK_BLOCKER_SQL
|
||||
),
|
||||
params![wf],
|
||||
)?;
|
||||
|
||||
|
||||
@@ -2,6 +2,78 @@
|
||||
//! `impl Database` 的 工作流 域方法。共享基础设施(Database struct、连接管理、类型、helper)见父模块 `super`(crate::db)。
|
||||
use super::*;
|
||||
|
||||
/// 工作流完成 flip 的「未消费回退」阻塞子句(两条 flip SQL 共用:
|
||||
/// 本文件后台对账 + tasks.rs `record_task_report` 事务内 flip)。
|
||||
///
|
||||
/// 2026-08-22 修复:最后一批活跃点失败时,flip 只看「无 pending/queued/running」,
|
||||
/// 而 failed 点的失败上报**先于**策略链回退被处理——workflow 被提前置 completed,
|
||||
/// `trigger_strategy_fallback` 的 still_running 守卫随即拦截,cold_run 之后的
|
||||
/// seed_step/seed_step_stab 永不派发(生产实证:sdB_cno 仅剩
|
||||
/// t60000_g5.0_he-4_c-4_n-4_o-1 时 cold 失败即终局)。
|
||||
///
|
||||
/// 阻塞语义(对每个 failed 点取最新 failed/timeout 任务行,行选择口径镜像
|
||||
/// `pop_stage_strategy_for_fallback`:ORDER BY created_at DESC, rowid DESC):
|
||||
///
|
||||
/// 1. **未消费策略链**:按该行 failed_stage 归因(镜像 pop 的 stage→列映射,
|
||||
/// NULL 归因 tlusty)取对应策略链,json 长度 >1——弹掉刚失败首项后仍有
|
||||
/// 顺位。链随每次失败上报严格变短,终会耗尽,不会永久阻塞。
|
||||
/// 2. **种子轮换待执行**(2026-08-22 审查补充):链尾 [`seed_step_stab`] 失败
|
||||
/// 且仍存在未试过的同物理族干净种子(teff/logg/loghe 逐值相等、is_clean=1、
|
||||
/// 非自身、不在本点历史 seed_point_name 集合内)——镜像 scheduler.rs
|
||||
/// rotation 臂(trigger_strategy_fallback 内)的派发条件,否则链尾失败时
|
||||
/// flip 仍会先于 rotation 吞掉换种重试。轮换每次消耗一个种子,used 集合
|
||||
/// 单调增长,终会耗尽同族种子,不会永久阻塞。
|
||||
///
|
||||
/// JSON 防护:json_array_length/json_extract 对非法 JSON **抛错**而非返回 NULL
|
||||
/// (SQLite 3.45 实测)——若不加防护,一条脏行会让 record_task_report 整个
|
||||
/// 结算事务失败。列 NOT NULL DEFAULT 且唯一写入方是 serde_json,正常不可达;
|
||||
/// json_valid 守卫下脏行按长度 0 处理(保守放行翻转)。
|
||||
pub(crate) const UNCONSUMED_FALLBACK_BLOCKER_SQL: &str = "AND NOT EXISTS (
|
||||
SELECT 1 FROM grid_points gp
|
||||
JOIN tasks t
|
||||
ON t.point_name = gp.name
|
||||
AND t.workflow_name = gp.workflow_name
|
||||
AND t.status IN ('failed', 'timeout')
|
||||
AND t.rowid = (
|
||||
SELECT t2.rowid FROM tasks t2
|
||||
WHERE t2.point_name = gp.name
|
||||
AND t2.workflow_name = gp.workflow_name
|
||||
AND t2.status IN ('failed', 'timeout')
|
||||
ORDER BY t2.created_at DESC, t2.rowid DESC LIMIT 1
|
||||
)
|
||||
WHERE gp.workflow_name = workflows.name
|
||||
AND gp.status = 'failed'
|
||||
AND (
|
||||
CASE WHEN t.failed_stage = 'synspec'
|
||||
THEN CASE WHEN json_valid(t.synspec_strategies)
|
||||
THEN json_array_length(t.synspec_strategies)
|
||||
ELSE 0 END
|
||||
ELSE CASE WHEN json_valid(t.tlusty_strategies)
|
||||
THEN json_array_length(t.tlusty_strategies)
|
||||
ELSE 0 END
|
||||
END > 1
|
||||
OR (
|
||||
json_valid(t.tlusty_strategies)
|
||||
AND json_array_length(t.tlusty_strategies) = 1
|
||||
AND json_extract(t.tlusty_strategies, '$[0]') = 'seed_step_stab'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM seeds s
|
||||
WHERE s.is_clean = 1
|
||||
AND s.point_name != gp.name
|
||||
AND s.teff = gp.teff
|
||||
AND s.logg = gp.logg
|
||||
AND s.loghe = gp.loghe
|
||||
AND s.point_name NOT IN (
|
||||
SELECT t3.seed_point_name FROM tasks t3
|
||||
WHERE t3.point_name = gp.name
|
||||
AND t3.workflow_name = gp.workflow_name
|
||||
AND t3.seed_point_name IS NOT NULL
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)";
|
||||
|
||||
impl Database {
|
||||
pub async fn upsert_workflow(
|
||||
&self,
|
||||
@@ -47,8 +119,10 @@ impl Database {
|
||||
// L6 修复:原 `let _ =` 丢弃 r2d2/rusqlite 错误——若该后台对账 UPDATE 失败,
|
||||
// 工作流可持续卡在 running 无任何提示。现记录错误(仍返回 Ok 不中断主流程,
|
||||
// 因为每份上报内的 workflow-completion flip 才是主路径,见 record_task_report)。
|
||||
// 「未消费回退链」子句见 UNCONSUMED_FALLBACK_BLOCKER_SQL(2026-08-22 修复)。
|
||||
if let Err(e) = conn.execute(
|
||||
"UPDATE workflows
|
||||
&format!(
|
||||
"UPDATE workflows
|
||||
SET status = 'completed', updated_at = datetime('now')
|
||||
WHERE status = 'running'
|
||||
AND EXISTS (SELECT 1 FROM grid_points WHERE workflow_name = workflows.name)
|
||||
@@ -56,7 +130,10 @@ impl Database {
|
||||
SELECT 1 FROM grid_points
|
||||
WHERE workflow_name = workflows.name
|
||||
AND status IN ('pending', 'queued', 'running')
|
||||
)",
|
||||
)
|
||||
{}",
|
||||
UNCONSUMED_FALLBACK_BLOCKER_SQL
|
||||
),
|
||||
[],
|
||||
) {
|
||||
tracing::error!("后台对账:同步 running 工作流完成态失败: {}", e);
|
||||
@@ -90,6 +167,7 @@ impl Database {
|
||||
}
|
||||
|
||||
// 一次 GROUP BY 聚合全部工作流的网格计数并回填(不做逐工作流查询,无 N+1)。
|
||||
// tlusty 总收敛与 stab 分项为 2026-08-25 补(口径见 WorkflowStats.tlusty_converged)。
|
||||
let mut agg_stmt = conn.prepare(
|
||||
"SELECT workflow_name,
|
||||
COUNT(*) AS total,
|
||||
@@ -98,6 +176,8 @@ impl Database {
|
||||
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'cold_run' THEN 1 ELSE 0 END) AS cold,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step' THEN 1 ELSE 0 END) AS seed,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' THEN 1 ELSE 0 END) AS tlusty_converged,
|
||||
SUM(CASE WHEN tlusty_status = 'converged' AND tlusty_success_method = 'seed_step_stab' THEN 1 ELSE 0 END) AS stab,
|
||||
SUM(CASE WHEN tlusty_status = 'failed' THEN 1 ELSE 0 END) AS tlusty_failed,
|
||||
SUM(CASE WHEN synspec_status = 'converged' THEN 1 ELSE 0 END) AS synspec_converged,
|
||||
SUM(CASE WHEN synspec_status = 'failed' THEN 1 ELSE 0 END) AS synspec_failed,
|
||||
@@ -114,10 +194,12 @@ impl Database {
|
||||
running: r.get::<_, Option<i64>>(4)?.unwrap_or(0),
|
||||
cold_run_converged: r.get::<_, Option<i64>>(5)?.unwrap_or(0),
|
||||
seed_step_converged: r.get::<_, Option<i64>>(6)?.unwrap_or(0),
|
||||
tlusty_failed: r.get::<_, Option<i64>>(7)?.unwrap_or(0),
|
||||
synspec_converged: r.get::<_, Option<i64>>(8)?.unwrap_or(0),
|
||||
synspec_failed: r.get::<_, Option<i64>>(9)?.unwrap_or(0),
|
||||
synspec_pending: r.get::<_, Option<i64>>(10)?.unwrap_or(0),
|
||||
tlusty_converged: r.get::<_, Option<i64>>(7)?.unwrap_or(0),
|
||||
seed_step_stab_converged: r.get::<_, Option<i64>>(8)?.unwrap_or(0),
|
||||
tlusty_failed: r.get::<_, Option<i64>>(9)?.unwrap_or(0),
|
||||
synspec_converged: r.get::<_, Option<i64>>(10)?.unwrap_or(0),
|
||||
synspec_failed: r.get::<_, Option<i64>>(11)?.unwrap_or(0),
|
||||
synspec_pending: r.get::<_, Option<i64>>(12)?.unwrap_or(0),
|
||||
},
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -176,6 +176,32 @@ pub const MIGRATIONS: &[Migration] = &[
|
||||
detect: |c| Ok(!has_column(c, "grid_points", "success_method")?),
|
||||
up: &["ALTER TABLE grid_points DROP COLUMN success_method"],
|
||||
},
|
||||
// M14(2026-08-25,大气收敛统计补漏):Phase 5b 阶段列(tlusty_status)上线前完成的历史
|
||||
// 点该列为 NULL——但 tlusty_success_method 仅在整管线成功(tlusty_enabled=1 且收敛)时
|
||||
// 写入(record_task_report 成功分支),故 completed + method 非空 ⟹ 大气必已收敛。
|
||||
// 回填 'converged',否则 stats 的 CASE WHEN tlusty_status='converged' 漏计
|
||||
// (生产实证:sdB_cno 9216 点中 9 个此形态,前端 9137/9216 差额的一部分)。
|
||||
Migration {
|
||||
version: 14,
|
||||
name: "backfill-tlusty-status-for-completed",
|
||||
detect: |c| {
|
||||
let n: i64 = c.query_row(
|
||||
"SELECT COUNT(*) FROM grid_points
|
||||
WHERE status = 'completed'
|
||||
AND COALESCE(tlusty_success_method, '') != ''
|
||||
AND COALESCE(tlusty_status, '') = ''",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
Ok(n == 0)
|
||||
},
|
||||
up: &[
|
||||
"UPDATE grid_points SET tlusty_status = 'converged' \
|
||||
WHERE status = 'completed' \
|
||||
AND COALESCE(tlusty_success_method, '') != '' \
|
||||
AND COALESCE(tlusty_status, '') = ''",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/// 当前 schema 版本(`PRAGMA user_version`)。
|
||||
@@ -562,4 +588,54 @@ mod tests {
|
||||
);
|
||||
assert_eq!(current_version(&conn).unwrap(), 13);
|
||||
}
|
||||
|
||||
/// M14(2026-08-25):completed + tlusty_success_method 非空 + tlusty_status 空
|
||||
/// 的历史点回填 'converged'(method 仅在整管线成功时写入);其它形态不动。
|
||||
#[test]
|
||||
fn m14_backfills_tlusty_status_for_legacy_completed() {
|
||||
let mut conn = mem_conn();
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE grid_points (
|
||||
name TEXT NOT NULL,
|
||||
workflow_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
tlusty_success_method TEXT,
|
||||
synspec_success_method TEXT,
|
||||
tlusty_status TEXT,
|
||||
synspec_status TEXT,
|
||||
pending_strategies TEXT
|
||||
);
|
||||
INSERT INTO grid_points (name, workflow_name, status, tlusty_success_method, tlusty_status) VALUES
|
||||
('legacy_ok', 'wf', 'completed', 'cold_run', NULL),
|
||||
('legacy_seed', 'wf', 'completed', 'seed_step', ''),
|
||||
('already_failed', 'wf', 'failed', 'cold_run', 'failed'),
|
||||
('already_conv', 'wf', 'completed', 'cold_run', 'converged'),
|
||||
('no_method', 'wf', 'completed', NULL, NULL);",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// 仅跑 M14(全量 MIGRATIONS 含 M1 的 tasks 迁移,本夹具只建 grid_points)。
|
||||
let m14 = MIGRATIONS.iter().find(|m| m.version == 14).unwrap();
|
||||
apply_migrations_with(&mut conn, &[*m14]).unwrap();
|
||||
|
||||
let read = |c: &Connection, name: &str| -> Option<String> {
|
||||
c.query_row(
|
||||
"SELECT tlusty_status FROM grid_points WHERE name = ?1",
|
||||
rusqlite::params![name],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(read(&conn, "legacy_ok").as_deref(), Some("converged"));
|
||||
assert_eq!(read(&conn, "legacy_seed").as_deref(), Some("converged"));
|
||||
// failed 点 / 已有状态 / 无 method 的点不受影响。
|
||||
assert_eq!(read(&conn, "already_failed").as_deref(), Some("failed"));
|
||||
assert_eq!(read(&conn, "already_conv").as_deref(), Some("converged"));
|
||||
assert_eq!(read(&conn, "no_method"), None);
|
||||
assert_eq!(current_version(&conn).unwrap(), 14);
|
||||
|
||||
// 幂等:再跑一遍 detect 命中(无待回填行)→ 仅推进版本,不改数据。
|
||||
apply_migrations_with(&mut conn, &[*m14]).unwrap();
|
||||
assert_eq!(read(&conn, "no_method"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +360,7 @@ impl GridScheduler {
|
||||
/// 「弹出再压回」保持原序而陷入死循环。
|
||||
async fn resolve_dispatchable_chain(
|
||||
db: &Database,
|
||||
workflow_name: &str,
|
||||
params: &GridPointParams,
|
||||
mut chain: Vec<String>,
|
||||
) -> Result<(Vec<String>, Option<String>)> {
|
||||
@@ -394,6 +395,36 @@ impl GridScheduler {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 稳定化种子步进(2026-08-18,docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md):
|
||||
// 仅在 exact_family(同 Teff/logg/logHe、仅 CNO 不同)内找种子——稳定化配方
|
||||
// (POPZER+DPSILG)的验证前提是种子与目标仅差丰度微扰,跨 Teff/logg 种子
|
||||
// 不在适用域。排除目标点自身名(防止把自己旧产物当种子)。
|
||||
// 2026-08-20 种子轮换:配方对种子逐点敏感(同距离换 N/换 O 邻居收敛性不同),
|
||||
// 排除本点历史任务已用过的种子,重试时轮换到下一个未试过的同族邻居。
|
||||
Some("seed_step_stab") => {
|
||||
let mut exclude = vec![params.model_name()];
|
||||
exclude.extend(db.list_used_seed_names(¶ms.model_name(), workflow_name).await?);
|
||||
let found_seed = db
|
||||
.find_exact_family_seed_from_db(params, &exclude)
|
||||
.await?;
|
||||
match found_seed {
|
||||
Some(seed) => {
|
||||
if !skipped.is_empty() {
|
||||
chain.extend(skipped);
|
||||
}
|
||||
return Ok((chain, Some(seed.name.clone())));
|
||||
}
|
||||
None if chain.len() > 1 => {
|
||||
let head = chain.remove(0);
|
||||
skipped.push(head);
|
||||
info!("策略解析:seed_step_stab 无同物理族 CNO 邻居种子,暂存顺位");
|
||||
}
|
||||
None => {
|
||||
chain.clear();
|
||||
return Ok((chain, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 空链或非 seed_step 顺位:可直接派发。先前跳过的 seed_step 追加到链尾,
|
||||
// 保留为后续回退顺位(空链由调用方判空处理)。
|
||||
_ => {
|
||||
@@ -561,7 +592,7 @@ impl GridScheduler {
|
||||
};
|
||||
let (dispatch_chain, seed_point_name) = if tlusty_cfg.enabled {
|
||||
let (chain, seed) =
|
||||
Self::resolve_dispatchable_chain(&self.db, ¶ms, base_chain).await?;
|
||||
Self::resolve_dispatchable_chain(&self.db, workflow_name, ¶ms, base_chain).await?;
|
||||
if chain.is_empty() {
|
||||
// enabled 阶段无可派发顺位(基础链全为无种子的 seed_step)→ 打回 pending,
|
||||
// 待近邻种子出现后由下轮调度自愈派发(不是终态,不能被卡死在 queued)。
|
||||
@@ -896,13 +927,6 @@ impl GridScheduler {
|
||||
popped,
|
||||
policy: row_policy,
|
||||
} = snap;
|
||||
if rest_strategies.is_empty() {
|
||||
info!(
|
||||
"策略回退:网格点 {} 的 {} 策略链已耗尽(弹出最后项 {}),保持 failed 终态",
|
||||
name, stage_label, popped
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 2026-08-04 语义修正:策略链回退**只由策略链驱动**(弹出失败首项、派发下一顺位,
|
||||
// 链耗尽保持 failed),不再受执行策略门控——策略只决定启动工作流时对历史终态点的
|
||||
@@ -929,6 +953,83 @@ impl GridScheduler {
|
||||
.await
|
||||
.unwrap_or((None, None, None, None, None, None, None, None));
|
||||
|
||||
// 2026-08-20 种子轮换:seed_step_stab 常为链上最后一项,失败弹出后链空。
|
||||
// 稳定化配方对种子逐点敏感(同 CNO 距离、换不同元素方向的邻居收敛性不同,
|
||||
// 见 docs/failed81_cno_seed_popzer_dpsilg_2026_08_18.md §九)——首个选中
|
||||
// 的邻居失败不代表配方无效。排除本点历史已用种子后若还有未试过的同族
|
||||
// 邻居,则以 seed_step_stab 单顺位重链重派。轮换次数天然受同族邻居数约束:
|
||||
// 全部试过后此处不再命中,落入下方 failed 终态,无死循环。
|
||||
if rest_strategies.is_empty() && failed_stage != "synspec" && popped == "seed_step_stab" {
|
||||
let mut exclude = vec![params.model_name()];
|
||||
exclude.extend(self.db.list_used_seed_names(name, workflow_name).await?);
|
||||
if let Some(fresh_seed) = self
|
||||
.db
|
||||
.find_exact_family_seed_from_db(params, &exclude)
|
||||
.await?
|
||||
{
|
||||
info!(
|
||||
"策略回退:工作流 {} 网格点 {} seed_step_stab 失败但存在未试过的同族邻居种子 {},轮换种子重派(已试过 {:?})",
|
||||
workflow_name, name, fresh_seed.name, &exclude[1..]
|
||||
);
|
||||
// 轮换任务显式注入新种子(绕过 resolve 的种子查找,避免其再次
|
||||
// 命中同一邻居)。
|
||||
let mut stab_cfg = tlusty_cfg.clone();
|
||||
stab_cfg.strategies = vec!["seed_step_stab".to_string()];
|
||||
stab_cfg.policy = row_policy.clone();
|
||||
let task_spec = TaskSpec {
|
||||
task_id: Uuid::new_v4(),
|
||||
point_name: name.to_string(),
|
||||
params: params.clone(),
|
||||
seed_point_name: Some(fresh_seed.name),
|
||||
timeout_sec,
|
||||
workflow_name: Some(workflow_name.to_string()),
|
||||
wave: 0,
|
||||
tlusty_config: stab_cfg,
|
||||
synspec_config: synspec_cfg.clone(),
|
||||
synspec_params,
|
||||
tlusty_chain_params: tlusty_chain.clone(),
|
||||
seed_chain_params: seed_chain.clone(),
|
||||
tlusty_input_params: tlusty_input.clone(),
|
||||
atmosphere_ref: None,
|
||||
energy_tolerance,
|
||||
temp_max_factor,
|
||||
temp_floor,
|
||||
temp_ceiling,
|
||||
emflux_tolerance,
|
||||
convergence_min_ratio,
|
||||
bfac_max,
|
||||
bfac_min,
|
||||
linelist: linelist.clone(),
|
||||
};
|
||||
self.db.insert_task(&task_spec).await?;
|
||||
self.db
|
||||
.update_grid_status(name, common::models::GridPointStatus::Queued, workflow_name)
|
||||
.await?;
|
||||
if let Err(e) = self.queue.push_task(&task_spec).await {
|
||||
let _ = self
|
||||
.db
|
||||
.update_grid_status(
|
||||
name,
|
||||
common::models::GridPointStatus::Pending,
|
||||
workflow_name,
|
||||
)
|
||||
.await;
|
||||
let _ = self.queue.remove_task(&task_spec.task_id.to_string()).await;
|
||||
let _ = self.db.delete_task(&task_spec.task_id).await;
|
||||
return Err(e);
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
if rest_strategies.is_empty() {
|
||||
info!(
|
||||
"策略回退:网格点 {} 的 {} 策略链已耗尽(弹出最后项 {}),保持 failed 终态",
|
||||
name, stage_label, popped
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// SYNSPEC 链回退:重试光谱合成。无邻居种子门控(大气来自目标点自身既有产物,
|
||||
// 见 docs/task_engine_decoupling_design.md §5)——旧实现把 synspec 失败误归因到
|
||||
// TLUSTY 链并做无意义的种子搜索(修复审查 #2)。
|
||||
@@ -1011,7 +1112,7 @@ impl GridScheduler {
|
||||
// pending_rest:resolve 前的基础剩余链(含全部 seed_step 顺位),H1 空链分支据此记录标记。
|
||||
let pending_rest = rest_strategies.clone();
|
||||
let (rest_strategies, seed_point_name) =
|
||||
Self::resolve_dispatchable_chain(&self.db, params, rest_strategies).await?;
|
||||
Self::resolve_dispatchable_chain(&self.db, workflow_name, params, rest_strategies).await?;
|
||||
if rest_strategies.is_empty() {
|
||||
// H1 修复:resolve_dispatchable_chain 仅在「剩余顺位全为无近邻种子的 seed_step」
|
||||
// 时返回空(调用方已在上方 line 790 排除了真耗尽)。此时**不能**保持 failed 终态:
|
||||
@@ -2909,6 +3010,7 @@ tlusty_stage:\n enabled: true\n policy: skip_converged\n strategies: [seed_st
|
||||
// 无种子 → 重复 seed_step 链应判空(不派发、不死循环)。
|
||||
let (chain, seed) = GridScheduler::resolve_dispatchable_chain(
|
||||
&db,
|
||||
"wf",
|
||||
¶ms,
|
||||
vec!["seed_step".to_string(), "seed_step".to_string()],
|
||||
)
|
||||
@@ -2921,6 +3023,7 @@ tlusty_stage:\n enabled: true\n policy: skip_converged\n strategies: [seed_st
|
||||
// 两个无种子 seed_step 保留为回退顺位且链首可派发。
|
||||
let (chain2, seed2) = GridScheduler::resolve_dispatchable_chain(
|
||||
&db,
|
||||
"wf",
|
||||
¶ms,
|
||||
vec![
|
||||
"seed_step".to_string(),
|
||||
|
||||
@@ -28,6 +28,7 @@ async fn mark_imported(db: &Database, name: &str, wf: &str, params: &GridPointPa
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
db.upsert_point_summary(name, wf, &summary, method)
|
||||
@@ -1866,6 +1867,7 @@ async fn dispatch_and_report(
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
let report = common::models::TaskReport {
|
||||
|
||||
@@ -41,6 +41,7 @@ fn make_converged_summary(name: &str, params: &GridPointParams) -> ModelSummary
|
||||
temp_check: None,
|
||||
emflux_check: None,
|
||||
bfac_check: None,
|
||||
ladder_seeds: Vec::new(),
|
||||
note: None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user