"""Python 与 Rust 进程插件同 type 混布对拍(多语言演进规划 §3.4 场景 5) 前置条件: 1. 本地 Zenoh Router 已运行(scripts/zenoh_router.py) 2. Rust 参考插件已编译;venv 已安装(worker_serve console script) 验证内容: 1. Python 与 Rust 对同一 TOML 配置发布的描述符内容 hash 一致 (网关据此幂等去重,内容不一致会告警冲突) 2. 同一 type 下两个实现副本均被 liveliness 发现(负载均衡透明) 3. 两个实现的 RPC 均返回等价结果(worker_id 区分副本) 4. SIGTERM 后各自摘除(不互相干扰) 用法(项目根目录): .venv/bin/python utils/worker-rs/scripts/verify_mixed.py """ import hashlib import orjson import os import signal import subprocess import sys import time import zenoh PASS = 0 FAIL = 0 ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) RUST_BIN = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "target", "debug", "examples", "demo") ) PY_WORKER = os.path.join(ROOT, ".venv", "bin", "worker_serve") def report(name, cond, detail=""): global PASS, FAIL if cond: PASS += 1 print(f"✅ {name}" + (f" — {detail}" if detail else "")) else: FAIL += 1 print(f"❌ {name}" + (f" — {detail}" if detail else "")) def start(cmd, worker_type): proc = subprocess.Popen( cmd + ["--type", worker_type, "--count", "1"], cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) deadline = time.monotonic() + 25 while time.monotonic() < deadline: if proc.poll() is not None: out = proc.stdout.read().decode() if proc.stdout else "" raise RuntimeError(f"Worker 启动失败 (退出码 {proc.returncode}):\n{out}") line = proc.stdout.readline() if proc.stdout else b"" if "等待退出信号" in line.decode("utf-8", "ignore"): return proc raise RuntimeError("Worker 25s 未就绪") def rpc(session, worker_expr, subject, payload, timeout=10.0): full_key = f"{worker_expr}/{subject}" replies = session.get( full_key, payload=orjson.dumps(payload), consolidation=zenoh.ConsolidationMode.NONE, timeout=timeout, ) return [orjson.loads(bytes(r.ok.payload)) for r in replies if r.ok] def main(): session = zenoh.open(zenoh.Config.from_file("config/zenoh_config.json5")) print("=== Python + Rust 同 type(demo) 混布对拍 ===") # 先建订阅(zenoh 不重放历史值) samples = {} def cb(sample): if not sample.payload: return # 空载荷样本(如 DELETE 预发布期),忽略 key = str(sample.key_expr) samples[key] = orjson.loads(bytes(sample.payload)) sub = session.declare_subscriber("chongming/routes/demo/**", cb) # 同时启动 Python 与 Rust 两个实现 py_proc = start([PY_WORKER], "demo") rs_proc = start([RUST_BIN], "demo") time.sleep(2.0) # 1. 描述符内容一致性:同一 route_id 多次发布(Python/Rust 各一次)内容 hash 必须一致 expect = { "demo_hello__GET", "demo_storage_upload_bytes__POST", "demo_storage_download_bytes__GET", "demo_storage_presign_upload__POST", "demo_storage_presign_download__GET", } seen = {k: [] for k in expect} for key, desc in samples.items(): rid = key.split("/")[-1] if rid in seen: seen[rid].append(orjson.dumps(desc, option=orjson.OPT_SORT_KEYS).decode()) all_same = True for rid, versions in seen.items(): if len(versions) < 1: continue hashes = {hashlib.sha1(v.encode("utf-8")).hexdigest()[:8] for v in versions} if len(hashes) > 1: all_same = False print(f" ⚠ {rid} 描述符内容不一致: {hashes}") report( "Python/Rust 描述符逐字段一致(内容 hash 唯一,无冲突告警)", all_same and all(seen.values()), f"各 route_id 版本数: { {k: len(v) for k, v in seen.items()} }", ) # 2. liveliness:同 type 两个实现副本均被发现 exprs = [ str(r.ok.key_expr) for r in session.liveliness().get("worker/demo/*") if r.ok ] report( "liveliness 发现 Python + Rust 副本(共 2 个)", len(exprs) == 2, f"实际: {exprs}", ) if len(exprs) < 2: print("混布副本不足,跳过 RPC 对拍") else: # 区分 Python 副本与 Rust 副本:Rust worker_id 格式同为 worker-0-, # 无法从 key 区分实现,但可分别对两个副本调用并验证都响应 for expr in exprs: replies = rpc( session, expr, "demo.hello", {"meta": {"method": "GET"}, "data": {"name": "mixed"}}, ) ok = ( len(replies) == 1 and replies[0].get("greeting") == "Hello, mixed!" and replies[0].get("worker_id") == expr.split("/")[-1] ) report(f"RPC 对拍 {expr}", ok, f"实际: {replies}") # storage 端点:两实现均应返回错误(Python 未配置 storage 降级;Rust 未实现降级) replies = rpc( session, exprs[0], "demo.storage.upload_bytes", {"meta": {"method": "POST"}, "data": {"object_key": "a.txt", "data_b64": "eA=="}}, ) report( "storage 端点两实现均降级为错误", len(replies) == 1 and replies[0].get("operation") == "error", f"实际: {replies}", ) # 3. SIGTERM 顺序退出:互不干扰 os.kill(rs_proc.pid, signal.SIGTERM) rs_proc.wait(timeout=10) time.sleep(1.5) remain = [ str(r.ok.key_expr) for r in session.liveliness().get("worker/demo/*") if r.ok ] report( "Rust 退出后 Python 副本不受影响", len(remain) == 1, f"剩余: {remain}", ) os.kill(py_proc.pid, signal.SIGTERM) py_proc.wait(timeout=10) sub.undeclare() session.close() print(f"\n=== 结果: {PASS} 通过, {FAIL} 失败 ===") sys.exit(1 if FAIL else 0) if __name__ == "__main__": main()