""" 内嵌 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()))