modify: rust 插件
This commit is contained in:
+227
@@ -0,0 +1,227 @@
|
||||
//! 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, ®istry)?;
|
||||
|
||||
// 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())
|
||||
}
|
||||
Reference in New Issue
Block a user