Files
chongming-worker-rs/scripts/verify_mixed.py
T

189 lines
6.2 KiB
Python
Raw Normal View History

2026-08-31 17:18:03 +08:00
"""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-<pid>,
# 无法从 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()