Files
chongming-core/test/test_stream_e2e_zenoh.py
T

183 lines
6.5 KiB
Python
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.
"""
真实 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 / "gateway" / "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 / "gateway" / "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())