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