Files
DCTS/crates/common/src/embedded.rs
T
fmq cd370d88e7 feat(all): 任务引擎双阶段解耦、僵尸涡旋修复、动态 CPU 配额与前端详情页重构
将 TLUSTY/SYNSPEC 拆为各自独立的 enabled/policy/strategies 阶段,
以策略链自动弹栈取代单级 seed_step 布尔回退;定向修复 2026-08-02
僵尸任务涡旋事故;新增节点并发配额热调;前端详情页从 1412 行巨型
视图拆为薄控制器 + detail 子模块,并补齐工具层与单测。

引擎与调度(task_engine_decoupling_design.md)
- models.rs: 新增 StagePolicy / EngineStageConfig / TaskSpec 阶段字段、
  normalize_compat() 校正旧版在途消息策略链、failed_stage 归因
- scheduler.rs: resolve_dispatchable_chain 派发门控、
  trigger_strategy_fallback 按 failed_stage 精确弹栈;启动期
  force_recompute/skip_converged(默认)/skip_failed 三策略
- db.rs: tasks 表 +7 列持久化阶段配置;终态守卫
  (mark_grid_point_running 仅 pending/queued→running;
  record_task_report 拒绝迟到失败翻黑 converged);策略弹栈快照

僵尸涡旋修复(runbook-20260802-zombie-vortex-fix.md)
- 全链路跨库活性交叉校验:派发/claim/孤儿回收/回退统一查 MQ 队列活性,
  活则放行、死则清僵尸,结构性消除"每点重复派发"
- stop/重启卫生:清队列同步 delete_tasks_by_ids,杜绝遗留 pending 行
- report_task: 幂等吸收 + 409 区分迟到冗余结果,仅 state_changed 时回退
- MQ: NULL workflow_name 回填 __legacy__、requeue 后迟到上报被 403 竞态修复

动态 CPU 配额(dynamic_cpu_slots_design.md)
- admin.rs: POST /admin/nodes/:id/quota(Option<Option<i32>> 区分
  缺字段/显式 null);nodes 表 +admin_max_slots
- worker.rs: effective_max_slots = min(admin, physical),心跳下发原子生效

科学产物保全(tlusty_result_artifacts.md)
- runner.rs: SYNSPEC 启动前快照 fort.12/fort.14 → .bfac/.emflux 防覆盖
- 半失败点(大气收敛+光谱失败)改判 Failed 并写入 note;仅 SYNSPEC
  场景不再恒判失败;撤销归档 LRU 200 上限改为永久保留
- executor.rs: 透传 synspec_params 数值参数(此前固定 None)

前端(dashboard/)
- workflowDetail.js 1412→328 行,拆出 views/detail/{ctx,overview,
  pointsTable,parSets,pointPanel}.js,AbortController 治理监听/请求生命周期
- 删除 wfActions.js,新增 wfEnginePanel.js(双阶段三维配置编辑面板)
- 新增 utils/{errors,format,icons,polling,yamlStage}.js 纯函数模块
- 路由级动态 import 代码分割;节点配额三点菜单 + Modal 管理
- 首次引入 node:test 单测(format/polling/yamlStage/psCache,644 行)
- 系统性补齐 a11y:skip-link、ARIA、Tab 键盘漫游、toast 关闭、退出动画

文档与工具
- 新增 6 篇设计/调研:引擎解耦、动态配额、涡旋 runbook、
  光谱正确性分析、收敛判断、产物归档
- PIPELINE/design/api/database 等协同重写为分布式 C/S 架构口径
- scripts/fetch_results.sh 跨节点产物备份;import_results 按 cno 升序导入
- workflows/sdB_cno.yaml: 新增 tlusty/synspec_stage 配置块,修正 wstart 笔误
2026-08-04 23:40:52 +08:00

187 lines
6.3 KiB
Rust

use anyhow::{Context, Result};
use reqwest::Client;
use sha2::{Digest, Sha256};
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use tracing::info;
#[cfg(feature = "embed-binaries")]
pub static TLUSTY_BIN: &[u8] = include_bytes!("../../../assets/tlusty_static");
#[cfg(not(feature = "embed-binaries"))]
pub static TLUSTY_BIN: &[u8] = &[];
#[cfg(feature = "embed-binaries")]
pub static SYNSPEC_BIN: &[u8] = include_bytes!("../../../assets/synspec_static");
#[cfg(not(feature = "embed-binaries"))]
pub static SYNSPEC_BIN: &[u8] = &[];
#[derive(Debug, Clone)]
pub struct RuntimePaths {
pub tlusty_exe: PathBuf,
pub synspec_exe: PathBuf,
pub data_dir: PathBuf,
pub linelist: PathBuf,
}
fn calc_hash(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
hex::encode(hasher.finalize())
}
/// Ensures Fortran runtime binaries are unpacked and common partition function data files are fetched
pub async fn ensure_runtime(
runtime_dir: &Path,
server_url: &str,
client: &Client,
) -> Result<RuntimePaths> {
fs::create_dir_all(runtime_dir)
.with_context(|| format!("Failed to create runtime dir: {}", runtime_dir.display()))?;
let tlusty_exe = runtime_dir.join("tlusty_static");
let synspec_exe = runtime_dir.join("synspec_static");
let data_dir = runtime_dir.join("data");
let linelist = runtime_dir.join("gfVIS99.dat");
fs::create_dir_all(&data_dir)?;
// 1. Unpack tlusty_static & synspec_static binaries if embedded
if !TLUSTY_BIN.is_empty() {
write_if_changed(&tlusty_exe, TLUSTY_BIN, true)?;
}
if !SYNSPEC_BIN.is_empty() {
write_if_changed(&synspec_exe, SYNSPEC_BIN, true)?;
}
// 2. Fetch baseline equation of state partition function tables if missing locally
let common_files = &[
"irwin_bc.dat",
"irwin_orig.dat",
"tsuji.molec_bc2",
"tsuji.molec_orig",
];
ensure_specific_data_files(&data_dir, server_url, client, common_files).await?;
// 3. Check gfVIS99.dat
if !linelist.exists() {
let url = format!("{}/api/data/linelist", server_url);
info!("本地缺失主谱线库 gfVIS99.dat,开始从服务端下载: {}...", url);
let resp = client.get(&url).send().await?;
if resp.status().is_success() {
let bytes = resp.bytes().await?;
fs::write(&linelist, &bytes)?;
info!("成功下载并保存主谱线库 gfVIS99.dat");
} else {
anyhow::bail!(
"从服务端下载主谱线库 gfVIS99.dat 失败,HTTP 状态码: {}",
resp.status()
);
}
}
let abs_runtime_dir =
fs::canonicalize(runtime_dir).unwrap_or_else(|_| runtime_dir.to_path_buf());
let tlusty_exe = abs_runtime_dir.join("tlusty_static");
let synspec_exe = abs_runtime_dir.join("synspec_static");
let data_dir = abs_runtime_dir.join("data");
let linelist = abs_runtime_dir.join("gfVIS99.dat");
Ok(RuntimePaths {
tlusty_exe,
synspec_exe,
data_dir,
linelist,
})
}
static DATA_DOWNLOAD_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
/// Checks local `./runtime/data/` for specific required files. If missing, downloads ONLY those specific files from Server!
pub async fn ensure_specific_data_files(
data_dir: &Path,
server_url: &str,
client: &Client,
required_files: &[&str],
) -> Result<()> {
let _guard = DATA_DOWNLOAD_MUTEX.lock().await;
tokio::fs::create_dir_all(data_dir).await?;
for &filename in required_files {
let local_file = data_dir.join(filename);
if !local_file.exists() {
let file_url = format!("{}/api/data/file/{}", server_url, filename);
info!(
"本地缺失数据文件 {},开始从服务端拉取: {}",
filename, file_url
);
let resp = client.get(&file_url).send().await?;
if resp.status().is_success() {
let bytes = resp.bytes().await?;
let tmp_file = data_dir.join(format!(
"{}.{}.tmp",
filename,
uuid::Uuid::new_v4().simple()
));
tokio::fs::write(&tmp_file, &bytes).await?;
tokio::fs::rename(&tmp_file, &local_file).await?;
info!("成功保存数据文件: {}", filename);
} else {
anyhow::bail!(
"服务端返回 HTTP {} 错误,数据文件: {}",
resp.status(),
filename
);
}
}
}
Ok(())
}
fn write_if_changed(target_path: &Path, content: &[u8], executable: bool) -> Result<()> {
let should_write = if target_path.exists() {
match fs::read(target_path) {
Ok(existing) => calc_hash(&existing) != calc_hash(content),
Err(_) => true,
}
} else {
true
};
if should_write {
// 原子写:先写 .tmp 再 rename,避免半写后崩溃留下截断/损坏的可执行二进制。
// 历史上直接 File::create 覆盖目标,若 write_all 中途进程被杀/磁盘满,会留下
// 截断的 tlusty_static,且权限可能已设 0o755(可执行但损坏),node 尝试运行时
// 产生难以定位的 Fortran 崩溃。tmp + rename 保证目标要么是完整旧版、要么是完整新版。
let tmp_path = target_path.with_extension("tmp.write");
{
let mut file = File::create(&tmp_path)
.with_context(|| format!("创建临时文件失败: {}", tmp_path.display()))?;
file.write_all(content)?;
file.flush()?;
// drop 前 sync_all 确保数据落盘,降低断电丢数据的概率。
let _ = file.sync_all();
}
#[cfg(unix)]
if executable {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&tmp_path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&tmp_path, perms)?;
}
fs::rename(&tmp_path, target_path).with_context(|| {
format!(
"原子重命名 {} -> {} 失败",
tmp_path.display(),
target_path.display()
)
})?;
}
Ok(())
}