//! Worker 进程主流程(对齐 Python process.py `worker_serve`) //! //! 流程: //! 1. 读全局 config.toml([zenoh].json5_path)→ 打开 Zenoh 会话 //! 2. 自读 config/workers//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, /// 外部进程实例标识(多实例全局分摊用;缺省 {hostname}:{pid}) pub instance: Option, } 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, 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> = 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, String> { let mut routes: Vec = 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()) }