feat(all): 源精度命名体系、工作流可观测台、节点停用管理与白名单归档
核心变更:
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
This commit is contained in:
+120
-19
@@ -69,12 +69,13 @@ impl SqliteTaskQueue {
|
||||
created_at DATETIME NOT NULL,
|
||||
claimed_at DATETIME,
|
||||
workflow_name TEXT,
|
||||
claimed_by_node_id TEXT
|
||||
claimed_by_node_id TEXT,
|
||||
wave INTEGER NOT NULL DEFAULT 0
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
// 兼容旧库:若 task_queue 表已存在但缺少 workflow_name / claimed_by_node_id 列,则补列。
|
||||
// SQLite 的 ALTER TABLE ADD COLUMN 是在线操作,旧数据该列默认 NULL。
|
||||
// 兼容旧库:若 task_queue 表已存在但缺少 workflow_name / claimed_by_node_id / wave 列,则补列。
|
||||
// SQLite 的 ALTER TABLE ADD COLUMN 是在线操作,旧数据该列默认 NULL / 0。
|
||||
// PRAGMA table_info 检测列是否存在以实现幂等 migration。
|
||||
let has_col = |conn: &rusqlite::Connection, col: &str| -> rusqlite::Result<bool> {
|
||||
let mut stmt = conn.prepare("PRAGMA table_info(task_queue)")?;
|
||||
@@ -92,6 +93,12 @@ impl SqliteTaskQueue {
|
||||
if !has_col(&conn, "claimed_by_node_id")? {
|
||||
conn.execute("ALTER TABLE task_queue ADD COLUMN claimed_by_node_id TEXT", [])?;
|
||||
}
|
||||
if !has_col(&conn, "wave")? {
|
||||
conn.execute(
|
||||
"ALTER TABLE task_queue ADD COLUMN wave INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
)?;
|
||||
}
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_task_queue_status_created ON task_queue(status, created_at)",
|
||||
[],
|
||||
@@ -100,6 +107,12 @@ impl SqliteTaskQueue {
|
||||
"CREATE INDEX IF NOT EXISTS idx_task_queue_workflow ON task_queue(workflow_name)",
|
||||
[],
|
||||
)?;
|
||||
// 覆盖 pop_task 出队排序:status 固定 'pending' 过滤后,按 wave(全局难度优先)
|
||||
// → created_at(同 wave 内 FIFO)。
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_task_queue_pop ON task_queue(status, wave, created_at)",
|
||||
[],
|
||||
)?;
|
||||
Ok(pool)
|
||||
})
|
||||
.await??;
|
||||
@@ -112,14 +125,15 @@ impl SqliteTaskQueue {
|
||||
let payload = serde_json::to_string(task)?;
|
||||
let task_id_str = task.task_id.to_string();
|
||||
let workflow_name = task.workflow_name.clone();
|
||||
let wave = task.wave;
|
||||
let pool = self.pool.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||
let conn = pool.get().map_err(|e| anyhow::anyhow!("Queue DB pool error: {}", e))?;
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO task_queue (task_id, payload, status, created_at, workflow_name)
|
||||
VALUES (?1, ?2, 'pending', datetime('now'), ?3)",
|
||||
params![task_id_str, payload, workflow_name],
|
||||
"INSERT OR REPLACE INTO task_queue (task_id, payload, status, created_at, workflow_name, wave)
|
||||
VALUES (?1, ?2, 'pending', datetime('now'), ?3, ?4)",
|
||||
params![task_id_str, payload, workflow_name, wave],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -139,8 +153,14 @@ impl SqliteTaskQueue {
|
||||
let tx_res = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate);
|
||||
match tx_res {
|
||||
Ok(tx) => {
|
||||
// 出队排序:wave ASC → created_at ASC(不按 workflow_name 分组)。
|
||||
// - wave ASC:全局低难度 wave 优先,恢复"先易后难积累种子"的设计意图。
|
||||
// 各工作流的低难度点会先于任何工作流的高难度点被消化,避免某个工作流
|
||||
// 在高难度 wave 上饿死另一个工作流的低难度点。
|
||||
// - created_at ASC:同 wave 内严格 FIFO。调度器按工作流批量推入时,
|
||||
// 多个工作流的任务 created_at 天然交错,故同 wave 内不会单工作流独占。
|
||||
let mut stmt = tx.prepare(
|
||||
"SELECT task_id, payload FROM task_queue WHERE status = 'pending' ORDER BY created_at ASC LIMIT 1"
|
||||
"SELECT task_id, payload FROM task_queue WHERE status = 'pending' ORDER BY wave ASC, created_at ASC LIMIT 1"
|
||||
)?;
|
||||
|
||||
let row = stmt.query_row([], |row| {
|
||||
@@ -344,17 +364,18 @@ mod tests {
|
||||
task_id,
|
||||
point_name: "t35000_g5.5_he-1_c-2_n-2_o-2".to_string(),
|
||||
params: GridPointParams {
|
||||
teff: 35000.0,
|
||||
logg: 5.5,
|
||||
loghe: -1.0,
|
||||
logc: -2.0,
|
||||
logn: -2.0,
|
||||
logo: -2.0,
|
||||
teff: 35000.0.into(),
|
||||
logg: 5.5.into(),
|
||||
loghe: (-1.0).into(),
|
||||
logc: (-2.0).into(),
|
||||
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()),
|
||||
wave: 0,
|
||||
};
|
||||
queue.push_task(&task).await.unwrap();
|
||||
|
||||
@@ -387,12 +408,12 @@ mod tests {
|
||||
|
||||
let task_id = Uuid::new_v4();
|
||||
let params = GridPointParams {
|
||||
teff: 35000.0,
|
||||
logg: 5.5,
|
||||
loghe: -1.0,
|
||||
logc: -2.0,
|
||||
logn: -2.0,
|
||||
logo: -2.0,
|
||||
teff: 35000.0.into(),
|
||||
logg: 5.5.into(),
|
||||
loghe: (-1.0).into(),
|
||||
logc: (-2.0).into(),
|
||||
logn: (-2.0).into(),
|
||||
logo: (-2.0).into(),
|
||||
};
|
||||
let task = TaskSpec {
|
||||
task_id,
|
||||
@@ -402,6 +423,7 @@ mod tests {
|
||||
seed_point_name: None,
|
||||
timeout_sec: 60,
|
||||
workflow_name: None,
|
||||
wave: 0,
|
||||
};
|
||||
queue.push_task(&task).await.unwrap();
|
||||
|
||||
@@ -434,4 +456,83 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(claim_after.is_none());
|
||||
}
|
||||
|
||||
/// 辅助:构造一个最小 TaskSpec,方便下面两个排序测试。
|
||||
fn mk_task(wf: &str, wave: i32, point_name: &str) -> TaskSpec {
|
||||
TaskSpec {
|
||||
task_id: Uuid::new_v4(),
|
||||
point_name: point_name.to_string(),
|
||||
params: GridPointParams {
|
||||
teff: 35000.0.into(),
|
||||
logg: 5.5.into(),
|
||||
loghe: (-1.0).into(),
|
||||
logc: (-2.0).into(),
|
||||
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()),
|
||||
wave,
|
||||
}
|
||||
}
|
||||
|
||||
/// 跨工作流 wave 优先级:wf_a 推多个高 wave 任务,wf_b 推一个低 wave 任务,
|
||||
/// 验证 wf_b 的低难度任务不会被 wf_a 的大量高难度任务饿死。
|
||||
///
|
||||
/// 排序为 wave ASC, created_at ASC(不按 workflow_name 分组):
|
||||
/// wf_b 的 wave=0 任务优先于 wf_a 的所有 wave=9 任务出队,
|
||||
/// 即使 wf_a 的任务 created_at 更早、数量更多。
|
||||
/// 这保证了"全局先易后难"——任一工作流的低难度点优先于其他工作流的高难度点。
|
||||
#[tokio::test]
|
||||
async fn test_pop_cross_workflow_wave_priority() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db_path = temp_dir.path().join("pop_fair.db");
|
||||
let queue = SqliteTaskQueue::new(&db_path.to_string_lossy())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// wf_a 推 3 个高难度任务(wave=9,created_at 更早)
|
||||
for i in 0..3 {
|
||||
queue
|
||||
.push_task(&mk_task("wf_a", 9, &format!("a{i}")))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
// wf_b 推 1 个低难度任务(wave=0,created_at 最晚)
|
||||
queue.push_task(&mk_task("wf_b", 0, "b0")).await.unwrap();
|
||||
|
||||
// 第 1 次 pop:wf_b 的 wave=0(全局最低 wave 优先),而非 wf_a 的先入任务
|
||||
let p1 = queue.pop_task("n1").await.unwrap().unwrap();
|
||||
assert_eq!(p1.workflow_name.as_deref(), Some("wf_b"));
|
||||
assert_eq!(p1.point_name, "b0");
|
||||
|
||||
// 之后才轮到 wf_a 的 wave=9 任务(FIFO 顺序)
|
||||
for i in 0..3 {
|
||||
let p = queue.pop_task("n2").await.unwrap().unwrap();
|
||||
assert_eq!(p.workflow_name.as_deref(), Some("wf_a"));
|
||||
assert_eq!(p.point_name, format!("a{i}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// 出队同工作流内按 wave ASC:低难度 wave 优先,即使它 created_at 更晚。
|
||||
#[tokio::test]
|
||||
async fn test_pop_wave_priority_within_workflow() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db_path = temp_dir.path().join("pop_wave.db");
|
||||
let queue = SqliteTaskQueue::new(&db_path.to_string_lossy())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 先推 wave=5(created_at 早),再推 wave=1(created_at 晚)
|
||||
queue.push_task(&mk_task("wf_x", 5, "hard")).await.unwrap();
|
||||
queue.push_task(&mk_task("wf_x", 1, "easy")).await.unwrap();
|
||||
|
||||
// pop 应先拿到 wave=1 的 easy(若纯 FIFO 会先拿到先入的 hard)
|
||||
let p1 = queue.pop_task("n1").await.unwrap().unwrap();
|
||||
assert_eq!(p1.point_name, "easy", "低 wave 优先出队");
|
||||
let p2 = queue.pop_task("n2").await.unwrap().unwrap();
|
||||
assert_eq!(p2.point_name, "hard");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user