Files
chongming-core/test/test_mqtt_broker_lock.py
T
2026-08-27 17:06:03 +08:00

158 lines
6.3 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.
"""
内嵌 MQTT Broker 宿主级分布式锁 + 孤儿接管测试
验证 setup_mqtt_broker 用 chongming-cache DistruibutedLock 取代文件锁后的语义:
1. 外部 Broker(use_embedded_broker=false)→ 跳过锁,直接返回
2. 未配置共享 cache → 无法取锁,跳过(并给出警告)
3. 非持有者:锁被他人持有 → 等待期间端口已就绪即复用,不启动子进程
4. 持有者 & 端口空闲 → 启动子进程并登记 PID
5. 持有者 & 端口被占 & 无孤儿登记 → 复用(不托管、不杀进程)
6. 持有者 & 端口被占 & 孤儿 PID 存活 → 终止孤儿、接管重启、登记新 PID
7. 关停 → 终止子进程、释放锁、清理登记
通过 monkeypatch is_port_in_use / _pid_alive / _terminate_pid / _wait_port_free 隔离
真实端口与进程;子进程目标替换为休眠桩,验证编排逻辑而非真实 amqtt 收发。
运行: uv run python test/test_mqtt_broker_lock.py
"""
import asyncio
import sys
import tempfile
import time
from pathlib import Path
from types import SimpleNamespace
_HERE = Path(__file__).resolve().parent # chongming-zenoh/
_PROJ = _HERE
for _p in (_PROJ / "src", _PROJ / "utils" / "worker" / "src"):
sys.path.insert(0, str(_p))
import chongming.app.core.lifespan_setup as ls
from chongming_cache import Cache, DistributedLock
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)
def _dummy_broker(*_a, **_k):
"""休眠桩子进程,替代真实 amqtt Broker(验证编排,不做 MQTT 收发)。"""
time.sleep(60)
def make_manager(cache):
return SimpleNamespace(cache=cache)
def make_app():
return SimpleNamespace(state=SimpleNamespace(
mqtt_broker_process=None, mqtt_broker_lock=None,
mqtt_broker_lock_acquired=False,
))
def _port_script(*seq):
"""一次性的 is_port_in_use 脚本(耗尽后一律 True,防止等待循环无限跑)。"""
it = iter(seq)
def fake(port, host="127.0.0.1"):
try:
return next(it)
except StopIteration:
return True
return fake
async def run_setup(cache, conf, port_script, patch=()):
"""用给定配置跑一遍 setup_mqtt_broker,返回 app。"""
ls.env = "testenv"
ls.config = {"testenv": {"mqtt": conf}}
ls._run_mqtt_broker = _dummy_broker
ls.is_port_in_use = port_script
ls._pid_alive = lambda pid: True
ls._terminate_pid = lambda pid: None
ls._wait_port_free = lambda *a, **k: None
for name, val in patch:
setattr(ls, name, val)
app = make_app()
mgr = make_manager(cache)
await ls.setup_mqtt_broker(app, mgr)
return app
async def main():
c = Cache(backend="diskcache", cache_store_path=tempfile.mkdtemp())
base = {"enabled": True, "host": "127.0.0.1", "port": 18883,
"use_embedded_broker": True}
# ── 1. 外部 Broker:跳过 ────────────────────────────────
ext = dict(base, use_embedded_broker=False)
app = await run_setup(c, ext, _port_script(False))
check("1.1 外部 Broker 跳过(不启动)", app.state.mqtt_broker_process is None
and app.state.mqtt_broker_lock is None)
# ── 2. 未配 cache:跳过 ─────────────────────────────────
app = await run_setup(None, base, _port_script(False))
check("2.1 未配 cache 跳过(不取锁)", app.state.mqtt_broker_lock is None
and app.state.mqtt_broker_process is None)
# ── 3. 非持有者:锁被他人持有 → 已监听则复用 ────────────
actor_lock = DistributedLock(c, ls._broker_lock_key()) # 先被别人抢到
actor_lock.acquire()
app = await run_setup(c, base, _port_script(True)) # 端口已被"占用"
check("3.1 非持有者复用(不取锁、不启动)",
app.state.mqtt_broker_lock is None
and app.state.mqtt_broker_process is None)
await actor_lock.stop()
actor_lock.release()
# ── 4. 持有者 & 端口空闲 → 启动并登记 PID ──────────────
app = await run_setup(c, base, _port_script(False, True))
check("4.1 持锁启动子进程", app.state.mqtt_broker_process is not None)
check("4.2 已登记子进程 PID",
c.get(ls._broker_owner_key()) == str(app.state.mqtt_broker_process.pid))
await ls.shutdown_mqtt_broker(app)
check("4.3 关停后释放锁", app.state.mqtt_broker_lock is None)
check("4.4 关停后清理登记", c.get(ls._broker_owner_key()) is None)
# ── 5. 持有者 & 端口被占 & 无孤儿登记 → 复用 ───────────
app = await run_setup(c, base, _port_script(True))
check("5.1 端口被占无孤儿:复用(不托管不杀)",
app.state.mqtt_broker_process is None
and app.state.mqtt_broker_lock is not None)
await ls.shutdown_mqtt_broker(app)
check("5.2 复用路径关停释放锁", app.state.mqtt_broker_lock is None)
# ── 6. 持有者 & 端口被占 & 孤儿 PID 存活 → 终止并接管 ──
# 端口脚本:入口=占用(True) → 终止后=空闲(False) → 新进程监听(True)
c.set(ls._broker_owner_key(), "99999") # 模拟前任登记的孤儿 PID
terminated = []
app = await run_setup(
c, base, _port_script(True, False, True),
patch=[("_pid_alive", lambda pid: pid == 99999),
("_terminate_pid", lambda pid: terminated.append(pid))],
)
check("6.1 检出并终止孤儿 PID 99999", 99999 in terminated, detail=f"{terminated}")
check("6.2 接管后启动并登记新 PID",
app.state.mqtt_broker_process is not None
and c.get(ls._broker_owner_key()) == str(app.state.mqtt_broker_process.pid))
await ls.shutdown_mqtt_broker(app)
check("6.3 接管后关停释放并清理", app.state.mqtt_broker_lock is None
and c.get(ls._broker_owner_key()) is None)
c.close()
print()
if failures:
print(f"FAILED {len(failures)}: {failures}")
return 1
print("ALL PASS")
return 0
if __name__ == "__main__":
sys.exit(asyncio.run(main()))