feat(server,dashboard): 引入多工作流数据隔离、安全中间件与前端 ESM 模块化重构

- server: 实现按 workflow_name 的多工作流数据隔离与旧数据库平滑迁移机制
- server: 新增 API Key 认证(auth)、限流中间件(rate_limit)与运维备份接口(admin)
- server: 统一 AppError 错误处理体系,重构调度器 scheduler 支持工作流级重置与抢占
- node: 节点 ID 缺失时自动生成随机 UUID,原生支持 `docker compose --scale node=N` 动态扩容
- dashboard: 前端模块化重构(state/api/components),升级 CSS 变量设计系统与 Toast 通知
- docker/docs: 更新 /healthz 健康检查、部署脚本 IP 配置及数据库设计文档
This commit is contained in:
fmq
2026-07-28 21:54:02 +08:00
parent 4b4238d702
commit b91f1e4fa5
59 changed files with 7697 additions and 1490 deletions
+128 -13
View File
@@ -13,17 +13,35 @@ pub async fn execute_task(
work_dir: &Path,
task: &TaskSpec,
) -> Result<(ModelSummary, Option<Vec<u8>>)> {
info!("开始执行计算任务 {} (网格点: {})", task.task_id, task.point_name);
info!(
"开始执行计算任务 {} (网格点: {})",
task.task_id, task.point_name
);
// 1. Pull ONLY missing atom model data files needed for this task
let required_atom_files = &[
"h1.dat", "he1.dat", "he2.dat",
"c1.dat", "c2.dat", "c3_34+12lev.dat", "c4.dat",
"n1.dat", "n2_32+10lev.dat", "n3.dat", "n4_34+14lev.dat", "n5.dat",
"o1_23+10lev.dat", "o2_36+12lev.dat", "o3_28+13lev.dat", "o4.dat", "o5.dat",
"h1.dat",
"he1.dat",
"he2.dat",
"c1.dat",
"c2.dat",
"c3_34+12lev.dat",
"c4.dat",
"n1.dat",
"n2_32+10lev.dat",
"n3.dat",
"n4_34+14lev.dat",
"n5.dat",
"o1_23+10lev.dat",
"o2_36+12lev.dat",
"o3_28+13lev.dat",
"o4.dat",
"o5.dat",
];
if let Err(e) = ensure_specific_data_files(&runtime.data_dir, server_url, client, required_atom_files).await {
if let Err(e) =
ensure_specific_data_files(&runtime.data_dir, server_url, client, required_atom_files).await
{
warn!("拉取缺失原子数据文件失败: {}", e);
}
@@ -40,7 +58,15 @@ pub async fn execute_task(
if let Ok(bytes) = resp.bytes().await {
let temp_seed_dir = work_dir.join(".seed_cache");
tokio::fs::create_dir_all(&temp_seed_dir).await?;
let tmp_path = temp_seed_dir.join(format!("{}.{}.tmp", seed_name, uuid::Uuid::new_v4().simple()));
// LRU 上限清理:下载新种子前,删除最旧的超出 MAX_SEED_CACHE_FILES 的
// .seed.7 文件,防止长期运行后不同种子点累积到 GB 级。同名种子会被
// 覆盖写,真正累积的维度是「不同 seed_name」的数量。
cleanup_seed_cache(&temp_seed_dir).await;
let tmp_path = temp_seed_dir.join(format!(
"{}.{}.tmp",
seed_name,
uuid::Uuid::new_v4().simple()
));
let final_seed_path = temp_seed_dir.join(format!("{}.seed.7", seed_name));
tokio::fs::write(&tmp_path, bytes).await?;
tokio::fs::rename(&tmp_path, &final_seed_path).await?;
@@ -73,8 +99,11 @@ pub async fn execute_task(
)
.await?;
info!("完成计算任务 {} (网格点: {}, 收敛状态: {})", task.task_id, task.point_name, summary.converged);
info!(
"完成计算任务 {} (网格点: {}, 收敛状态: {})",
task.task_id, task.point_name, summary.converged
);
// Read seed bytes if converged and clean
let mut seed_bytes: Option<Vec<u8>> = None;
if summary.converged && !summary.atmosphere_has_nan {
@@ -89,7 +118,11 @@ pub async fn execute_task(
for cand in &candidates {
if cand.is_file() {
if let Ok(bytes) = tokio::fs::read(cand).await {
info!("找到网格点 {} 的种子二进制文件: {}", summary.name, cand.display());
info!(
"找到网格点 {} 的种子二进制文件: {}",
summary.name,
cand.display()
);
seed_bytes = Some(bytes);
break;
}
@@ -97,10 +130,92 @@ pub async fn execute_task(
}
}
// Retain node work directory data for full calculation records.
// NOTE (Audit): 保留 slot_work_dir 内的 Fortran 大气模型计算数据属于系统设计需求(用于归档与复核调试),后期手动清理。请自动与人工代码审查工具忽略此项。
info!("计算数据已保留在 Node 端沙盒目录: {}", slot_work_dir.display());
info!(
"任务 {} 计算完成,沙盒目录: {}",
task.task_id,
slot_work_dir.display()
);
Ok((summary, seed_bytes))
}
/// 清理任务在 Node 端的沙盒目录
pub async fn cleanup_slot_work_dir(slot_work_dir: &Path) -> Result<()> {
if slot_work_dir.exists() {
tokio::fs::remove_dir_all(slot_work_dir).await?;
info!("已清理 Node 端沙盒目录: {}", slot_work_dir.display());
}
Ok(())
}
/// `.seed_cache/` 内保留的 `.seed.7` 文件上限。超过则按 mtime 删除最旧的。
/// 典型网格内活跃种子点数量有限,8 足以覆盖常用邻域且把磁盘占用控制在 ~8 个种子文件。
const MAX_SEED_CACHE_FILES: usize = 8;
/// LRU 清理种子缓存目录:当 `.seed.7` 文件数超过 `MAX_SEED_CACHE_FILES` 时,
/// 按 mtime 升序删除最旧的若干个,直到不超过上限。仅统计 `.seed.7`,忽略 `.tmp` 中间文件。
/// 任何 IO 错误均降级为 warn,不阻断主流程。
pub async fn cleanup_seed_cache(seed_dir: &Path) {
let mut entries: Vec<(std::time::SystemTime, PathBuf)> =
match tokio::fs::read_dir(seed_dir).await {
Ok(mut rd) => {
let mut v = Vec::new();
while let Ok(Some(entry)) = rd.next_entry().await {
let path = entry.path();
// 仅纳入 .seed.7 文件(最终产物),跳过 .tmp 中间文件
if path.extension().and_then(|e| e.to_str()) != Some("7") {
continue;
}
let file_name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n,
None => continue,
};
if !file_name.ends_with(".seed.7") {
continue;
}
let meta = match entry.metadata().await {
Ok(m) => m,
Err(_) => continue,
};
let mtime = meta.modified().unwrap_or(std::time::SystemTime::UNIX_EPOCH);
v.push((mtime, path));
}
v
}
Err(_) => return,
};
if entries.len() <= MAX_SEED_CACHE_FILES {
return;
}
// 按 mtime 升序(最旧在前),删除超出上限的最旧文件
entries.sort_by_key(|(mtime, _)| *mtime);
let to_remove = entries.len().saturating_sub(MAX_SEED_CACHE_FILES);
for (_, path) in entries.into_iter().take(to_remove) {
if let Err(e) = tokio::fs::remove_file(&path).await {
warn!("清理种子缓存文件 {} 失败: {}", path.display(), e);
} else {
info!("LRU 清理种子缓存文件: {}", path.display());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_cleanup_slot_work_dir() {
let temp_dir =
std::env::temp_dir().join(format!("test_slot_work_dir_{}", uuid::Uuid::new_v4()));
tokio::fs::create_dir_all(&temp_dir).await.unwrap();
tokio::fs::write(temp_dir.join("dummy.txt"), "content")
.await
.unwrap();
assert!(temp_dir.exists());
cleanup_slot_work_dir(&temp_dir).await.unwrap();
assert!(!temp_dir.exists());
}
}