183 lines
6.5 KiB
Python
183 lines
6.5 KiB
Python
"""
|
||
真实 Zenoh 会话下的流式端到端测试(peer 模式,无需 router)
|
||
|
||
单进程内起两个 Zenoh 会话(listen + connect),跑通完整生产链路:
|
||
@rpc_method(stream=True) 注册 -> create_worker 声明 Queryable
|
||
-> WorkerManager.call_stream 消费多回复 -> 逐块到达
|
||
|
||
验证:
|
||
1. 流式方法:3+1 块按序到达(真实 Zenoh 多回复 + ConsolidationMode.NONE)
|
||
2. 流式中途异常:{"error": ...} 收尾块转发后终止
|
||
3. 非流式方法:仍是单回复(兼容性回归)
|
||
4. generate_route_configs 输出 stream 标记、非流式不含
|
||
5. decorators 对生成器函数的调用不消费生成器
|
||
|
||
运行: uv run python test_stream_e2e_zenoh.py
|
||
"""
|
||
import asyncio
|
||
import importlib.util
|
||
import logging
|
||
import sys
|
||
import types
|
||
from pathlib import Path
|
||
|
||
import orjson
|
||
import zenoh
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||
sys.path.insert(0, str(REPO_ROOT / "utils" / "worker" / "src"))
|
||
sys.path.insert(0, str(REPO_ROOT / "src"))
|
||
|
||
# ---- 网关依赖隔离(同 test_stream_gateway):桩掉 chongming 包链与 chongming_cache ----
|
||
_pkg = types.ModuleType("chongming")
|
||
_pkg.__path__ = []
|
||
_app = types.ModuleType("chongming.app")
|
||
_app.__path__ = []
|
||
_core = types.ModuleType("chongming.app.core")
|
||
_core.__path__ = []
|
||
_logger_stub = types.ModuleType("chongming.app.core.logger")
|
||
_logger_stub.get_logger = lambda name: logging.getLogger(name)
|
||
_cache_stub = types.ModuleType("chongming_cache")
|
||
_cache_stub.Cache = object
|
||
_cache_stub.DistributedLock = object # worker_manager 顶层导入(本测试不触达)
|
||
for mod in (_pkg, _app, _core, _logger_stub, _cache_stub):
|
||
sys.modules[mod.__name__] = mod
|
||
|
||
_wm_path = REPO_ROOT / "src" / "chongming" / "app" / "core" / "worker_manager.py"
|
||
_spec = importlib.util.spec_from_file_location(
|
||
"chongming.app.core.worker_manager", _wm_path
|
||
)
|
||
_wm_mod = importlib.util.module_from_spec(_spec)
|
||
sys.modules["chongming.app.core.worker_manager"] = _wm_mod
|
||
_spec.loader.exec_module(_wm_mod) # type: ignore[union-attr]
|
||
|
||
WorkerManager = _wm_mod.WorkerManager
|
||
StreamIdleTimeoutError = _wm_mod.StreamIdleTimeoutError
|
||
|
||
from chongming_worker import clear_registry, rpc_method # noqa: E402
|
||
from chongming_worker.decorators import generate_route_configs # noqa: E402
|
||
from chongming_worker.runtime import create_worker # noqa: E402
|
||
|
||
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)
|
||
|
||
|
||
import os
|
||
LISTEN_PORT = int(os.environ.get("CM_STREAM_TEST_PORT", "7447"))
|
||
LISTEN = f"tcp/127.0.0.1:{LISTEN_PORT}"
|
||
|
||
|
||
def open_session(listen=None):
|
||
"""peer 模式会话:worker 侧 listen,网关侧 connect(显式端点,禁用组播)"""
|
||
c = zenoh.Config()
|
||
c.insert_json5("scouting/multicast/enabled", "false")
|
||
if listen:
|
||
c.insert_json5("listen/endpoints", f'["{listen}"]')
|
||
else:
|
||
c.insert_json5("connect/endpoints", f'["{LISTEN}"]')
|
||
return zenoh.open(c)
|
||
|
||
|
||
async def run_checks():
|
||
clear_registry()
|
||
|
||
# ---- 流式端点(返回异步生成器)----
|
||
@rpc_method(subject="streamtest.echo", method="POST", stream=True)
|
||
async def echo_stream(prompt: str = "hi", tokens: int = 3):
|
||
for i in range(tokens):
|
||
await asyncio.sleep(0.01)
|
||
yield {"token": f"{prompt}-{i}"}
|
||
yield {"done": True}
|
||
|
||
# ---- 流式端点:中途异常(框架应发 {"error": ...} 收尾块)----
|
||
@rpc_method(subject="streamtest.fail", method="POST", stream=True)
|
||
async def fail_stream():
|
||
yield {"token": "a"}
|
||
raise ValueError("mid-boom")
|
||
|
||
# ---- 非流式端点(兼容性对照)----
|
||
@rpc_method(subject="streamtest.add", method="GET")
|
||
async def add(a: float, b: float):
|
||
return a + b
|
||
|
||
# 1. 配置生成:流式标记
|
||
configs = generate_route_configs("streamtest")
|
||
stream_cfg = next(c for c in configs if c["subject"] == "streamtest.echo")
|
||
check("generate_route_configs 输出 stream 标记", stream_cfg.get("stream") is True)
|
||
add_cfg = next(c for c in configs if c["subject"] == "streamtest.add")
|
||
check("非流式配置不含 stream 标记", "stream" not in add_cfg)
|
||
|
||
# 2. 真实 Zenoh 会话(peer 模式,无 router)
|
||
session_worker = open_session(listen=LISTEN)
|
||
session_gw = open_session()
|
||
|
||
# 3. 启动真实 Worker(create_worker 声明 Queryable)
|
||
worker_task = asyncio.create_task(
|
||
create_worker(session_worker, "streamtest", "w1")
|
||
)
|
||
await asyncio.sleep(0.5) # 等待 queryable 在 peer 间可见
|
||
|
||
try:
|
||
# 4. 网关 WorkerManager.call_stream 真实消费流式端点
|
||
wm = WorkerManager(session_gw)
|
||
wm.register_worker("streamtest", "w1")
|
||
|
||
async def _consume(subject, data, idle_timeout=2.0):
|
||
gen = await wm.call_stream(
|
||
"streamtest", subject,
|
||
{"meta": {"method": "POST"}, "data": data},
|
||
timeout=10.0, idle_timeout=idle_timeout,
|
||
)
|
||
return [orjson.loads(b) async for b in gen]
|
||
|
||
chunks = await _consume("streamtest.echo", {"prompt": "hi", "tokens": 3})
|
||
check(
|
||
"流式 3+1 块按序到达(真实多回复)",
|
||
chunks == [
|
||
{"token": "hi-0"}, {"token": "hi-1"}, {"token": "hi-2"}, {"done": True},
|
||
],
|
||
f"chunks={chunks}",
|
||
)
|
||
|
||
# 5. 流式中途异常:错误块收尾
|
||
chunks = await _consume("streamtest.fail", {})
|
||
check(
|
||
"流式中途异常 error 块收尾",
|
||
chunks == [{"token": "a"}, {"error": "mid-boom"}],
|
||
f"chunks={chunks}",
|
||
)
|
||
|
||
# 6. 非流式兼容:call() 仍单回复
|
||
result = await wm.call(
|
||
"streamtest", "streamtest.add",
|
||
{"meta": {"method": "GET"}, "data": {"a": 2, "b": 3}},
|
||
timeout=10.0,
|
||
)
|
||
check("非流式 call() 正常单回复", result == 5.0, f"result={result}")
|
||
|
||
# 7. 负载均衡路径兼容:liveliness 发现的 worker 也能流式(同 call)
|
||
worker_expr = await wm.get_worker("streamtest")
|
||
check("get_worker 仍可解析本地 worker", worker_expr is not None)
|
||
|
||
finally:
|
||
worker_task.cancel()
|
||
await asyncio.gather(worker_task, return_exceptions=True)
|
||
session_gw.close()
|
||
session_worker.close()
|
||
|
||
|
||
def run():
|
||
asyncio.run(run_checks())
|
||
print(f"\n结果: {len(failures)} 个失败")
|
||
return 1 if failures else 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(run())
|