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:
+112
-28
@@ -135,19 +135,56 @@ impl GridConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
pub bind_addr: String,
|
||||
pub db_path: String,
|
||||
pub queue_db_path: String,
|
||||
pub results_dir: String,
|
||||
/// 数据库备份目录(每日自动备份落盘位置)。默认 "data/backups",可经 DCTS_BACKUP_DIR 覆盖。
|
||||
pub backup_dir: String,
|
||||
pub grid_config: String,
|
||||
pub stale_sec: u64,
|
||||
#[serde(default = "default_node_stale_sec")]
|
||||
pub node_stale_sec: u64,
|
||||
pub mq_type: String, // "sqlite" or "rabbitmq"
|
||||
pub rabbitmq_url: Option<String>,
|
||||
/// Admin 凭据(Dashboard 登录用)。优先 DCTS_ADMIN_TOKEN,回退旧变量 DCTS_AUTH_TOKEN。
|
||||
#[serde(default)]
|
||||
pub admin_token: Option<String>,
|
||||
/// 兼容字段:保留以判断「是否启用鉴权」与旧中间件逻辑。取 admin_token 的值。
|
||||
#[serde(default)]
|
||||
pub auth_token: Option<String>,
|
||||
/// 应急开关:DCTS_AUTH_DISABLE=1 时跳过全部鉴权(仅本地调试,默认关闭)。
|
||||
#[serde(default)]
|
||||
pub auth_disabled: bool,
|
||||
}
|
||||
|
||||
// 手写 Debug:token 类字段脱敏为 ***REDACTED***,防止日志/错误链泄露明文凭据。
|
||||
impl std::fmt::Debug for ServerConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ServerConfig")
|
||||
.field("bind_addr", &self.bind_addr)
|
||||
.field("db_path", &self.db_path)
|
||||
.field("queue_db_path", &self.queue_db_path)
|
||||
.field("results_dir", &self.results_dir)
|
||||
.field("backup_dir", &self.backup_dir)
|
||||
.field("grid_config", &self.grid_config)
|
||||
.field("stale_sec", &self.stale_sec)
|
||||
.field("node_stale_sec", &self.node_stale_sec)
|
||||
.field("mq_type", &self.mq_type)
|
||||
.field("rabbitmq_url", &self.rabbitmq_url)
|
||||
.field(
|
||||
"admin_token",
|
||||
&self.admin_token.as_ref().map(|_| "***REDACTED***"),
|
||||
)
|
||||
.field(
|
||||
"auth_token",
|
||||
&self.auth_token.as_ref().map(|_| "***REDACTED***"),
|
||||
)
|
||||
.field("auth_disabled", &self.auth_disabled)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_node_stale_sec() -> u64 {
|
||||
@@ -160,12 +197,13 @@ impl Default for ServerConfig {
|
||||
.or_else(|_| std::env::var("CNO_PORT"))
|
||||
.or_else(|_| std::env::var("PORT"))
|
||||
.unwrap_or_else(|_| "8090".to_string());
|
||||
let db_path = std::env::var("DCTS_DB_PATH")
|
||||
.unwrap_or_else(|_| "data/dcts.db".to_string());
|
||||
let db_path = std::env::var("DCTS_DB_PATH").unwrap_or_else(|_| "data/dcts.db".to_string());
|
||||
let queue_db_path = std::env::var("DCTS_QUEUE_DB_PATH")
|
||||
.unwrap_or_else(|_| "data/dcts_queue.db".to_string());
|
||||
let results_dir = std::env::var("DCTS_RESULTS_DIR")
|
||||
.unwrap_or_else(|_| "data/results".to_string());
|
||||
let results_dir =
|
||||
std::env::var("DCTS_RESULTS_DIR").unwrap_or_else(|_| "data/results".to_string());
|
||||
let backup_dir =
|
||||
std::env::var("DCTS_BACKUP_DIR").unwrap_or_else(|_| "data/backups".to_string());
|
||||
let grid_config = std::env::var("DCTS_GRID_CONFIG")
|
||||
.unwrap_or_else(|_| "workflows/sdB_cno.yaml".to_string());
|
||||
// 默认设置为 7800 秒,比计算任务默认超时(7200 秒)高 600 秒缓冲,避免两边的超时检测同时触发冲突
|
||||
@@ -177,26 +215,63 @@ impl Default for ServerConfig {
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(60);
|
||||
let mq_type = std::env::var("DCTS_MQ_TYPE")
|
||||
.unwrap_or_else(|_| "sqlite".to_string());
|
||||
let mq_type = std::env::var("DCTS_MQ_TYPE").unwrap_or_else(|_| "sqlite".to_string());
|
||||
let rabbitmq_url = std::env::var("DCTS_RABBITMQ_URL").ok();
|
||||
let auth_token = std::env::var("DCTS_AUTH_TOKEN").ok();
|
||||
|
||||
// ── 鉴权凭据解析 ──
|
||||
let legacy_token = std::env::var("DCTS_AUTH_TOKEN")
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty());
|
||||
let admin_token = std::env::var("DCTS_ADMIN_TOKEN")
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.or_else(|| legacy_token.clone());
|
||||
if legacy_token.is_some()
|
||||
&& (std::env::var("DCTS_ADMIN_TOKEN").is_err()
|
||||
|| std::env::var("DCTS_ENROLLMENT_TOKEN").is_err())
|
||||
{
|
||||
tracing::warn!(
|
||||
"检测到旧的 DCTS_AUTH_TOKEN,已自动用作 admin/enrollment 凭据。\
|
||||
建议迁移到 DCTS_ADMIN_TOKEN(管理)与 DCTS_ENROLLMENT_TOKEN(节点注册)"
|
||||
);
|
||||
}
|
||||
|
||||
let auth_disabled = std::env::var("DCTS_AUTH_DISABLE")
|
||||
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false);
|
||||
if auth_disabled {
|
||||
tracing::warn!(
|
||||
"⚠️ DCTS_AUTH_DISABLE=1 已生效:全部鉴权被跳过,仅供本地调试,切勿用于生产!"
|
||||
);
|
||||
}
|
||||
|
||||
// auth_token 兼容字段:用于 main.rs 判断「是否启用鉴权中间件」。
|
||||
// 启用条件 = 显式配置了 admin 或 enrollment 凭据,且未应急关闭。
|
||||
let auth_token = if auth_disabled {
|
||||
None
|
||||
} else {
|
||||
admin_token.clone()
|
||||
};
|
||||
|
||||
Self {
|
||||
bind_addr: format!("0.0.0.0:{}", port),
|
||||
db_path,
|
||||
queue_db_path,
|
||||
results_dir,
|
||||
backup_dir,
|
||||
grid_config,
|
||||
stale_sec,
|
||||
node_stale_sec,
|
||||
mq_type,
|
||||
rabbitmq_url,
|
||||
admin_token,
|
||||
auth_token,
|
||||
auth_disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct NodeConfig {
|
||||
pub node_id: String,
|
||||
pub server_url: String,
|
||||
@@ -204,7 +279,19 @@ pub struct NodeConfig {
|
||||
pub runtime_dir: String,
|
||||
pub work_dir: String,
|
||||
pub heartbeat_sec: u64,
|
||||
pub auth_token: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for NodeConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("NodeConfig")
|
||||
.field("node_id", &self.node_id)
|
||||
.field("server_url", &self.server_url)
|
||||
.field("max_slots", &self.max_slots)
|
||||
.field("runtime_dir", &self.runtime_dir)
|
||||
.field("work_dir", &self.work_dir)
|
||||
.field("heartbeat_sec", &self.heartbeat_sec)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NodeConfig {
|
||||
@@ -215,22 +302,27 @@ impl Default for NodeConfig {
|
||||
.unwrap_or_else(|_| "http://127.0.0.1:8090".to_string());
|
||||
let node_id = std::env::var("DCTS_NODE_ID")
|
||||
.or_else(|_| std::env::var("NODE_ID"))
|
||||
.and_then(|v| if v.trim().is_empty() { Err(std::env::VarError::NotPresent) } else { Ok(v) })
|
||||
.and_then(|v| {
|
||||
if v.trim().is_empty() {
|
||||
Err(std::env::VarError::NotPresent)
|
||||
} else {
|
||||
Ok(v)
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|_| format!("node-{}", uuid::Uuid::new_v4().simple()));
|
||||
let max_slots = std::env::var("DCTS_MAX_SLOTS")
|
||||
.or_else(|_| std::env::var("MAX_SLOTS"))
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<usize>().ok())
|
||||
.unwrap_or(4);
|
||||
let runtime_dir = std::env::var("DCTS_RUNTIME_DIR")
|
||||
.unwrap_or_else(|_| "data/runtime".to_string());
|
||||
let work_dir = std::env::var("DCTS_WORK_DIR")
|
||||
.unwrap_or_else(|_| "data/work".to_string());
|
||||
let runtime_dir =
|
||||
std::env::var("DCTS_RUNTIME_DIR").unwrap_or_else(|_| "data/runtime".to_string());
|
||||
let work_dir = std::env::var("DCTS_WORK_DIR").unwrap_or_else(|_| "data/work".to_string());
|
||||
let heartbeat_sec = std::env::var("DCTS_HEARTBEAT_SEC")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(15);
|
||||
let auth_token = std::env::var("DCTS_AUTH_TOKEN").ok();
|
||||
|
||||
Self {
|
||||
node_id,
|
||||
server_url,
|
||||
@@ -238,7 +330,6 @@ impl Default for NodeConfig {
|
||||
runtime_dir,
|
||||
work_dir,
|
||||
heartbeat_sec,
|
||||
auth_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,21 +341,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_load_real_grid_configs() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
|
||||
|
||||
let sdb_path = root.join("workflows/sdB_cno.yaml");
|
||||
if sdb_path.exists() {
|
||||
let cfg = GridConfig::load_from_file(&sdb_path).expect("解析 workflows/sdB_cno.yaml 发生失败");
|
||||
let cfg = GridConfig::load_from_file(&sdb_path)
|
||||
.expect("解析 workflows/sdB_cno.yaml 发生失败");
|
||||
assert_eq!(cfg.nworkers, 16);
|
||||
assert_eq!(cfg.niter, Some(100));
|
||||
}
|
||||
|
||||
let dense_path = root.join("config_dense.yaml");
|
||||
if dense_path.exists() {
|
||||
let cfg = GridConfig::load_from_file(&dense_path).expect("解析 config_dense.yaml 发生失败");
|
||||
assert_eq!(cfg.template.as_deref(), Some("templates/cno_atmos.5.tpl"));
|
||||
assert_eq!(cfg.linelist.as_deref(), Some("data/gfVIS99.dat"));
|
||||
assert!(cfg.seed_step_fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -80,15 +80,12 @@ pub fn check_fort9(path: &Path, chmax: f64) -> ConvCheckResult {
|
||||
}
|
||||
|
||||
// Safely find depth with maximum absolute change without unwrap panic on NaN
|
||||
let worst = match cur_rows
|
||||
.iter()
|
||||
.max_by(|a, b| {
|
||||
a.maximum
|
||||
.abs()
|
||||
.partial_cmp(&b.maximum.abs())
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
{
|
||||
let worst = match cur_rows.iter().max_by(|a, b| {
|
||||
a.maximum
|
||||
.abs()
|
||||
.partial_cmp(&b.maximum.abs())
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
}) {
|
||||
Some(row) => row,
|
||||
None => {
|
||||
return ConvCheckResult {
|
||||
@@ -98,7 +95,9 @@ pub fn check_fort9(path: &Path, chmax: f64) -> ConvCheckResult {
|
||||
last_iter,
|
||||
n_depths: 0,
|
||||
chmax,
|
||||
error: Some("No valid iteration rows found when calculating maximum change".to_string()),
|
||||
error: Some(
|
||||
"No valid iteration rows found when calculating maximum change".to_string(),
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -113,15 +112,22 @@ pub fn check_fort9(path: &Path, chmax: f64) -> ConvCheckResult {
|
||||
last_iter,
|
||||
n_depths: cur_rows.len(),
|
||||
chmax,
|
||||
error: if is_valid_num { None } else { Some("Convergence value is NaN or Inf".to_string()) },
|
||||
error: if is_valid_num {
|
||||
None
|
||||
} else {
|
||||
Some("Convergence value is NaN or Inf".to_string())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if an atmosphere file (.7) contains NaN lines (>10% NaN lines = invalid) using exact word boundary
|
||||
///
|
||||
/// 文件缺失时返回 `false`(语义:不存在 NaN 内容)。这与“含 NaN 导致无效”是不同语义;
|
||||
/// 调用方需先自行确认文件存在性,不应将“缺失”与“含 NaN”混为一谈。
|
||||
pub fn atmosphere_has_nan(path: &Path) -> bool {
|
||||
let file = match File::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return true,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let reader = BufReader::new(file);
|
||||
let mut total_lines = 0;
|
||||
@@ -142,7 +148,6 @@ pub fn atmosphere_has_nan(path: &Path) -> bool {
|
||||
(nan_lines as f64) > (total_lines as f64 * 0.1)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -162,5 +167,9 @@ mod tests {
|
||||
let banana_file_path = dir.path().join("banana.7");
|
||||
std::fs::write(&banana_file_path, "banana 2 3\nbanana 5 6\n7 8 9\n").unwrap();
|
||||
assert!(!atmosphere_has_nan(&banana_file_path));
|
||||
|
||||
// Missing file returns false (absence != contains NaN)
|
||||
let missing_path = dir.path().join("missing.7");
|
||||
assert!(!atmosphere_has_nan(&missing_path));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,9 +54,13 @@ pub async fn ensure_runtime(
|
||||
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"];
|
||||
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
|
||||
@@ -69,11 +73,15 @@ pub async fn ensure_runtime(
|
||||
fs::write(&linelist, &bytes)?;
|
||||
info!("成功下载并保存主谱线库 gfVIS99.dat");
|
||||
} else {
|
||||
anyhow::bail!("从服务端下载主谱线库 gfVIS99.dat 失败,HTTP 状态码: {}", resp.status());
|
||||
anyhow::bail!(
|
||||
"从服务端下载主谱线库 gfVIS99.dat 失败,HTTP 状态码: {}",
|
||||
resp.status()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let abs_runtime_dir = fs::canonicalize(runtime_dir).unwrap_or_else(|_| runtime_dir.to_path_buf());
|
||||
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");
|
||||
@@ -103,17 +111,28 @@ pub async fn ensure_specific_data_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);
|
||||
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()));
|
||||
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);
|
||||
anyhow::bail!(
|
||||
"服务端返回 HTTP {} 错误,数据文件: {}",
|
||||
resp.status(),
|
||||
filename
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,16 @@ pub fn generate_fort55_content(cfg: &SynspecConfig) -> String {
|
||||
let line3 = " 0 0 0 0 0";
|
||||
let line4 = " 1 1 0 0 0";
|
||||
let line5 = " 0 0 0";
|
||||
let line6 = format!(" {:.1} {:.1} 10 0 {} {}", cfg.wstart, cfg.wend, cfg.rel_cutoff, cfg.abs_cutoff);
|
||||
let line6 = format!(
|
||||
" {:.1} {:.1} 10 0 {} {}",
|
||||
cfg.wstart, cfg.wend, cfg.rel_cutoff, cfg.abs_cutoff
|
||||
);
|
||||
let line7 = " 0 0";
|
||||
|
||||
format!("{}\n{}\n{}\n{}\n{}\n{}\n{}\n", line1, line2, line3, line4, line5, line6, line7)
|
||||
format!(
|
||||
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n",
|
||||
line1, line2, line3, line4, line5, line6, line7
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+161
-28
@@ -9,40 +9,172 @@ struct IonDef {
|
||||
}
|
||||
|
||||
const IONS_H: &[IonDef] = &[
|
||||
IonDef { iat: 1, iz: 0, nlevs: 9, typion: " H 1", filei: "data/h1.dat" },
|
||||
IonDef { iat: 1, iz: 1, nlevs: 1, typion: " H 2", filei: " " },
|
||||
IonDef {
|
||||
iat: 1,
|
||||
iz: 0,
|
||||
nlevs: 9,
|
||||
typion: " H 1",
|
||||
filei: "data/h1.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 1,
|
||||
iz: 1,
|
||||
nlevs: 1,
|
||||
typion: " H 2",
|
||||
filei: " ",
|
||||
},
|
||||
];
|
||||
|
||||
const IONS_HE: &[IonDef] = &[
|
||||
IonDef { iat: 2, iz: 0, nlevs: 14, typion: "He 1", filei: "data/he1.dat" },
|
||||
IonDef { iat: 2, iz: 1, nlevs: 14, typion: "He 2", filei: "data/he2.dat" },
|
||||
IonDef { iat: 2, iz: 2, nlevs: 1, typion: "He 3", filei: " " },
|
||||
IonDef {
|
||||
iat: 2,
|
||||
iz: 0,
|
||||
nlevs: 14,
|
||||
typion: "He 1",
|
||||
filei: "data/he1.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 2,
|
||||
iz: 1,
|
||||
nlevs: 14,
|
||||
typion: "He 2",
|
||||
filei: "data/he2.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 2,
|
||||
iz: 2,
|
||||
nlevs: 1,
|
||||
typion: "He 3",
|
||||
filei: " ",
|
||||
},
|
||||
];
|
||||
|
||||
const IONS_C: &[IonDef] = &[
|
||||
IonDef { iat: 6, iz: 0, nlevs: 40, typion: " C 1", filei: "data/c1.dat" },
|
||||
IonDef { iat: 6, iz: 1, nlevs: 22, typion: " C 2", filei: "data/c2.dat" },
|
||||
IonDef { iat: 6, iz: 2, nlevs: 46, typion: " C 3", filei: "data/c3_34+12lev.dat" },
|
||||
IonDef { iat: 6, iz: 3, nlevs: 25, typion: " C 4", filei: "data/c4.dat" },
|
||||
IonDef { iat: 6, iz: 4, nlevs: 1, typion: " C 5", filei: " " },
|
||||
IonDef {
|
||||
iat: 6,
|
||||
iz: 0,
|
||||
nlevs: 40,
|
||||
typion: " C 1",
|
||||
filei: "data/c1.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 6,
|
||||
iz: 1,
|
||||
nlevs: 22,
|
||||
typion: " C 2",
|
||||
filei: "data/c2.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 6,
|
||||
iz: 2,
|
||||
nlevs: 46,
|
||||
typion: " C 3",
|
||||
filei: "data/c3_34+12lev.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 6,
|
||||
iz: 3,
|
||||
nlevs: 25,
|
||||
typion: " C 4",
|
||||
filei: "data/c4.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 6,
|
||||
iz: 4,
|
||||
nlevs: 1,
|
||||
typion: " C 5",
|
||||
filei: " ",
|
||||
},
|
||||
];
|
||||
|
||||
const IONS_N: &[IonDef] = &[
|
||||
IonDef { iat: 7, iz: 0, nlevs: 34, typion: " N 1", filei: "data/n1.dat" },
|
||||
IonDef { iat: 7, iz: 1, nlevs: 42, typion: " N 2", filei: "data/n2_32+10lev.dat" },
|
||||
IonDef { iat: 7, iz: 2, nlevs: 32, typion: " N 3", filei: "data/n3.dat" },
|
||||
IonDef { iat: 7, iz: 3, nlevs: 48, typion: " N 4", filei: "data/n4_34+14lev.dat" },
|
||||
IonDef { iat: 7, iz: 4, nlevs: 16, typion: " N 5", filei: "data/n5.dat" },
|
||||
IonDef { iat: 7, iz: 5, nlevs: 1, typion: " N 6", filei: " " },
|
||||
IonDef {
|
||||
iat: 7,
|
||||
iz: 0,
|
||||
nlevs: 34,
|
||||
typion: " N 1",
|
||||
filei: "data/n1.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 7,
|
||||
iz: 1,
|
||||
nlevs: 42,
|
||||
typion: " N 2",
|
||||
filei: "data/n2_32+10lev.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 7,
|
||||
iz: 2,
|
||||
nlevs: 32,
|
||||
typion: " N 3",
|
||||
filei: "data/n3.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 7,
|
||||
iz: 3,
|
||||
nlevs: 48,
|
||||
typion: " N 4",
|
||||
filei: "data/n4_34+14lev.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 7,
|
||||
iz: 4,
|
||||
nlevs: 16,
|
||||
typion: " N 5",
|
||||
filei: "data/n5.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 7,
|
||||
iz: 5,
|
||||
nlevs: 1,
|
||||
typion: " N 6",
|
||||
filei: " ",
|
||||
},
|
||||
];
|
||||
|
||||
const IONS_O: &[IonDef] = &[
|
||||
IonDef { iat: 8, iz: 0, nlevs: 33, typion: " O 1", filei: "data/o1_23+10lev.dat" },
|
||||
IonDef { iat: 8, iz: 1, nlevs: 48, typion: " O 2", filei: "data/o2_36+12lev.dat" },
|
||||
IonDef { iat: 8, iz: 2, nlevs: 41, typion: " O 3", filei: "data/o3_28+13lev.dat" },
|
||||
IonDef { iat: 8, iz: 3, nlevs: 39, typion: " O 4", filei: "data/o4.dat" },
|
||||
IonDef { iat: 8, iz: 4, nlevs: 6, typion: " O 5", filei: "data/o5.dat" },
|
||||
IonDef { iat: 8, iz: 5, nlevs: 1, typion: " O 6", filei: " " },
|
||||
IonDef {
|
||||
iat: 8,
|
||||
iz: 0,
|
||||
nlevs: 33,
|
||||
typion: " O 1",
|
||||
filei: "data/o1_23+10lev.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 8,
|
||||
iz: 1,
|
||||
nlevs: 48,
|
||||
typion: " O 2",
|
||||
filei: "data/o2_36+12lev.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 8,
|
||||
iz: 2,
|
||||
nlevs: 41,
|
||||
typion: " O 3",
|
||||
filei: "data/o3_28+13lev.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 8,
|
||||
iz: 3,
|
||||
nlevs: 39,
|
||||
typion: " O 4",
|
||||
filei: "data/o4.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 8,
|
||||
iz: 4,
|
||||
nlevs: 6,
|
||||
typion: " O 5",
|
||||
filei: "data/o5.dat",
|
||||
},
|
||||
IonDef {
|
||||
iat: 8,
|
||||
iz: 5,
|
||||
nlevs: 1,
|
||||
typion: " O 6",
|
||||
filei: " ",
|
||||
},
|
||||
];
|
||||
|
||||
fn fmt_abn(logx: f64) -> String {
|
||||
@@ -64,11 +196,11 @@ pub fn make_input5(
|
||||
|
||||
// Atoms block
|
||||
let mut atom_rows: Vec<(i32, String)> = vec![
|
||||
(2, "0.".to_string()), // 1 H
|
||||
(2, fmt_abn(params.loghe)), // 2 He
|
||||
(0, "0.".to_string()), // 3 Li
|
||||
(0, "0.".to_string()), // 4 Be
|
||||
(0, "0.".to_string()), // 5 B
|
||||
(2, "0.".to_string()), // 1 H
|
||||
(2, fmt_abn(params.loghe)), // 2 He
|
||||
(0, "0.".to_string()), // 3 Li
|
||||
(0, "0.".to_string()), // 4 Be
|
||||
(0, "0.".to_string()), // 5 B
|
||||
];
|
||||
|
||||
if has_c {
|
||||
@@ -81,7 +213,8 @@ pub fn make_input5(
|
||||
atom_rows.push((2, fmt_abn(params.logo))); // 8 O
|
||||
}
|
||||
|
||||
let natoms = 5 + (if has_c { 1 } else { 0 }) + (if has_n { 1 } else { 0 }) + (if has_o { 1 } else { 0 });
|
||||
let natoms =
|
||||
5 + (if has_c { 1 } else { 0 }) + (if has_n { 1 } else { 0 }) + (if has_o { 1 } else { 0 });
|
||||
|
||||
let mut atoms_block = format!(" {}\n* mode abn modpf\n", natoms);
|
||||
for (mode, abn) in &atom_rows {
|
||||
|
||||
@@ -8,4 +8,3 @@ pub mod models;
|
||||
pub mod nst_writer;
|
||||
pub mod runner;
|
||||
pub mod seed_finder;
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ impl FormatTime for LocalTimeFormatter {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Initializes high-performance, non-blocking structured logging for DCTS applications
|
||||
pub fn init_logging(app_name: &str, default_filter: &str) -> Result<Vec<WorkerGuard>> {
|
||||
let mut guards = Vec::new();
|
||||
@@ -29,7 +28,8 @@ pub fn init_logging(app_name: &str, default_filter: &str) -> Result<Vec<WorkerGu
|
||||
let log_outputs = env::var("LOG_OUTPUTS").unwrap_or_else(|_| "stdout,file".to_string());
|
||||
let log_dir = env::var("LOG_DIR").unwrap_or_else(|_| "data/logs".to_string());
|
||||
|
||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&log_level));
|
||||
let env_filter =
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&log_level));
|
||||
let is_json = log_format.to_lowercase() == "json";
|
||||
|
||||
let mut layers: Vec<Box<dyn Layer<tracing_subscriber::Registry> + Send + Sync>> = Vec::new();
|
||||
@@ -39,7 +39,9 @@ pub fn init_logging(app_name: &str, default_filter: &str) -> Result<Vec<WorkerGu
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(std::io::stdout());
|
||||
guards.push(guard);
|
||||
|
||||
let fmt_layer = fmt::layer().with_timer(LocalTimeFormatter).with_writer(non_blocking);
|
||||
let fmt_layer = fmt::layer()
|
||||
.with_timer(LocalTimeFormatter)
|
||||
.with_writer(non_blocking);
|
||||
if is_json {
|
||||
layers.push(fmt_layer.json().with_ansi(false).boxed());
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// 6D grid point parameter specification
|
||||
@@ -84,7 +84,6 @@ impl std::fmt::Display for GridPointStatus {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl From<&str> for GridPointStatus {
|
||||
fn from(s: &str) -> Self {
|
||||
match s {
|
||||
@@ -106,6 +105,10 @@ pub struct TaskSpec {
|
||||
pub task_type: TaskType,
|
||||
pub seed_point_name: Option<String>,
|
||||
pub timeout_sec: u64,
|
||||
/// 所属工作流名称,用于按工作流隔离队列清理(stop_workflow 只清当前工作流的任务)。
|
||||
/// 旧数据反序列化时缺省为 None。
|
||||
#[serde(default)]
|
||||
pub workflow_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -251,10 +254,12 @@ mod tests {
|
||||
assert_eq!(GridPointStatus::Failed.to_string(), "failed");
|
||||
|
||||
assert_eq!(GridPointStatus::from("queued"), GridPointStatus::Queued);
|
||||
assert_eq!(GridPointStatus::from("converged"), GridPointStatus::Converged);
|
||||
assert_eq!(
|
||||
GridPointStatus::from("converged"),
|
||||
GridPointStatus::Converged
|
||||
);
|
||||
assert_eq!(GridPointStatus::from("done"), GridPointStatus::Converged);
|
||||
assert_eq!(GridPointStatus::from("failed"), GridPointStatus::Failed);
|
||||
assert_eq!(GridPointStatus::from("unknown"), GridPointStatus::Pending);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,4 +63,3 @@ mod tests {
|
||||
assert!(content.contains("IELCOR=-1"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+72
-16
@@ -6,11 +6,11 @@ use crate::gen_input5::make_input5;
|
||||
use crate::models::{GridPointParams, ModelSummary, StageSummary, TaskType};
|
||||
use crate::nst_writer::generate_nst_content;
|
||||
use anyhow::Result;
|
||||
use tokio::fs::File;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use tokio::process::Command as AsyncCommand;
|
||||
use std::time::Instant;
|
||||
use tokio::fs::File;
|
||||
use tokio::process::Command as AsyncCommand;
|
||||
use tracing::{info, warn};
|
||||
|
||||
pub fn default_cold_chain() -> Vec<StageConfig> {
|
||||
@@ -130,7 +130,15 @@ impl<'a> ExecutionRunner<'a> {
|
||||
seed_atmos: Option<&Path>,
|
||||
synspec_cfg: Option<&SynspecConfig>,
|
||||
) -> Result<ModelSummary> {
|
||||
self.run_model_with_timeout(params, task_type, custom_chain, seed_atmos, synspec_cfg, 7200).await
|
||||
self.run_model_with_timeout(
|
||||
params,
|
||||
task_type,
|
||||
custom_chain,
|
||||
seed_atmos,
|
||||
synspec_cfg,
|
||||
7200,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn run_model_with_timeout(
|
||||
@@ -157,7 +165,9 @@ impl<'a> ExecutionRunner<'a> {
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let abs_data_dir = tokio::fs::canonicalize(&self.runtime.data_dir).await.unwrap_or_else(|_| self.runtime.data_dir.clone());
|
||||
let abs_data_dir = tokio::fs::canonicalize(&self.runtime.data_dir)
|
||||
.await
|
||||
.unwrap_or_else(|_| self.runtime.data_dir.clone());
|
||||
if let Err(e) = std::os::unix::fs::symlink(&abs_data_dir, &link_data) {
|
||||
warn!("构建 data 数据集软链时发生提示性告警: {}", e);
|
||||
}
|
||||
@@ -171,7 +181,10 @@ impl<'a> ExecutionRunner<'a> {
|
||||
if let Some(seed_path) = seed_atmos {
|
||||
if seed_path.is_file() {
|
||||
if let Err(e) = tokio::fs::copy(seed_path, &fort8).await {
|
||||
warn!("向工作沙盒引导填载首期收敛模型种子 fort.8 发生复制错误: {}", e);
|
||||
warn!(
|
||||
"向工作沙盒引导填载首期收敛模型种子 fort.8 发生复制错误: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,15 +232,24 @@ impl<'a> ExecutionRunner<'a> {
|
||||
} else if let Some(ref s_path) = current_seed {
|
||||
if s_path.is_file() {
|
||||
if let Err(e) = tokio::fs::copy(s_path, &fort8).await {
|
||||
warn!("阶段 {} 重载候选近邻推算种子模型期间发生文件复制异常: {}", stage_def.label, e);
|
||||
warn!(
|
||||
"阶段 {} 重载候选近邻推算种子模型期间发生文件复制异常: {}",
|
||||
stage_def.label, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run tlusty.exe
|
||||
let fin = File::open(&input5_path).await?.into_std().await;
|
||||
let fout = File::create(model_dir.join(format!("{}.6", name))).await?.into_std().await;
|
||||
let ferr = File::create(model_dir.join(format!("{}.err", name))).await?.into_std().await;
|
||||
let fout = File::create(model_dir.join(format!("{}.6", name)))
|
||||
.await?
|
||||
.into_std()
|
||||
.await;
|
||||
let ferr = File::create(model_dir.join(format!("{}.err", name)))
|
||||
.await?
|
||||
.into_std()
|
||||
.await;
|
||||
|
||||
let child = AsyncCommand::new(&self.runtime.tlusty_exe)
|
||||
.current_dir(&model_dir)
|
||||
@@ -246,7 +268,6 @@ impl<'a> ExecutionRunner<'a> {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let fort9 = model_dir.join("fort.9");
|
||||
let fort7 = model_dir.join("fort.7");
|
||||
|
||||
@@ -293,7 +314,10 @@ impl<'a> ExecutionRunner<'a> {
|
||||
stage_summaries.push(stage_summary);
|
||||
|
||||
if !final_converged && stage_def.require_converged {
|
||||
warn!("阶段 {} 要求收敛但未达标,中止后续收敛链阶段", stage_def.label);
|
||||
warn!(
|
||||
"阶段 {} 要求收敛但未达标,中止后续收敛链阶段",
|
||||
stage_def.label
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -344,14 +368,19 @@ impl<'a> ExecutionRunner<'a> {
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let abs_linelist = tokio::fs::canonicalize(&self.runtime.linelist).await.unwrap_or_else(|_| self.runtime.linelist.clone());
|
||||
let abs_linelist = tokio::fs::canonicalize(&self.runtime.linelist)
|
||||
.await
|
||||
.unwrap_or_else(|_| self.runtime.linelist.clone());
|
||||
let _ = std::os::unix::fs::symlink(&abs_linelist, &fort19_path);
|
||||
}
|
||||
|
||||
let input5_path = model_dir.join(format!("{}.5", name));
|
||||
if input5_path.is_file() {
|
||||
let fin = File::open(&input5_path).await?.into_std().await;
|
||||
let fout = File::create(model_dir.join(format!("{}.log", name))).await?.into_std().await;
|
||||
let fout = File::create(model_dir.join(format!("{}.log", name)))
|
||||
.await?
|
||||
.into_std()
|
||||
.await;
|
||||
|
||||
let child = AsyncCommand::new(&self.runtime.synspec_exe)
|
||||
.current_dir(&model_dir)
|
||||
@@ -361,7 +390,8 @@ impl<'a> ExecutionRunner<'a> {
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let status_res = run_child_async_with_timeout(child, timeout_sec).await;
|
||||
let synspec_timeout_sec = 600_u64.min(timeout_sec);
|
||||
let status_res = run_child_async_with_timeout(child, synspec_timeout_sec).await;
|
||||
let rc = match status_res {
|
||||
Ok(st) => st.code().unwrap_or(-1),
|
||||
Err(e) => {
|
||||
@@ -374,13 +404,25 @@ impl<'a> ExecutionRunner<'a> {
|
||||
|
||||
// Copy/move outputs: fort.7 (Synspec spectrum) -> .spec, fort.17 -> .cont, fort.12 -> .iden
|
||||
if model_dir.join("fort.7").is_file() {
|
||||
let _ = tokio::fs::rename(model_dir.join("fort.7"), model_dir.join(format!("{}.spec", name))).await;
|
||||
let _ = tokio::fs::rename(
|
||||
model_dir.join("fort.7"),
|
||||
model_dir.join(format!("{}.spec", name)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if model_dir.join("fort.17").is_file() {
|
||||
let _ = tokio::fs::copy(model_dir.join("fort.17"), model_dir.join(format!("{}.cont", name))).await;
|
||||
let _ = tokio::fs::copy(
|
||||
model_dir.join("fort.17"),
|
||||
model_dir.join(format!("{}.cont", name)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if model_dir.join("fort.12").is_file() {
|
||||
let _ = tokio::fs::copy(model_dir.join("fort.12"), model_dir.join(format!("{}.iden", name))).await;
|
||||
let _ = tokio::fs::copy(
|
||||
model_dir.join("fort.12"),
|
||||
model_dir.join(format!("{}.iden", name)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -415,3 +457,17 @@ impl<'a> ExecutionRunner<'a> {
|
||||
Ok(summary)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_synspec_timeout_calculation() {
|
||||
let long_tlusty_timeout: u64 = 7200;
|
||||
let synspec_timeout = 600_u64.min(long_tlusty_timeout);
|
||||
assert_eq!(synspec_timeout, 600);
|
||||
|
||||
let short_tlusty_timeout: u64 = 300;
|
||||
let synspec_timeout_short = 600_u64.min(short_tlusty_timeout);
|
||||
assert_eq!(synspec_timeout_short, 300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,11 @@ pub fn calculate_seed_distance(cand: &GridPointParams, target: &GridPointParams)
|
||||
+ (cand.logn - target.logn).abs()
|
||||
+ (cand.logo - target.logo).abs();
|
||||
|
||||
if d_teff < 1.0 && d_logg < 0.01 && d_loghe < 0.01 {
|
||||
// exact family 判定:Teff/logg/logHe 视为“同物理族”,仅 CNO 丰度不同。
|
||||
// Teff 容忍度取半步 5000K:实际网格 Teff 档位通常为整数千(20000/30000/.../60000),
|
||||
// 半步既能覆盖 config_dense 等 10000K 步长的相邻档互作种子,
|
||||
// 又避免跨过大 Teff 间距导致 sdB 高温模型用低温种子而不收敛(sdB_cno 步长 40000K 仍不命中 exact)。
|
||||
if d_teff < 5000.0 && d_logg < 0.01 && d_loghe < 0.01 {
|
||||
(true, d_cno)
|
||||
} else {
|
||||
// 距离公式物理意义与标定阐释:
|
||||
@@ -32,6 +36,3 @@ pub fn calculate_seed_distance(cand: &GridPointParams, target: &GridPointParams)
|
||||
(false, global_d)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user