modify: worker 运行时下沉 chongming-worker SDK——进程插件与网关零导入关系

This commit is contained in:
2026-08-14 18:04:57 +08:00
parent 1f727bcdd7
commit 1995b2d170
10 changed files with 120 additions and 495 deletions
+7
View File
@@ -384,6 +384,13 @@ uv pip install -e utils/<name> # 安装/更新工具包
进程入口 `uv run worker_serve --type <name>`),经 Zenoh 在 `chongming/routes/**` 进程入口 `uv run worker_serve --type <name>`),经 Zenoh 在 `chongming/routes/**`
注册路由,网关动态挂载 HTTP 端点 注册路由,网关动态挂载 HTTP 端点
- 两种模式可同时存在(混合部署) - 两种模式可同时存在(混合部署)
- **进程插件与网关完全隔离**:进程插件只依赖 `chongming-worker` SDK + zenoh,
与网关(`chongming` 包)零导入关系,仅通过 Zenoh(`chongming/routes/**` 注册路由、
`worker/<type>/<id>` 服务发现)通讯。`workers/` 目录仅为开发便利,
**生产部署时进程插件与网关分别打包到不同容器**:
- 网关容器:`chongming`(含 `src/chongming/`)+ 依赖
- 进程插件容器:插件代码(`workers/<name>/`)+ `chongming-worker` + 依赖,
入口 `chongming-worker --type <name>`(或 `uv run worker_serve --type <name>`)
- **Zenoh 配置**:`config/zenoh_config.json5` - **Zenoh 配置**:`config/zenoh_config.json5`
- **插件注册**:`config/workers/<name>/config.toml` 的 `[workers]` 节,定义加载方式、副本数与运行时配置(旧全局 `config/workers/config.toml` 仅作兜底兼容) - **插件注册**:`config/workers/<name>/config.toml` 的 `[workers]` 节,定义加载方式、副本数与运行时配置(旧全局 `config/workers/config.toml` 仅作兜底兼容)
- **路由定义**:`config/workers/<name>/*.toml`,每个端点一个文件 - **路由定义**:`config/workers/<name>/*.toml`,每个端点一个文件
+1 -1
View File
@@ -68,4 +68,4 @@ gen_models = "chongming.scripts.gen_models:gen_models"
gen_plugin = "chongming.scripts.gen_plugin:gen_plugin" gen_plugin = "chongming.scripts.gen_plugin:gen_plugin"
gunicorn = "chongming:gunicorn_serve" gunicorn = "chongming:gunicorn_serve"
serve = "chongming:serve" serve = "chongming:serve"
worker_serve = "chongming:worker_serve" worker_serve = "chongming_worker.process:worker_serve"
-120
View File
@@ -23,126 +23,6 @@ def serve():
uvicorn.run("chongming.app:app", **server_config) uvicorn.run("chongming.app:app", **server_config)
def worker_serve():
"""
独立 Worker 进程入口(process 模式)
流程: 读取全局配置 → 发现本插件的 config/workers/<type>/config.toml →
import 插件(触发 @rpc_method 注册)→ 契约校验(硬失败则退出)→
创建 Worker 副本(count 进程内保留)→ 经 Zenoh 发布路由描述符 →
等待 SIGTERM/SIGINT → 摘除路由 + cleanup + 关闭会话。
用法: uv run worker_serve --type example [--count 3] [--config config.toml]
需在项目根目录运行(配置与插件均按相对路径解析)。
"""
import argparse
import asyncio
import inspect
import os
import signal
import sys
import zenoh
from .app.core.contracts import validate_worker_contracts
from .app.core.lifespan_setup import _discover_plugins
from .app.core.route_publisher import RoutePublisher
parser = argparse.ArgumentParser(description="chongming 独立 Worker 进程")
parser.add_argument("--type", required=True, help="插件类型(如 example / auth)")
parser.add_argument("--count", type=int, default=None, help="副本数(覆盖配置中的 count)")
parser.add_argument("--config", default="config.toml", help="全局配置文件路径")
args = parser.parse_args()
# 控制台脚本启动时 sys.path 不含项目根目录,显式加入以保证
# plugins.<name>(inline 插件)与 workers.<name>(进程插件)均可导入
project_root = os.path.abspath(os.getcwd())
if project_root not in sys.path:
sys.path.insert(0, project_root)
config = load_config()
logger = get_logger("app")
plugins = _discover_plugins()
wc = next((p for p in plugins if p.get("type") == args.type), None)
if wc is None:
logger.error("未找到插件配置: %s(请检查 config/workers/%s/config.toml)", args.type, args.type)
sys.exit(1)
if wc.get("load_mode", "inline") != "external":
logger.warning(
"[%s] 插件配置 load_mode 不是 \"external\":网关也会在进程内加载该插件,"
"外部进程将与网关内副本重复服务。若以独立进程运行,请在插件 "
"config.toml 的 [workers] 节设置 load_mode = \"external\"。",
args.type,
)
async def _run():
# 1. Zenoh 会话
zconfig = zenoh.Config()
zconfig.from_file(config.get("zenoh", {}).get("json5_path", "config/zenoh_config.json5"))
session = zenoh.open(zconfig)
logger.info("[%s] Zenoh 会话已建立", args.type)
try:
# 2. import 插件(触发 @rpc_method 注册),然后契约校验
module = __import__(wc["module"], fromlist=[wc["factory"]])
routes = validate_worker_contracts(args.type)
# 3. 创建 Worker 副本(worker_manager=None:跨进程经 liveliness 发现)
factory = getattr(module, wc["factory"])
count = args.count or wc.get("count", 1)
if inspect.iscoroutinefunction(factory):
tasks = await factory(session, wc["config"], count, worker_manager=None)
else:
tasks = factory(session, wc["config"], count, worker_manager=None)
# 4. 发布路由描述符(网关 RouteRegistry 订阅后动态挂载 HTTP 路由)
publisher = RoutePublisher(session, args.type)
mqtt_topics = wc.get("mqtt_topics") or []
publisher.publish_routes(routes, mqtt_topics)
# 5. 等待退出信号;期间周期性重发布(网关后启动/样本丢失时自愈)
stop_event = asyncio.Event()
loop = asyncio.get_running_loop()
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, stop_event.set)
async def _announce():
while not stop_event.is_set():
try:
await asyncio.wait_for(stop_event.wait(), timeout=10.0)
return
except asyncio.TimeoutError:
publisher.publish_routes(routes, mqtt_topics)
announce_task = asyncio.create_task(_announce())
logger.info("[%s] Worker 进程就绪,等待退出信号...", args.type)
await stop_event.wait()
announce_task.cancel()
await asyncio.gather(announce_task, return_exceptions=True)
# 6. 优雅退出:摘除路由 → cleanup → 取消副本任务 → 关闭会话
publisher.unpublish()
cleanup = getattr(module, wc.get("cleanup", ""), None)
if cleanup is not None:
worker_id = f"worker/{args.type}/worker-i-{os.getpid()}"
if inspect.iscoroutinefunction(cleanup):
await cleanup(worker_id)
else:
cleanup(worker_id)
for t in tasks:
t.cancel()
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
logger.info("[%s] Worker 进程已退出", args.type)
finally:
session.close()
try:
asyncio.run(_run())
except KeyboardInterrupt:
pass
def gunicorn_serve(): def gunicorn_serve():
""" """
使用 gunicorn 启动服务 使用 gunicorn 启动服务
+14 -191
View File
@@ -1,166 +1,31 @@
""" """
启动契约校验(config/workers TOML ↔ @rpc_method 注册表 ↔ gen_models 生成模型) 启动契约校验(网关 inline 模式)
挂载于 lifespan 的 load_workers 与 register_routes 之间,此时插件模块已导入 校验逻辑的唯一实现在 chongming_worker.contracts(网关与 worker 进程共享),
(@rpc_method 注册表就绪)且 TOML 路由尚未注册到 FastAPI。 本模块只做网关侧的编排:对 inline 插件批量校验、日志汇总、硬失败终止启动。
策略: external 插件在自己进程内做同样校验(validate_worker_contracts),网关侧跳过。
- 硬失败(汇总后 raise,启动终止):
HTTP 路由(GET/POST/PUT/DELETE/PATCH 且 internal != true)在注册表中
缺少对应 subject#METHOD 的 @rpc_method → 该路由运行时必然 503,提前失败。
- 软警告(仅日志汇总,不阻断启动):
[MQTT] / internal 路由缺少注册、已注册方法无 TOML 路由、
TOML 声明 params/response_model 但生成模型缺失(gen_models 过期)、孤儿模型。
""" """
import importlib from typing import Any, Dict, List
from typing import Any, Dict, List, Optional, Set
from chongming_worker import get_all_rpc_methods from chongming_worker import get_all_rpc_methods
from chongming_worker.contracts import check_plugin, load_plugin_models
from chongming_worker.route_config import load_worker_routes_from_dir
from .route_generator import load_worker_routes_from_dir
from .logger import get_logger from .logger import get_logger
logger = get_logger("contracts") logger = get_logger("contracts")
# 参与 HTTP 路由注册的方法(与 route_generator 的注册行为一致)
HTTP_METHODS = ("GET", "POST", "PUT", "DELETE", "PATCH")
def _model_name(base: str, method: str, suffix: str) -> str:
"""与 gen_models._model_name 保持一致的模型命名: <Op><Method><Suffix>"""
method_title = "Mqtt" if method == "MQTT" else method.title()
return f"{base.title().replace('-', '').replace('_', '')}{method_title}{suffix}"
def _load_plugin_models(worker_type: str) -> Optional[Set[str]]:
"""
加载插件 models.pydantic 中所有 Pydantic 模型的类名集合。
返回 None 表示模块不存在(插件无生成模型,跳过模型相关校验)。
注: gen_models 生成的 `<Plugin>Response` 联合类型——多模型时为 UnionType
(自动被 isinstance(type) 过滤),单模型时是类的别名(与真实模型同一对象,
按对象去重排除),两者都不会被误报为孤儿模型。
"""
from pydantic import BaseModel
for mod_name in (
f"plugins.{worker_type}.models.pydantic",
f"src.plugins.{worker_type}.models.pydantic",
f"workers.{worker_type}.models.pydantic",
):
try:
mod = importlib.import_module(mod_name)
except ImportError:
continue
names: Set[str] = set()
seen_objs: Set[int] = set()
for name in dir(mod):
obj = getattr(mod, name)
if (
isinstance(obj, type)
and issubclass(obj, BaseModel)
and obj is not BaseModel
and id(obj) not in seen_objs
):
seen_objs.add(id(obj))
# 用 obj.__name__ 而非 dir() 名称:gen_models 生成的
# <Plugin>Response 别名与真实模型同一对象,别名保留原类名,
# 按对象去重后自动收敛到真实模型名(避免别名抢占排序位)
names.add(obj.__name__)
return names
return None
def _check_plugin(
worker_type: str,
toml_routes: List[Dict[str, Any]],
registered: Dict[str, Any],
model_names: Optional[Set[str]],
) -> tuple[List[str], List[str]]:
"""
对单个插件执行 TOML ↔ 注册表 ↔ 生成模型一致性校验。
Args:
worker_type: 插件类型
toml_routes: load_worker_routes_from_dir() 输出的路由列表
registered: get_all_rpc_methods().get(worker_type, {}) 注册表
model_names: 插件 models.pydantic 的模型名集合(None 表示无生成模型)
Returns:
(hard_errors, warnings): 硬错误与软警告消息列表
"""
hard_errors: List[str] = []
warnings: List[str] = []
toml_keys = {f"{r['subject']}#{r['method']}" for r in toml_routes}
for r in toml_routes:
key = f"{r['subject']}#{r['method']}"
route_desc = f"{r['method']} {r['path']} -> subject '{r['subject']}'"
if key in registered:
continue
if r["method"] in HTTP_METHODS and not r.get("internal", False):
# 硬失败:运行时必然 503
hint = ""
if not registered:
hint = "(该插件注册表为空,请检查启动日志中的 '加载 Worker 插件失败' 错误)"
hard_errors.append(
f"[{worker_type}] {route_desc} 缺少对应的 @rpc_method 注册{hint}"
)
else:
# 软警告:MQTT / internal 端点缺失注册
warnings.append(
f"[{worker_type}] {route_desc} 无对应的 @rpc_method(method='{r['method']}') 注册"
)
# 已注册方法无 TOML 路由(反向漂移检测)
for key in registered:
if key not in toml_keys:
warnings.append(
f"[{worker_type}] 已注册 '{key}' 但无对应 TOML 路由定义"
)
# 模型一致性(仅当插件存在生成模型时校验)
if model_names is not None:
referenced: Set[str] = set()
for r in toml_routes:
route_name = r.get("route_name", "")
if not route_name:
continue
method = r["method"]
if r.get("params"):
expected = _model_name(route_name, method, "Request")
referenced.add(expected)
if expected not in model_names:
warnings.append(
f"[{worker_type}] '{r['subject']}' [{method}] 声明了 params,"
f"但模型 {expected} 不存在(可能未运行 gen_models)"
)
if r.get("response_model"):
expected = _model_name(route_name, method, "Response")
referenced.add(expected)
if expected not in model_names:
warnings.append(
f"[{worker_type}] '{r['subject']}' [{method}] 声明了 response_model,"
f"但模型 {expected} 不存在(可能未运行 gen_models)"
)
for name in sorted(model_names - referenced):
warnings.append(
f"[{worker_type}] 模型 {name} 未被任何 TOML 路由引用"
f"(孤儿模型,可重新运行 gen_models 清理)"
)
return hard_errors, warnings
def validate_contracts(plugins: List[Dict[str, Any]]) -> None: def validate_contracts(plugins: List[Dict[str, Any]]) -> None:
""" """
对每个发现的插件执行三方一致性校验(网关 inline 模式,启动时)。 对每个发现的 inline 插件执行三方一致性校验。
挂载于 lifespan 的 load_workers 与 register_routes 之间,此时插件模块已导入
(@rpc_method 注册表就绪)且 TOML 路由尚未注册到 FastAPI。
Args: Args:
plugins: _discover_plugins() 返回的插件配置列表 plugins: _discover_plugins() 返回的 inline 插件配置列表
Raises: Raises:
RuntimeError: 存在硬失败项(HTTP 路由缺失 @rpc_method 注册)时 RuntimeError: 存在硬失败项(HTTP 路由缺失 @rpc_method 注册)时
@@ -176,8 +41,8 @@ def validate_contracts(plugins: List[Dict[str, Any]]) -> None:
toml_routes = load_worker_routes_from_dir(worker_type) toml_routes = load_worker_routes_from_dir(worker_type)
registered = registry.get(worker_type, {}) registered = registry.get(worker_type, {})
model_names = _load_plugin_models(worker_type) model_names = load_plugin_models(worker_type)
hard, warns = _check_plugin(worker_type, toml_routes, registered, model_names) hard, warns = check_plugin(worker_type, toml_routes, registered, model_names)
hard_errors.extend(hard) hard_errors.extend(hard)
warnings.extend(warns) warnings.extend(warns)
@@ -197,45 +62,3 @@ def validate_contracts(plugins: List[Dict[str, Any]]) -> None:
) )
logger.info("契约校验通过(TOML 路由 ↔ @rpc_method 注册表一致)") logger.info("契约校验通过(TOML 路由 ↔ @rpc_method 注册表一致)")
def validate_worker_contracts(worker_type: str) -> List[Dict[str, Any]]:
"""
Worker 进程侧契约校验(process 模式):校验单个插件并返回可发布的路由描述符。
调用时机:worker 进程 import 插件模块后(@rpc_method 注册表就绪)、
发布路由描述符之前。校验逻辑与网关 validate_contracts 共用 _check_plugin。
Args:
worker_type: 插件类型(与 config/workers/<type> 目录一致)
Returns:
通过校验的 TOML 路由描述符列表(直接用于 RoutePublisher 发布)
Raises:
RuntimeError: 存在硬失败项(HTTP 路由缺失 @rpc_method 注册)时,
调用方应终止 worker 进程,避免发布残缺路由。
"""
registered = get_all_rpc_methods().get(worker_type, {})
toml_routes = load_worker_routes_from_dir(worker_type)
model_names = _load_plugin_models(worker_type)
hard_errors, warnings = _check_plugin(
worker_type, toml_routes, registered, model_names
)
for w in warnings:
logger.warning(" ⚠ %s", w)
if hard_errors:
logger.error("Worker 契约校验失败,发现 %d 个硬性错误:", len(hard_errors))
for e in hard_errors:
logger.error(" ✗ %s", e)
raise RuntimeError(
f"[{worker_type}] 契约校验失败: " + "; ".join(hard_errors)
)
logger.info(
"[%s] 契约校验通过,可发布 %d 个路由描述符", worker_type, len(toml_routes)
)
return toml_routes
+2 -88
View File
@@ -11,6 +11,8 @@ from inspect import Parameter, Signature
from fastapi import FastAPI, HTTPException, APIRouter, Query, Path as FastAPIPath, Request, Depends from fastapi import FastAPI, HTTPException, APIRouter, Query, Path as FastAPIPath, Request, Depends
from pydantic import create_model, Field, BaseModel from pydantic import create_model, Field, BaseModel
from chongming_worker.route_config import WORKER_CONFIG_DIR, load_worker_routes_from_dir
from .worker_manager import WorkerManager from .worker_manager import WorkerManager
from .logger import get_logger from .logger import get_logger
from .auth import store_user_to_request from .auth import store_user_to_request
@@ -110,94 +112,6 @@ class RouteConfig:
] ]
# 默认的 worker 配置目录
WORKER_CONFIG_DIR = "config/workers"
def load_worker_routes_from_dir(worker_type: str) -> List[Dict[str, Any]]:
"""
从 config/workers/{worker_type}/ 目录加载路由配置。
目录结构:
config/workers/{worker_type}/
config.toml # [worker] 分组信息 + registration + defaults
add.toml # 接口名 = 文件名(不含扩展名)
[GET]
summary = ...
params = [...]
response_model = { ... }
[POST]
... # 同一个接口支持多个请求方法
[MQTT]
... # MQTT 专用端点(不注册 HTTP 路由)
返回: List[Dict], 每个 dict 包含 flatten 后的路由配置,
每个方法节(如 GET/POST/MQTT) 生成一个独立的路由条目。
"""
from pathlib import Path
import tomllib
base_dir = Path(WORKER_CONFIG_DIR) / worker_type
if not base_dir.is_dir():
logger.warning(f"Worker 配置目录不存在: {base_dir}")
return []
# 1. 读取分组配置
group_cfg_path = base_dir / "config.toml"
group_defaults: Dict[str, Any] = {}
registration: Dict[str, Any] = {}
router_prefix: str = ""
if group_cfg_path.exists():
with open(group_cfg_path, "rb") as f:
cfg = tomllib.load(f)
w = cfg.get("worker", {})
group_defaults = w.get("defaults", {})
registration = w.get("registration", {})
router_prefix = registration.get("router_prefix", "")
# 2. 遍历所有接口 toml 文件
routes: List[Dict[str, Any]] = []
for route_file in sorted(base_dir.glob("*.toml")):
if route_file.name == "config.toml":
continue
# 文件名(不含 .toml)作为路径名
route_name = route_file.stem # 例如 "add"
base_path = router_prefix + "/" + route_name # 例如 "/calc/add"
with open(route_file, "rb") as f:
route_data = tomllib.load(f)
# 每个方法节(GET/POST/PUT/DELETE)生成一个独立路由
for method, spec in route_data.items():
if not isinstance(spec, dict):
logger.warning(f"忽略非 dict 的方法节: {route_file.name}[{method}]")
continue
# 合并: spec 显式字段优先,然后 group_defaults 兜底
route = dict(spec)
for key, default_val in group_defaults.items():
if key not in route:
route[key] = default_val
# 覆盖关键字段 - method 始终从 TOML 节键获取,不被 defaults 覆盖
route["method"] = method.upper()
route["path"] = route.get("path", base_path)
if "subject" not in route:
# 自动生成 subject: worker_type.route_name (如 "calc.add")
route["subject"] = f"{worker_type}.{route_name}"
# 记录来源文件名(契约校验用其推断 gen_models 模型命名)
route["route_name"] = route_name
routes.append(route)
logger.debug(
f" 已加载: {route['method']} {route['path']} -> {route['subject']}"
)
logger.info(f"从 {base_dir} 加载了 {len(routes)} 个路由")
return routes
def register_route_group( def register_route_group(
app: FastAPI, app: FastAPI,
worker_manager: WorkerManager, worker_manager: WorkerManager,
-90
View File
@@ -1,90 +0,0 @@
"""
路由描述符发布器(worker 进程侧)
独立 worker 进程在完成契约校验后,将路由描述符 put 到 Zenoh 的
`chongming/routes/<type>/...` 命名空间;进程优雅退出时 delete 摘除。
描述符 schema_version=1,字段与网关 RouteConfig 完全对位(TOML 兼容 dict),
网关 RouteRegistry 订阅同名空间并动态挂载 HTTP 路由。
"""
import json
from typing import Any, Dict, List, Optional
import zenoh
from .logger import get_logger
logger = get_logger("route_publisher")
ROUTES_PREFIX = "chongming/routes"
META_KEY = "__meta__"
SCHEMA_VERSION = 1
def route_id(subject: str, method: str) -> str:
"""route_id = subject 与 method 的编码(避免 keyexpr 特殊字符歧义)"""
return f"{subject.replace('.', '_')}__{method.upper()}"
class RoutePublisher:
"""发布/摘除某 worker_type 的路由描述符。"""
def __init__(self, session: zenoh.Session, worker_type: str):
self._session = session
self._worker_type = worker_type
self._keys: List[str] = []
def publish_routes(
self,
routes: List[Dict[str, Any]],
mqtt_topics: Optional[List[Dict[str, Any]]] = None,
):
"""
发布路由描述符。
Args:
routes: 契约校验通过的路由描述符列表(TOML 兼容格式)
mqtt_topics: 插件声明的 [[workers.mqtt_topics]](可空)
"""
for r in routes:
subject = r.get("subject", "")
method = r.get("method", "GET")
if not subject:
logger.warning("跳过无 subject 的路由描述符: %s", r)
continue
descriptor = dict(r)
descriptor["schema_version"] = SCHEMA_VERSION
descriptor["type"] = self._worker_type
key = f"{ROUTES_PREFIX}/{self._worker_type}/{route_id(subject, method)}"
self._session.put(key, json.dumps(descriptor, ensure_ascii=False).encode("utf-8"))
self._keys.append(key)
logger.info(
"已发布路由: %s %s (subject=%s)",
method.upper(), descriptor.get("path", "?"), subject,
)
if mqtt_topics:
meta_key = f"{ROUTES_PREFIX}/{self._worker_type}/{META_KEY}"
meta = {"schema_version": SCHEMA_VERSION, "mqtt_topics": mqtt_topics}
self._session.put(
meta_key, json.dumps(meta, ensure_ascii=False).encode("utf-8")
)
self._keys.append(meta_key)
logger.info("已发布插件元信息: %d 个自定义 MQTT 主题", len(mqtt_topics))
# 去重(周期性重发布会重复调用本方法)
self._keys = list(dict.fromkeys(self._keys))
logger.info(
"[%s] 路由发布完成: %d 端点 + %s", self._worker_type,
len(self._keys), "元信息" if mqtt_topics else "无元信息",
)
def unpublish(self):
"""摘除全部已发布的路由描述符(优雅退出时调用)。"""
for key in self._keys:
try:
self._session.delete(key)
except Exception as e: # pragma: no cover
logger.warning("摘除路由失败 [%s]: %s", key, e)
logger.info("[%s] 已摘除 %d 个路由描述符", self._worker_type, len(self._keys))
self._keys.clear()
+2 -3
View File
@@ -24,14 +24,13 @@ from typing import Any, Dict, List, Optional
import zenoh import zenoh
from chongming_worker.routes_publisher import META_KEY, ROUTES_PREFIX, SCHEMA_VERSION
from .logger import get_logger from .logger import get_logger
from .route_generator import register_route_group, _inject_schemas_to_openapi from .route_generator import register_route_group, _inject_schemas_to_openapi
logger = get_logger("route_registry") logger = get_logger("route_registry")
ROUTES_PREFIX = "chongming/routes"
META_KEY = "__meta__"
SCHEMA_VERSION = 1
# worker 重启风暴防抖窗口(秒) # worker 重启风暴防抖窗口(秒)
DEBOUNCE_SECONDS = 2.0 DEBOUNCE_SECONDS = 2.0
+1 -1
View File
@@ -17,8 +17,8 @@ import httpx
import zenoh import zenoh
from fastapi import FastAPI from fastapi import FastAPI
from chongming_worker.routes_publisher import RoutePublisher
from src.chongming.app.core.route_registry import RouteRegistry from src.chongming.app.core.route_registry import RouteRegistry
from src.chongming.app.core.route_publisher import RoutePublisher
failures = [] failures = []
+91
View File
@@ -0,0 +1,91 @@
"""
Worker 进程隔离性测试
验证进程插件与网关完全隔离:阻断 chongming(网关包)的两种导入路径后,
worker 侧全链路(SDK 导入 → 插件配置自读 → 插件 import → 契约校验)仍可用。
任何对 chongming.* / src.chongming.* 的导入都会直接触发 ImportError。
"""
import sys
from pathlib import Path
SRC = Path(__file__).parent
sys.path.insert(0, str(SRC))
# 阻断网关包导入(两种访问路径)。chongming_worker / zenoh / pydantic 等不受影响。
sys.modules["chongming"] = None # type: ignore[assignment]
sys.modules["src.chongming"] = None # type: ignore[assignment]
failures = []
def check(name, cond, detail=""):
status = "PASS" if cond else "FAIL"
print(f"[{status}] {name}" + (f" — {detail}" if detail else ""))
if not cond:
failures.append(name)
# ========== 1. worker SDK 导入(若依赖网关包将在此抛 ImportError) ==========
import chongming_worker.contracts as con_mod # noqa: E402
import chongming_worker.process as proc_mod # noqa: E402
import chongming_worker.route_config as rc_mod # noqa: E402
import chongming_worker.routes_publisher as pub_mod # noqa: E402
check("worker SDK 导入(阻断 chongming 后成功)", True)
# ========== 2. 插件配置自读(不依赖网关 _discover_plugins) ==========
wc = proc_mod.load_plugin_config("demo")
check(
"插件配置自读(type/load_mode/module)",
wc["type"] == "demo"
and wc["load_mode"] == "external"
and wc["module"] == "workers.demo",
f"type={wc['type']}, load_mode={wc['load_mode']}, module={wc['module']}",
)
# ========== 3. 完整 worker 启动前置链路:import 插件 + 契约校验 ==========
import workers.demo # noqa: E402,F401 触发 @rpc_method 注册
routes = con_mod.validate_worker_contracts("demo")
check(
"进程内契约校验通过(TOML ↔ 注册表 ↔ 模型)",
len(routes) == 1 and routes[0]["subject"] == "demo.hello",
f"routes={[r['subject'] for r in routes]}",
)
# ========== 4. 代码级双重保险:SDK 无对网关包的 import 语句(AST 精确检查) ==========
import ast # noqa: E402
import inspect # noqa: E402
def _imports_gateway(mod) -> list:
"""返回模块中引用 chongming(非 chongming_worker)的 import 节点列表"""
found = []
tree = ast.parse(inspect.getsource(mod))
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name == "chongming" or alias.name.startswith("chongming."):
found.append(alias.name)
elif isinstance(node, ast.ImportFrom):
if node.module and (
node.module == "chongming" or node.module.startswith("chongming.")
):
found.append(node.module)
return found
for mod in (proc_mod, con_mod, pub_mod, rc_mod):
bad = _imports_gateway(mod)
if bad:
check(f"{mod.__name__} import 了网关包: {bad}", False)
break
else:
check("worker SDK 无对网关包的 import 语句", True)
print("\n" + ("=" * 40))
if failures:
print(f"❌ {len(failures)} 项未通过: {failures}")
sys.exit(1)
print("✅ Worker 进程隔离性全部通过")
Generated
+2 -1
View File
@@ -319,9 +319,10 @@ source = { git = "http://124.71.68.6:3000/chongming/chongming-stream-manager.git
[[package]] [[package]]
name = "chongming-worker" name = "chongming-worker"
version = "0.1.0" version = "0.1.0"
source = { git = "http://124.71.68.6:3000/chongming/chongming-utils-worker.git#4908378e597eb200e255c71df92d16e272b4b12e" } source = { git = "http://124.71.68.6:3000/chongming/chongming-utils-worker.git#c0a5b45b72a606c2c69850403d435c4883b4ff78" }
dependencies = [ dependencies = [
{ name = "eclipse-zenoh" }, { name = "eclipse-zenoh" },
{ name = "pydantic" },
{ name = "tomli" }, { name = "tomli" },
] ]