feat(all): 重炼 crates/common 核心组件、上线 Web 运维看板与 Docker 容器化部署

This commit is contained in:
fmq
2026-07-28 10:31:57 +08:00
commit 4b4238d702
71 changed files with 163402 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
use anyhow::Result;
use server::api::{self, AppState};
use server::db::Database;
use server::scheduler::GridScheduler;
use axum::{
routing::{get, post},
Router,
};
use clap::Parser;
use common::config::ServerConfig;
use common::logging::init_logging;
use mq::sqlite_queue::SqliteTaskQueue;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::time::{sleep, Duration};
use tower_http::cors::CorsLayer;
use tower_http::services::{ServeDir, ServeFile};
use tracing::info;
#[derive(Parser, Debug)]
#[command(name = "server", version = "0.1.0", about = "Distributed Computing TLUSTY/SYNSPEC (DCTS) Server")]
struct CliArgs {
/// Optional path to workflow configuration YAML file to auto-register on startup
#[arg(short = 'w', long = "workflow")]
workflow: Option<PathBuf>,
/// Listen port (overrides DCTS_PORT env var)
#[arg(short = 'p', long = "port")]
port: Option<u16>,
}
#[tokio::main]
async fn main() -> Result<()> {
dotenvy::dotenv().ok();
let _logging_guards = init_logging("server", "info,server=debug")?;
let cli = CliArgs::parse();
info!("启动 DCTS 服务端 (Distributed Computing TLUSTY/SYNSPEC Server)...");
let mut server_cfg = ServerConfig::default();
if let Some(port) = cli.port {
server_cfg.bind_addr = format!("0.0.0.0:{}", port);
}
if let Some(wf) = cli.workflow {
server_cfg.grid_config = wf.to_string_lossy().to_string();
}
let db = Database::new(&server_cfg.db_path).await?;
let queue = Arc::new(SqliteTaskQueue::new(&server_cfg.queue_db_path).await?);
let scheduler = Arc::new(GridScheduler::new(
db.clone(),
queue.clone(),
server_cfg.results_dir.clone(),
));
// Auto-register sdB_cno.yaml if exists and not yet in DB
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);
} else {
info!("已在数据库中成功预注册默认工作流 'sdB_cno'");
}
}
}
let state = AppState {
db,
queue: queue.clone(),
scheduler: scheduler.clone(),
results_dir: server_cfg.results_dir,
auth_token: server_cfg.auth_token.clone(),
};
// Background loop for stale task requeueing, offline node detection, and scheduler checking
let bg_db = state.db.clone();
let bg_queue = queue.clone();
let bg_scheduler = scheduler.clone();
let stale_sec = server_cfg.stale_sec;
let node_stale_sec = server_cfg.node_stale_sec;
tokio::spawn(async move {
loop {
sleep(Duration::from_secs(30)).await;
if let Ok(requeued_points) = bg_queue.requeue_stale_tasks(stale_sec).await {
if !requeued_points.is_empty() {
info!("重新将 {} 个超时/掉线任务放回待计算队列", requeued_points.len());
let _ = bg_db.reset_specific_grid_points_to_pending(&requeued_points).await;
}
}
if let Ok(offline) = bg_db.mark_stale_nodes_offline(node_stale_sec).await {
if offline > 0 {
info!("已标记 {} 个心跳超时的计算节点为离线状态", offline);
}
}
if let Err(e) = bg_scheduler.schedule_pending_tasks().await {
tracing::warn!("后台定时性任务调度检测失败: {}", e);
}
}
});
let api_router = Router::new()
// Core Node & Task API
.route("/node/register", post(api::node::register_node))
.route("/node/heartbeat", post(api::node::heartbeat_node))
.route("/task/claim", post(api::task::claim_task))
.route("/task/report", post(api::task::report_task))
.route("/seed/:name", get(api::seed::download_seed))
.route("/status", get(api::status::get_status))
// Static Data API
.route("/data/file/*filename", get(api::data::download_single_data_file))
.route("/data/linelist", get(api::data::download_linelist))
// Workflow Management CRUD API
.route("/workflows", get(api::workflow::list_workflows).post(api::workflow::save_workflow))
.route("/workflows/:name", get(api::workflow::get_workflow).put(api::workflow::save_workflow).delete(api::workflow::delete_workflow))
.route("/workflows/:name/start", post(api::workflow::start_workflow))
.route("/workflows/:name/stop", post(api::workflow::stop_workflow));
let api_router = if state.auth_token.is_some() {
info!("已为 DCTS 服务端 API 路由启用 Bearer Token / X-API-Key 访问控制鉴权");
let auth_layer = axum::middleware::from_fn_with_state(state.clone(), api::auth_middleware);
api_router.layer(auth_layer)
} else {
tracing::warn!("⚠️ 警告:未检测到 DCTS_AUTH_TOKEN 环境变量,服务端目前运行在【内网无鉴权模式】!所有 REST API 接口均为公开可访问状态。");
api_router
};
// Host Dashboard SPA static files from dashboard/dist if directory exists or fallback to index.html
let serve_dir = ServeDir::new("dashboard/dist")
.fallback(ServeFile::new("dashboard/dist/index.html"));
let app = Router::new()
.nest("/api", api_router)
.layer(CorsLayer::permissive())
.fallback_service(serve_dir)
.with_state(state);
let addr: SocketAddr = server_cfg.bind_addr.parse()?;
info!("DCTS 服务端已在 http://{} 启动监听", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app)
.with_graceful_shutdown(async {
let _ = tokio::signal::ctrl_c().await;
info!("收到 Ctrl+C 终止信号,DCTS 服务端准备优雅关闭...");
})
.await?;
info!("DCTS 服务端已安全关闭。");
Ok(())
}