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:
+105
-21
@@ -10,7 +10,7 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use clap::Parser;
|
||||
use common::config::ServerConfig;
|
||||
use common::config::{GridConfig, ServerConfig};
|
||||
use common::logging::init_logging;
|
||||
use mq::sqlite_queue::SqliteTaskQueue;
|
||||
use std::net::SocketAddr;
|
||||
@@ -57,22 +57,30 @@ async fn main() -> Result<()> {
|
||||
let queue = Arc::new(SqliteTaskQueue::new(&server_cfg.queue_db_path).await?);
|
||||
let scheduler = Arc::new(GridScheduler::new(db.clone(), queue.clone()));
|
||||
|
||||
// Auto-register sdB_cno.yaml if exists and not yet in DB
|
||||
// Auto-register sdB_cno.yaml if exists and not yet in DB.
|
||||
// 仅在 DB 中尚无该工作流时注册(INSERT),绝不覆盖已存在的配置——
|
||||
// 旧实现用 upsert 每次启动都用文件内容覆盖 config_yaml/description/status,
|
||||
// 导致管理员通过 API 编辑过的配置在重启后被静默回退。
|
||||
let default_wf_path = Path::new(&server_cfg.grid_config);
|
||||
if default_wf_path.is_file() {
|
||||
if let Ok(yaml_content) = std::fs::read_to_string(default_wf_path) {
|
||||
if let Err(e) = db
|
||||
.upsert_workflow(
|
||||
"sdB_cno",
|
||||
Some("sdB CNO 6D Stellar Atmosphere Grid"),
|
||||
&yaml_content,
|
||||
"idle",
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("预注册默认工作流失败: {}", e);
|
||||
let already_exists = matches!(db.get_workflow("sdB_cno").await, Ok(Some(_)));
|
||||
if !already_exists {
|
||||
if let Err(e) = db
|
||||
.upsert_workflow(
|
||||
"sdB_cno",
|
||||
Some("sdB CNO 6D Stellar Atmosphere Grid"),
|
||||
&yaml_content,
|
||||
"idle",
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("预注册默认工作流失败: {}", e);
|
||||
} else {
|
||||
info!("已在数据库中成功预注册默认工作流 'sdB_cno'");
|
||||
}
|
||||
} else {
|
||||
info!("已在数据库中成功预注册默认工作流 'sdB_cno'");
|
||||
info!("默认工作流 'sdB_cno' 已存在于数据库,保留现有配置(不覆盖 API 编辑)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,6 +92,51 @@ async fn main() -> Result<()> {
|
||||
tracing::warn!("⚠️ 检测到系统当前正在使用弱口令凭据或默认 Token!建议生产环境在 .env 中配置使用 openssl rand -hex 32 生成的高强度 Token!");
|
||||
}
|
||||
|
||||
// 启动恢复:把卡在 `initializing` 态的工作流重新初始化。
|
||||
// 背景:`start_workflow` 把状态切到 `initializing` 后在后台 spawn `initialize_grid`,
|
||||
// 若进程在初始化中途崩溃/重启,工作流会永久卡在 `initializing`——`get_running_workflow_names`
|
||||
// 仍把它视为可调度,但网格展开未完成,导致半初始化网格被调度。
|
||||
// `initialize_grid` 是幂等的(upsert ON CONFLICT DO NOTHING),重跑可补齐缺失点并把
|
||||
// 状态推进到 `running`。失败则回退为 `idle` 等待人工重启(与 start_workflow 口径一致)。
|
||||
match db.get_initializing_workflows().await {
|
||||
Ok(stuck) if !stuck.is_empty() => {
|
||||
info!(
|
||||
"检测到 {} 个卡在 initializing 态的工作流(上次启动未完成即重启),开始重新初始化...",
|
||||
stuck.len()
|
||||
);
|
||||
for (wf_name, wf_yaml) in &stuck {
|
||||
match GridConfig::from_yaml_str(wf_yaml) {
|
||||
Ok(cfg) => {
|
||||
match scheduler.initialize_grid(&cfg, wf_name).await {
|
||||
Ok(_) => {
|
||||
let _ = db.update_workflow_status(wf_name, "running").await;
|
||||
info!("启动恢复:工作流 {} 已完成重新初始化并切回 running", wf_name);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"启动恢复:工作流 {} 重新初始化失败,回退为 idle: {}",
|
||||
wf_name,
|
||||
e
|
||||
);
|
||||
let _ = db.update_workflow_status(wf_name, "idle").await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"启动恢复:工作流 {} 的 YAML 配置解析失败,回退为 idle: {}",
|
||||
wf_name,
|
||||
e
|
||||
);
|
||||
let _ = db.update_workflow_status(wf_name, "idle").await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::warn!("启动恢复:查询 initializing 工作流失败: {}", e),
|
||||
}
|
||||
|
||||
let rate_limiter = api::rate_limit::RateLimiter::new(5, std::time::Duration::from_secs(300));
|
||||
|
||||
let state = AppState {
|
||||
@@ -165,6 +218,21 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// 孤儿 running 点回收(#6 修复兜底):queue 行已消失(误删/崩溃丢队列)
|
||||
// 但 grid_points 仍卡在 running 的点,requeue_stale_tasks 找不到它们,
|
||||
// 在此按 tasks 表的 stale pending 记录兜底重置为 pending,让调度器重新派发。
|
||||
match bg_db_clone.reset_orphaned_running_points(stale_sec).await {
|
||||
Ok(reset) => {
|
||||
if reset > 0 {
|
||||
info!("已回收 {} 个孤儿 running 网格点(领用凭证丢失,重置为 pending)", reset);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("回收孤儿 running 网格点失败: {}", e);
|
||||
has_error = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = bg_scheduler_clone.schedule_pending_tasks().await {
|
||||
tracing::warn!("后台定时性任务调度检测失败: {}", e);
|
||||
has_error = true;
|
||||
@@ -231,7 +299,6 @@ async fn main() -> Result<()> {
|
||||
// 大体积上传端点单独拎出,套用更宽松的 body limit(256MB,覆盖收敛种子 .7 文件量级)
|
||||
// 并限制并发数:每个 report 请求最多 256MB 驻留内存,无并发上限时 N 个请求可耗尽内存。
|
||||
// 限流后超出并发数的请求排队等待(而非直接拒绝),保证正常业务不被误伤。
|
||||
// 其余 API 用 10MB 默认上限,防止大文件内存耗尽 DoS。
|
||||
const REPORT_BODY_LIMIT: usize = 256 * 1024 * 1024;
|
||||
const DEFAULT_BODY_LIMIT: usize = 10 * 1024 * 1024;
|
||||
const REPORT_MAX_CONCURRENCY: usize = 4;
|
||||
@@ -254,10 +321,16 @@ async fn main() -> Result<()> {
|
||||
api::rate_limit::rate_limit_middleware,
|
||||
);
|
||||
|
||||
let api_router = Router::new()
|
||||
// 其余 API(小体积)套用 10MB 默认上限,防止大文件内存耗尽 DoS。
|
||||
// 注意:body limit layer 从外到内执行、先接触原始 body 流的层先生效。
|
||||
// 必须把 10MB 限制只套在"小体积子 router"上,再与 report_router 合并,
|
||||
// 合并后的外层不能再套任何全局 limit —— 否则外层 10MB 会截断 report 的 256MB body 流,
|
||||
// 导致收敛种子 .7(常 >10MB)上报被 413 拒绝、结果反复重算。
|
||||
let small_body_router = Router::new()
|
||||
// Auth API
|
||||
.route("/login", post(api::auth::login))
|
||||
.route("/auth/check", get(api::auth::check_auth))
|
||||
.route("/auth/logout", post(api::auth::logout))
|
||||
// Core Node & Task API
|
||||
.route(
|
||||
"/node/register",
|
||||
@@ -329,12 +402,24 @@ async fn main() -> Result<()> {
|
||||
"/admin/nodes/:node_id/enable",
|
||||
post(api::admin::enable_node),
|
||||
)
|
||||
// 合并大体积上报路由(继承各自的 body limit)
|
||||
.merge(report_router)
|
||||
.layer(DefaultBodyLimit::max(DEFAULT_BODY_LIMIT));
|
||||
|
||||
// 鉴权启用条件:未应急关闭,且配置了 admin 凭据。
|
||||
// 合并两个子 router:各自携带自己的 body limit,互不覆盖。
|
||||
let api_router = small_body_router.merge(report_router);
|
||||
|
||||
// 鉴权策略:fail-closed。
|
||||
// - 配置了 DCTS_ADMIN_TOKEN → 启用完整鉴权。
|
||||
// - 显式 DCTS_AUTH_DISABLE=1 → 无鉴权(仅本地调试,需运维主动声明承担风险)。
|
||||
// - 既未配置 token、又未显式 disable → **拒绝启动**。
|
||||
// 避免 .env 缺失/变量名拼错/容器未注入环境变量时服务静默退化为完全无鉴权裸奔。
|
||||
let auth_enabled = !state.auth_disabled && state.admin_token.is_some();
|
||||
if !auth_enabled && !state.auth_disabled {
|
||||
anyhow::bail!(
|
||||
"拒绝启动:未配置 DCTS_ADMIN_TOKEN 且未显式设置 DCTS_AUTH_DISABLE=1。\
|
||||
生产部署必须在 .env 中配置 DCTS_ADMIN_TOKEN;若确为本地调试,\
|
||||
请显式设置 DCTS_AUTH_DISABLE=1 以承担无鉴权风险。"
|
||||
);
|
||||
}
|
||||
|
||||
let api_router = if auth_enabled {
|
||||
info!("已启用 API 身份鉴权保护(Admin 端点需 admin token 验证;Node 节点免 Token 提交申请,经 Dashboard 管理员审批授权下发)");
|
||||
@@ -346,9 +431,8 @@ async fn main() -> Result<()> {
|
||||
let auth_layer = axum::middleware::from_fn_with_state(state.clone(), api::auth_middleware);
|
||||
api_router.layer(auth_layer).layer(rate_limit_layer)
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"⚠️ 警告:未配置 DCTS_ADMIN_TOKEN(且未启用 DCTS_AUTH_DISABLE),\
|
||||
服务端运行在【无鉴权模式】!公网部署务必配置凭据。"
|
||||
info!(
|
||||
"DCTS_AUTH_DISABLE=1 已生效:服务端运行在无鉴权模式(仅限本地调试,切勿用于生产)。"
|
||||
);
|
||||
api_router
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user