modify: rust 插件
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user