modify: 分布式方案
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
内嵌 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()))
|
||||
Reference in New Issue
Block a user