Files
chongming-worker-rs/src/serve.rs
T
2026-08-31 17:18:03 +08:00

227 lines
8.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Worker 进程主流程(对齐 Python process.py `worker_serve`)
//!
//! 流程:
//! 1. 读全局 config.toml([zenoh].json5_path)→ 打开 Zenoh 会话
//! 2. 自读 config/workers/<type>/config.toml([workers] 节展平 + mqtt_topics + cache)
//! 3. 加载路由描述符,按 handler 注册表做契约自检(跳过无 handler 的端点)
//! 4. (可选)建立共享缓存,登记实例参与方并启动心跳(多实例全局分摊)
//! 5. 发布路由描述符 → 网关 RouteRegistry 动态挂载 HTTP 路由
//! 6. 建 WorkerPool:每副本槽位 = liveliness token + 业务 Queryable + __admin__ Queryable
//! 7. 每 10s 重发布描述符(自愈)
//! 8. SIGTERM/SIGINT → 摘除路由 → 优雅关停全部副本 → 摘除实例 → 关闭会话
use std::sync::Arc;
use serde_json::Value;
use tokio::signal::unix::{signal, SignalKind};
use tokio::sync::Mutex;
use crate::cache::Cache;
use crate::config::{load_global_config, load_plugin_config};
use crate::descriptor::load_routes_from_dir;
use crate::publisher::{announce_loop, RoutePublisher};
use crate::runtime::WorkerPool;
use crate::HandlerRegistry;
/// serve 入口参数
pub struct ServeOptions {
/// 全局配置文件路径(读取 [zenoh] 节)
pub config_path: String,
/// 插件配置目录(默认 "config/workers")
pub config_dir: String,
/// 副本数覆盖(None = 使用插件配置中的 count)
pub count: Option<usize>,
/// 外部进程实例标识(多实例全局分摊用;缺省 {hostname}:{pid})
pub instance: Option<String>,
}
impl Default for ServeOptions {
fn default() -> Self {
Self {
config_path: "config.toml".into(),
config_dir: "config/workers".into(),
count: None,
instance: None,
}
}
}
/// 实例参与者心跳间隔(秒,与网关/ Python 一致)
const INSTANCE_HEARTBEAT_SECS: u64 = 15;
/// 启动独立 Worker 进程(阻塞直到收到 SIGTERM/SIGINT 并完成优雅退出)
pub async fn serve(
worker_type: &str,
registry: Arc<HandlerRegistry>,
opts: ServeOptions,
) -> Result<(), String> {
// 1. 全局配置 → Zenoh 会话
let global = load_global_config(&opts.config_path)?;
tracing::info!("[{}] 读取 Zenoh 配置: {}", worker_type, global.zenoh_json5_path);
let zconfig = zenoh::Config::from_file(&global.zenoh_json5_path)
.map_err(|e| format!("加载 Zenoh 配置 {} 失败: {e}", global.zenoh_json5_path))?;
let session = zenoh::open(zconfig)
.await
.map_err(|e| format!("打开 Zenoh 会话失败: {e}"))?;
tracing::info!("[{}] Zenoh 会话已建立", worker_type);
// 2. 插件配置
let plugin = load_plugin_config(worker_type, &opts.config_dir)?;
if plugin.load_mode != "external" {
tracing::warn!(
"[{}] 插件配置 load_mode 不是 \"external\":网关也会在进程内加载该插件,\
外部进程将与网关内副本重复服务。",
worker_type,
);
}
// 2.5 共享缓存([workers.cache],进程插件自行管理)+ 实例参与方登记
let cache_cfg = plugin
.runtime
.get("workers")
.and_then(|w| w.get("cache"))
.cloned();
let (cache, backend) = match &cache_cfg {
Some(cfg) => Cache::from_config(cfg).await,
None => (Cache::None, "none".to_string()),
};
let mode = cache_cfg
.as_ref()
.and_then(|c| c.get("launch_worker_count").and_then(Value::as_str))
.unwrap_or("toml")
.to_lowercase();
let instance_id = opts.instance.clone().unwrap_or_else(default_instance_id);
if cache.enabled() {
crate::cache::heartbeat_instance(&cache, worker_type, &instance_id).await;
tracing::info!(
"[{}] 已启用配置缓存(backend={backend},mode={mode})并登记实例参与方 {}",
worker_type, instance_id,
);
}
// 3. 路由描述符 + 契约自检
let all_routes = load_routes_from_dir(worker_type, &opts.config_dir)?;
let routes = contract_self_check(worker_type, &all_routes, &registry)?;
// 4. 发布路由描述符
let publisher = Arc::new(RoutePublisher::new(session.clone(), worker_type));
publisher
.publish_routes(&routes, &plugin.mqtt_topics)
.await?;
// 5. 解析本进程应承载的副本数(多实例全局分摊)
let default_count = opts.count.unwrap_or(plugin.count).max(1);
let count = crate::cache::resolve_worker_count(
&cache,
worker_type,
default_count,
&mode,
None,
Some(&instance_id),
)
.await
.max(1);
// 6. 建 WorkerPool
let pool: Arc<Mutex<WorkerPool>> = Arc::new(Mutex::new(WorkerPool::new(
worker_type.to_string(),
session.clone(),
registry,
routes.clone(),
plugin.runtime.clone(),
opts.config_dir,
cache.clone(),
instance_id.clone(),
)));
{
let mut p = pool.lock().await;
p.set_target(count, &pool).await;
}
tracing::info!("[{worker_type}] Worker 池就绪({count} 副本)");
// 7. 周期重发布(自愈)+ 实例心跳
let announce = tokio::spawn(announce_loop(
publisher.clone(),
routes.clone(),
plugin.mqtt_topics.clone(),
));
let hb_cache = cache.clone();
let hb_type = worker_type.to_string();
let hb_instance = instance_id.clone();
let heartbeat = tokio::spawn(async move {
let mut interval =
tokio::time::interval(std::time::Duration::from_secs(INSTANCE_HEARTBEAT_SECS));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
interval.tick().await;
crate::cache::heartbeat_instance(&hb_cache, &hb_type, &hb_instance).await;
}
});
tracing::info!("[{worker_type}] Worker 进程就绪,等待退出信号...");
// 8. 等待退出信号
let mut sigterm = signal(SignalKind::terminate())
.map_err(|e| format!("注册 SIGTERM 失败: {e}"))?;
let mut sigint = signal(SignalKind::interrupt())
.map_err(|e| format!("注册 SIGINT 失败: {e}"))?;
tokio::select! {
_ = sigterm.recv() => tracing::info!("[{worker_type}] 收到 SIGTERM,优雅退出"),
_ = sigint.recv() => tracing::info!("[{worker_type}] 收到 SIGINT,优雅退出"),
}
// 9. 优雅退出:停周期任务 → 摘除路由 → 关停全部副本 → 摘除实例 → 关闭会话
announce.abort();
let _ = announce.await;
heartbeat.abort();
let _ = heartbeat.await;
publisher.unpublish().await;
{
let mut p = pool.lock().await;
p.shutdown().await;
}
if cache.enabled() {
crate::cache::unregister_instance(&cache, worker_type, &instance_id).await;
}
session
.close()
.await
.map_err(|e| format!("关闭 Zenoh 会话失败: {e}"))?;
tracing::info!("[{worker_type}] Worker 进程已退出");
Ok(())
}
/// 契约自检:跳过无 handler 的端点,无可用端点则报错
fn contract_self_check(
worker_type: &str,
all_routes: &[Value],
registry: &HandlerRegistry,
) -> Result<Vec<Value>, String> {
let mut routes: Vec<Value> = Vec::new();
for r in all_routes {
let subject = r.get("subject").and_then(Value::as_str).unwrap_or("");
let method = r.get("method").and_then(Value::as_str).unwrap_or("GET");
let path = r.get("path").and_then(Value::as_str).unwrap_or("?");
if registry.has(subject, method) {
routes.push(r.clone());
} else {
tracing::warn!(
"[{}] 跳过 {method} {path} (subject={subject}): 无对应 handler 注册",
worker_type,
);
}
}
if routes.is_empty() {
return Err(format!(
"[{worker_type}] 没有任何可发布的端点(handler 注册表为空或全部被跳过)"
));
}
tracing::info!("[{worker_type}] 契约自检通过,可发布 {} 个路由描述符", routes.len());
Ok(routes)
}
/// 默认实例 id:{hostname}:{pid}(同机多进程自动区分)
fn default_instance_id() -> String {
let host = std::env::var("HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
format!("{host}:{}", std::process::id())
}