Files
chongming-worker-rs/scripts/verify_mixed.py
T
2026-08-31 17:18:03 +08:00

189 lines
6.2 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.
"""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()