modify: ruff 清理 + 单例锁持有者释放 + 移除死代码
This commit is contained in:
@@ -413,8 +413,13 @@ def setup_mqtt_broker(app: FastAPI):
|
|||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
raise RuntimeError("MQTT Broker 端口未就绪(已被其他 worker 锁定)")
|
raise RuntimeError("MQTT Broker 端口未就绪(已被其他 worker 锁定)")
|
||||||
|
|
||||||
|
# 记录本进程持有锁,关闭时由持有者释放(避免误删其他 worker 的锁)
|
||||||
|
app.state.mqtt_broker_lock_acquired = True
|
||||||
|
|
||||||
if is_port_in_use(port, host):
|
if is_port_in_use(port, host):
|
||||||
logger.info(f"MQTT Broker 已在 {host}:{port} 运行,跳过")
|
logger.info(f"MQTT Broker 已在 {host}:{port} 运行,跳过")
|
||||||
|
_release_singleton_lock("mqtt_broker")
|
||||||
|
app.state.mqtt_broker_lock_acquired = False
|
||||||
return
|
return
|
||||||
proc = multiprocessing.Process(
|
proc = multiprocessing.Process(
|
||||||
target=_run_mqtt_broker, args=(host, port), daemon=False
|
target=_run_mqtt_broker, args=(host, port), daemon=False
|
||||||
@@ -433,7 +438,7 @@ def setup_mqtt_broker(app: FastAPI):
|
|||||||
|
|
||||||
|
|
||||||
async def shutdown_mqtt_broker(app: FastAPI):
|
async def shutdown_mqtt_broker(app: FastAPI):
|
||||||
"""终止 MQTT Broker 子进程"""
|
"""终止 MQTT Broker 子进程(仅锁持有者释放锁)"""
|
||||||
proc = getattr(app.state, "mqtt_broker_process", None)
|
proc = getattr(app.state, "mqtt_broker_process", None)
|
||||||
if proc and proc.is_alive():
|
if proc and proc.is_alive():
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
@@ -441,6 +446,8 @@ async def shutdown_mqtt_broker(app: FastAPI):
|
|||||||
if proc.is_alive():
|
if proc.is_alive():
|
||||||
proc.kill()
|
proc.kill()
|
||||||
logger.info("MQTT Broker 子进程已终止")
|
logger.info("MQTT Broker 子进程已终止")
|
||||||
|
if getattr(app.state, "mqtt_broker_lock_acquired", False):
|
||||||
|
_release_singleton_lock("mqtt_broker")
|
||||||
|
|
||||||
|
|
||||||
async def setup_mqtt(app: FastAPI, worker_manager: WorkerManager) -> MQTTBridge | None:
|
async def setup_mqtt(app: FastAPI, worker_manager: WorkerManager) -> MQTTBridge | None:
|
||||||
@@ -463,13 +470,14 @@ async def setup_mqtt(app: FastAPI, worker_manager: WorkerManager) -> MQTTBridge
|
|||||||
custom_topics = list(mqtt_config.get("custom_topics", []) or [])
|
custom_topics = list(mqtt_config.get("custom_topics", []) or [])
|
||||||
custom_topics += list(getattr(app.state, "plugin_mqtt_topics", []) or [])
|
custom_topics += list(getattr(app.state, "plugin_mqtt_topics", []) or [])
|
||||||
|
|
||||||
# 1. 启动 Broker 子进程(文件锁已确保只有一个)
|
# Broker 子进程已在 lifespan 第 3 步启动(setup_mqtt_broker + 文件锁单例)
|
||||||
# setup_mqtt_broker(app, host, port)
|
|
||||||
|
|
||||||
# 2. 原子锁确保只有一个 worker 启动 MQTT Bridge
|
# 原子锁确保只有一个 worker 启动 MQTT Bridge
|
||||||
if not _acquire_singleton_lock("mqtt_bridge"):
|
if not _acquire_singleton_lock("mqtt_bridge"):
|
||||||
logger.info("MQTT Bridge 已由其他 worker 启动,跳过")
|
logger.info("MQTT Bridge 已由其他 worker 启动,跳过")
|
||||||
return None
|
return None
|
||||||
|
# 记录本进程持有锁,关闭时由持有者释放(避免误删其他 worker 的锁)
|
||||||
|
app.state.mqtt_bridge_lock_acquired = True
|
||||||
|
|
||||||
bridge = MQTTBridge(
|
bridge = MQTTBridge(
|
||||||
worker_manager=worker_manager,
|
worker_manager=worker_manager,
|
||||||
@@ -484,10 +492,12 @@ async def setup_mqtt(app: FastAPI, worker_manager: WorkerManager) -> MQTTBridge
|
|||||||
|
|
||||||
|
|
||||||
async def shutdown_mqtt(app: FastAPI):
|
async def shutdown_mqtt(app: FastAPI):
|
||||||
"""停止 MQTT Bridge 和 Broker 子进程"""
|
"""停止 MQTT Bridge 和 Broker 子进程(仅锁持有者释放锁)"""
|
||||||
bridge: MQTTBridge | None = getattr(app.state, "mqtt_bridge", None)
|
bridge: MQTTBridge | None = getattr(app.state, "mqtt_bridge", None)
|
||||||
if bridge and bridge.is_running:
|
if bridge and bridge.is_running:
|
||||||
await bridge.stop()
|
await bridge.stop()
|
||||||
|
if getattr(app.state, "mqtt_bridge_lock_acquired", False):
|
||||||
|
_release_singleton_lock("mqtt_bridge")
|
||||||
await shutdown_mqtt_broker(app)
|
await shutdown_mqtt_broker(app)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
"""MQTT Bridge — 纯客户端模式,Broker 由子进程管理"""
|
"""MQTT Bridge — 纯客户端模式,Broker 由子进程管理"""
|
||||||
import asyncio, json, re
|
import asyncio
|
||||||
from typing import Any, Optional
|
import json
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from amqtt.client import MQTTClient
|
from amqtt.client import MQTTClient
|
||||||
from amqtt.mqtt.constants import QOS_1
|
from amqtt.mqtt.constants import QOS_1
|
||||||
from .logger import get_logger
|
from .logger import get_logger
|
||||||
|
|
||||||
logger = get_logger("mqtt")
|
logger = get_logger("mqtt")
|
||||||
MQTT_TOPIC_PREFIX = "chongming"
|
MQTT_TOPIC_PREFIX = "chongming"
|
||||||
MQTT_SUBSCRIBE_TOPIC = f"{MQTT_TOPIC_PREFIX}/+/+"
|
MQTT_SUBSCRIBE_TOPIC = f"{MQTT_TOPIC_PREFIX}/+/+"
|
||||||
@@ -34,7 +38,9 @@ def _compile_topic(topic: str) -> re.Pattern:
|
|||||||
|
|
||||||
class MQTTBridge:
|
class MQTTBridge:
|
||||||
def __init__(self, worker_manager, host="localhost", port=1883, custom_topics=None):
|
def __init__(self, worker_manager, host="localhost", port=1883, custom_topics=None):
|
||||||
self._wm = worker_manager; self._host = host; self._port = port
|
self._wm = worker_manager
|
||||||
|
self._host = host
|
||||||
|
self._port = port
|
||||||
self._client: Optional[MQTTClient] = None
|
self._client: Optional[MQTTClient] = None
|
||||||
self._lt: Optional[asyncio.Task] = None
|
self._lt: Optional[asyncio.Task] = None
|
||||||
self._running = False
|
self._running = False
|
||||||
@@ -46,15 +52,19 @@ class MQTTBridge:
|
|||||||
]
|
]
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
if self._running: return
|
if self._running:
|
||||||
try: await self._sc()
|
return
|
||||||
|
try:
|
||||||
|
await self._sc()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"MQTT 不可用: {e}"); return
|
logger.warning(f"MQTT 不可用: {e}")
|
||||||
|
return
|
||||||
self._running = True
|
self._running = True
|
||||||
logger.info(f"MQTT Bridge 已连接 {self._host}:{self._port}")
|
logger.info(f"MQTT Bridge 已连接 {self._host}:{self._port}")
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
if not self._running: return
|
if not self._running:
|
||||||
|
return
|
||||||
self._running = False
|
self._running = False
|
||||||
await self._sc_stop()
|
await self._sc_stop()
|
||||||
|
|
||||||
@@ -73,30 +83,38 @@ class MQTTBridge:
|
|||||||
if self._client:
|
if self._client:
|
||||||
try:
|
try:
|
||||||
await self._client.disconnect()
|
await self._client.disconnect()
|
||||||
except Exception: pass
|
except Exception:
|
||||||
|
pass
|
||||||
self._client = None
|
self._client = None
|
||||||
# 再取消消息循环
|
# 再取消消息循环
|
||||||
if self._lt and not self._lt.done():
|
if self._lt and not self._lt.done():
|
||||||
self._lt.cancel()
|
self._lt.cancel()
|
||||||
try: await self._lt
|
try:
|
||||||
except asyncio.CancelledError: pass
|
await self._lt
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
async def _loop(self):
|
async def _loop(self):
|
||||||
while self._running:
|
while self._running:
|
||||||
try:
|
try:
|
||||||
m = await self._client.deliver_message() # type: ignore
|
m = await self._client.deliver_message() # type: ignore
|
||||||
if m is None: break
|
if m is None:
|
||||||
|
break
|
||||||
await self._handle(m)
|
await self._handle(m)
|
||||||
except asyncio.CancelledError: break
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self._running:
|
if self._running:
|
||||||
logger.error(f"消息异常: {e}")
|
logger.error(f"消息异常: {e}")
|
||||||
break
|
break
|
||||||
|
|
||||||
async def _handle(self, m):
|
async def _handle(self, m):
|
||||||
t: str = m.topic; p = m.data # type: ignore
|
t: str = m.topic
|
||||||
try: pl = json.loads(p.decode("utf-8"))
|
p = m.data # type: ignore
|
||||||
except json.JSONDecodeError: return
|
try:
|
||||||
|
pl = json.loads(p.decode("utf-8"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return
|
||||||
|
|
||||||
# 信封协议(严格): {"meta": {...}, "data": {...}}
|
# 信封协议(严格): {"meta": {...}, "data": {...}}
|
||||||
# 非信封格式载荷直接丢弃并告警,请设备侧按新协议发布
|
# 非信封格式载荷直接丢弃并告警,请设备侧按新协议发布
|
||||||
@@ -120,7 +138,8 @@ class MQTTBridge:
|
|||||||
|
|
||||||
# 2. 内置接口 chongming/{worker}/{operation}
|
# 2. 内置接口 chongming/{worker}/{operation}
|
||||||
parts = t.split("/")
|
parts = t.split("/")
|
||||||
if len(parts) < 3: return
|
if len(parts) < 3:
|
||||||
|
return
|
||||||
wt, op = parts[1], parts[2]
|
wt, op = parts[1], parts[2]
|
||||||
subj = f"{wt}.{op}"
|
subj = f"{wt}.{op}"
|
||||||
rt = f"{MQTT_TOPIC_PREFIX}/{wt}/{op}/response"
|
rt = f"{MQTT_TOPIC_PREFIX}/{wt}/{op}/response"
|
||||||
@@ -128,7 +147,8 @@ class MQTTBridge:
|
|||||||
|
|
||||||
def _fill(self, template: str, matched: re.Match) -> str:
|
def _fill(self, template: str, matched: re.Match) -> str:
|
||||||
"""用匹配到的通配符值填充模板中的 {1} {2}... 占位符"""
|
"""用匹配到的通配符值填充模板中的 {1} {2}... 占位符"""
|
||||||
if "{" not in template: return template
|
if "{" not in template:
|
||||||
|
return template
|
||||||
for i, g in enumerate(matched.groups(), start=1):
|
for i, g in enumerate(matched.groups(), start=1):
|
||||||
template = template.replace("{" + str(i) + "}", g)
|
template = template.replace("{" + str(i) + "}", g)
|
||||||
return template
|
return template
|
||||||
@@ -153,15 +173,20 @@ class MQTTBridge:
|
|||||||
r = await self._wm.call(worker_type=wt, subject=subj, payload=pl,
|
r = await self._wm.call(worker_type=wt, subject=subj, payload=pl,
|
||||||
timeout=float(meta.get("timeout", 30)),
|
timeout=float(meta.get("timeout", 30)),
|
||||||
cacheable=bool(meta.get("cacheable", False)))
|
cacheable=bool(meta.get("cacheable", False)))
|
||||||
if r is not None: await self._pub(rt, r)
|
if r is not None:
|
||||||
except RuntimeError as e: await self._pub(rt, {"error":str(e)})
|
await self._pub(rt, r)
|
||||||
except Exception as e: await self._pub(rt, {"error":str(e)})
|
except RuntimeError as e:
|
||||||
|
await self._pub(rt, {"error": str(e)})
|
||||||
|
except Exception as e:
|
||||||
|
await self._pub(rt, {"error": str(e)})
|
||||||
|
|
||||||
async def _pub(self, topic, data):
|
async def _pub(self, topic, data):
|
||||||
try:
|
try:
|
||||||
pd = json.dumps(data, ensure_ascii=False, default=str)
|
pd = json.dumps(data, ensure_ascii=False, default=str)
|
||||||
await self._client.publish(topic, pd.encode("utf-8"), QOS_1) # type: ignore
|
await self._client.publish(topic, pd.encode("utf-8"), QOS_1) # type: ignore
|
||||||
except Exception as e: logger.error(f"发布失败 [{topic}]: {e}")
|
except Exception as e:
|
||||||
|
logger.error(f"发布失败 [{topic}]: {e}")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_running(self): return self._running
|
def is_running(self):
|
||||||
|
return self._running
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import asyncio
|
|||||||
import random
|
import random
|
||||||
import json
|
import json
|
||||||
import orjson
|
import orjson
|
||||||
import time
|
from typing import Any, Dict, List, Optional
|
||||||
from typing import Any, Dict, List, Optional, Set
|
|
||||||
import zenoh
|
import zenoh
|
||||||
from diskcache import Cache
|
from diskcache import Cache
|
||||||
from .logger import get_logger
|
from .logger import get_logger
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@ async def main():
|
|||||||
res = await sess.execute(select(Users))
|
res = await sess.execute(select(Users))
|
||||||
rows = res.scalars().all()
|
rows = res.scalars().all()
|
||||||
|
|
||||||
print(f"✅ 4 个并发启动全部成功")
|
print("✅ 4 个并发启动全部成功")
|
||||||
print(f"✅ users 表记录数: {len(rows)}(应为 1)")
|
print(f"✅ users 表记录数: {len(rows)}(应为 1)")
|
||||||
for u in rows:
|
for u in rows:
|
||||||
print(f" - {u.username} <{u.email}> superuser={u.is_superuser}")
|
print(f" - {u.username} <{u.email}> superuser={u.is_superuser}")
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ def check(name, cond, detail=""):
|
|||||||
|
|
||||||
|
|
||||||
# ========== 1. global_var 白名单 / require / __all__ ==========
|
# ========== 1. global_var 白名单 / require / __all__ ==========
|
||||||
from plugins.example import global_var as gv
|
from plugins.example import global_var as gv # noqa: E402
|
||||||
|
|
||||||
check("global_var.__all__ 包含 db", "db" in gv.__all__)
|
check("global_var.__all__ 包含 db", "db" in gv.__all__)
|
||||||
check("global_var.__all__ 包含 set_global_var", "set_global_var" in gv.__all__)
|
check("global_var.__all__ 包含 set_global_var", "set_global_var" in gv.__all__)
|
||||||
@@ -45,7 +45,7 @@ check("模块属性 db 已被更新", gv.db is marker)
|
|||||||
|
|
||||||
|
|
||||||
# ========== 2. __init__.py 导入链(router + Plugin 基类)==========
|
# ========== 2. __init__.py 导入链(router + Plugin 基类)==========
|
||||||
import plugins.example as plug
|
import plugins.example as plug # noqa: E402
|
||||||
|
|
||||||
check("plugins.example 导入成功", True)
|
check("plugins.example 导入成功", True)
|
||||||
check("create_workers 存在", callable(getattr(plug, "create_workers", None)))
|
check("create_workers 存在", callable(getattr(plug, "create_workers", None)))
|
||||||
@@ -54,7 +54,7 @@ check("plugin 实例存在", getattr(plug, "plugin", None) is not None)
|
|||||||
check("plugin._initialized 默认 False", plug.plugin._initialized is False)
|
check("plugin._initialized 默认 False", plug.plugin._initialized is False)
|
||||||
|
|
||||||
# ========== 3. create_workers 幂等性(mock session/config)==========
|
# ========== 3. create_workers 幂等性(mock session/config)==========
|
||||||
import asyncio
|
import asyncio # noqa: E402
|
||||||
|
|
||||||
# 用桩替代 Plugin 基类的 worker 创建实现,避免真实 zenoh 会话
|
# 用桩替代 Plugin 基类的 worker 创建实现,避免真实 zenoh 会话
|
||||||
calls = []
|
calls = []
|
||||||
|
|||||||
Reference in New Issue
Block a user