feat(all): 科学计算正确性修复、调度竞态消除、节点优雅退出、安全加固与收敛分析重构

科学计算正确性:
- 修复 Fortran 无-E 科学记数法(指数≥100 时 E 被挤掉,如 -1.35+118)导致
  发散行被静默跳过、误判收敛的 bug;扩展大气无效检测覆盖 Inf 与 *** 溢出标记
- 种子匹配改为 CNO 有向距离(富金属方向重罚 4×、贫金属方向轻罚 1×),
  基于 1191 个真实种子配对回测标定,回测净改善 314 个点
- GridAxisValue 反序列化拒绝非法文本(不再静默 NaN);chmax≤0 显式报错
- ions 行宽列宽对齐真实 fort.5 格式

调度与队列竞态:
- 原子选点(IMMEDIATE 事务 SELECT+UPDATE)消除并发调度重复派发 (#5)
- 调度互斥锁 + 冷启动优先策略(SeedStep 仅作失败后救援,不再正常路径热启动)
- 毒消息 dead_letter 标记防出队死循环;clear_queue 保留 claimed 行 (#6)
- 孤儿 running 点回收兜底;stale_sec 默认 7800→21600s(3 倍超时缓冲)

节点生命周期:
- SIGTERM+SIGINT 双信号监听(修复 Docker stop 发 SIGTERM 不触发优雅退出)
- SlotGuard RAII 防活动 slot 泄漏;子进程超时增加二级 30s wait 防 Fortran hang
- SeedStep 种子下载 fail-fast + 沙盒私有副本解耦 LRU 清理竞争
- reqwest Client 增加连接/请求超时;启动清理残留 task_* 沙盒

安全加固:
- 节点注册 registration_secret 二次凭据 (H8),恒定时间比对防时序旁路
- token 缓存 generation 机制消除 reissue 后旧 token TOCTOU 复活窗口
- 新增 /api/auth/logout 服务端 session 即时撤销;fail-closed 鉴权启动策略
- 前端 token 迁移 sessionStorage;YAML 高亮改 DOM API 消除 XSS 注入面
- 备份文件权限收紧 0600;点表动态值全面 escapeHtml

前端 Dashboard:
- 收敛性分析从热力图重构为 Parallel Sets 平行集合图(6 维+状态轴,手写 SVG 零依赖)
- 进度曲线横轴改为真实时间(服务端 now 锚定,停滞期诚实留白);轮询指数退避
- 移除 imported 收敛途径分类,导入点按实际途径 cold_run/seed_step 归类
- 初始化时 /api/auth/check 校验 token;401 toast 提示替代静默 reload

服务端恢复与工具链:
- 启动恢复 initializing 态工作流;默认工作流 INSERT-only 不覆盖 API 编辑
- body limit 分层(10MB 不再截断 256MB report);multipart 显式错误处理
- 嵌入二进制原子写(tmp+rename)防半写损坏
- import_results 判定收敛途径透传 success_method;conv.json 格式对齐本项目
- push_import_results.sh 退出码修复 + .bat UTF-8 BOM + scp 上传
- Docker USE_MIRRORS 默认关闭;移除无用 assets 挂载;删除 hosts.ini 入库
This commit is contained in:
fmq
2026-08-01 17:01:40 +08:00
parent 1bfa240cb0
commit c8fd24b120
45 changed files with 2821 additions and 831 deletions
+35 -28
View File
@@ -11,11 +11,18 @@ use crate::db::Database;
pub struct GridScheduler {
db: Database,
queue: Arc<SqliteTaskQueue>,
/// 调度互斥锁:防止 start_workflow 的即时调度与后台 30s 循环并发进入
/// schedule_pending_tasks,消除 TOCTOU 竞态导致的重复派发(#5 修复)。
schedule_lock: tokio::sync::Mutex<()>,
}
impl GridScheduler {
pub fn new(db: Database, queue: Arc<SqliteTaskQueue>) -> Self {
Self { db, queue }
Self {
db,
queue,
schedule_lock: tokio::sync::Mutex::new(()),
}
}
/// Expands grid points from config and registers them into the database.
@@ -140,12 +147,17 @@ impl GridScheduler {
true
}
/// Enqueues pending grid points into MQ with active seed detection and batching.
/// Enqueues pending grid points into MQ with batching(冷启动优先).
///
/// 多工作流分区(#3 修复):对**每个** running/initializing 工作流分别派发任务,
/// 替代原来「全局只一个 running workflow」的 LIMIT 1 假设。各工作流独立 batch
/// 独立 seed 匹配(seeds 仍是全局共享的物理资源池)。
/// 替代原来「全局只一个 running workflow」的 LIMIT 1 假设。各工作流独立 batch
/// 正常路径一律派发 ColdRun 冷启动,种子匹配只发生在失败后的
/// trigger_seed_step_fallbackseeds 仍是全局共享的物理资源池)。
pub async fn schedule_pending_tasks(&self) -> Result<usize> {
// 互斥锁:start_workflow 的即时调度与后台 30s 循环可能并发调用本方法,
// 两者各自 SELECT 同一批 pending 点会产生重复任务(#5 修复)。
let _guard = self.schedule_lock.lock().await;
let workflows = self.db.get_running_workflow_names().await?;
if workflows.is_empty() {
return Ok(0);
@@ -184,46 +196,35 @@ impl GridScheduler {
) -> Result<usize> {
let timeout_sec = self.get_workflow_timeout_sec(workflow_name).await;
// SQL 层直接附加 LIMIT = batch_limit + workflow_name 筛选,完全免除数万点位无谓内存反序列化
// 原子选点(#5 修复):IMMEDIATE 事务内完成 SELECT + UPDATE status='queued'
// 替代原来 get_pending_grid_points_limitSELECT+ update_grid_statusUPDATE
// 的分离操作,杜绝两个并发调度调用 SELECT 到同一批 pending 点的 TOCTOU 竞态。
let pending = self
.db
.get_pending_grid_points_limit(batch_limit, workflow_name)
.claim_pending_grid_points(batch_limit, workflow_name)
.await?;
let mut dispatched = 0;
for (name, params, wave) in pending {
// Check if any seed is available in DB for active SeedStep schedulingseeds 全局共享)
let (task_type, seed_name) = match self.db.find_best_seed_from_db(&params).await {
Ok(Some(seed_match)) => {
info!(
"工作流 {} 网格点 {} 匹配到数据库近邻种子 {} (距离: {:.2}),安排 SeedStep 热启动调度",
workflow_name, name, seed_match.name, seed_match.distance
);
(TaskType::SeedStep, Some(seed_match.name))
}
_ => (TaskType::ColdRun, None),
};
// 冷启动优先:正常调度路径一律走 ColdRun 自包含冷启动(lte 阶段 ltgray="T"
// 生成 grey start,不依赖任何种子文件)。SeedStep 仅作为冷启动失败后的救援
// 任务出现,由 trigger_seed_step_fallback(见下)派发——「冷启动优先、失败再
// 种子步进」的单向语义,避免旧版「正常路径热启动优先 + 节点端缺种子回退冷启
// 动链」的双向纠缠。
let task_spec = TaskSpec {
task_id: Uuid::new_v4(),
point_name: name.clone(),
params,
task_type,
seed_point_name: seed_name,
task_type: TaskType::ColdRun,
seed_point_name: None,
timeout_sec,
workflow_name: Some(workflow_name.to_string()),
wave,
};
self.db.insert_task(&task_spec).await?;
// 采用先标记 DB 状态Queued 后发 MQ 的时序,防止推入 MQ 后数据库修改异常导向下一轮误重投
self.db
.update_grid_status(
&name,
common::models::GridPointStatus::Queued,
workflow_name,
)
.await?;
// 点已在 claim_pending_grid_points 的 IMMEDIATE 事务中原子标记queued
// 无需再单独 update_grid_status(Queued)。push 失败时回滚为 pending 即可。
match self.queue.push_task(&task_spec).await {
Ok(_) => {
dispatched += 1;
@@ -250,6 +251,9 @@ impl GridScheduler {
);
}
let _ = self.queue.remove_task(&task_spec.task_id.to_string()).await;
// 同步清理先于 push 插入的 tasks 历史行,避免遗留 pending 历史记录
// 污染每点尝试计数统计(attempt_count 依赖 tasks 表聚合)。
let _ = self.db.delete_task(&task_spec.task_id).await;
}
}
}
@@ -333,6 +337,9 @@ impl GridScheduler {
)
.await;
let _ = self.queue.remove_task(&task_spec.task_id.to_string()).await;
// 清理先于 push 插入的 tasks 历史行,与上方 schedule_pending_tasks_for_workflow
// 回滚口径一致,避免遗留 pending 历史污染尝试计数。
let _ = self.db.delete_task(&task_spec.task_id).await;
return Err(e);
}
info!(