mqttxx 2.0.3__py3-none-any.whl → 3.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mqttxx/__init__.py CHANGED
@@ -3,12 +3,13 @@
3
3
  提供:
4
4
  - MQTTClient: MQTT 连接管理(自动重连、订阅队列化、TLS/SSL 支持)
5
5
  - RPCManager: 双向对等 RPC 调用(超时控制、权限检查、并发限制)
6
+ - EventChannelManager: 高吞吐事件广播通道(单向、无返回值)
6
7
  - 配置对象: MQTTConfig, TLSConfig, AuthConfig, RPCConfig 等
7
- - 协议定义: RPCRequest, RPCResponse
8
+ - 协议定义: RPCRequest, RPCResponse, EventMessage
8
9
  - 异常系统: 统一错误码和异常层次
9
10
  """
10
11
 
11
- __version__ = "2.0.0"
12
+ __version__ = "3.2.0"
12
13
  __author__ = "MQTTX Team"
13
14
 
14
15
  # 核心客户端
@@ -16,7 +17,9 @@ from .client import MQTTClient
16
17
 
17
18
  # RPC 管理器
18
19
  from .rpc import RPCManager
19
- from .conventions import ConventionalRPCManager
20
+
21
+ # Event Channel 管理器
22
+ from .events import EventChannelManager, EventMessage
20
23
 
21
24
  # 配置对象
22
25
  from .config import (
@@ -31,7 +34,7 @@ from .config import (
31
34
  from .protocol import (
32
35
  RPCRequest,
33
36
  RPCResponse,
34
- parse_message,
37
+ # parse_message 是内部 API,不再导出
35
38
  )
36
39
 
37
40
  # 异常系统
@@ -56,7 +59,9 @@ __all__ = [
56
59
  "MQTTClient",
57
60
  # RPC 管理器
58
61
  "RPCManager",
59
- "ConventionalRPCManager", # 约定式 RPC(强约束系统)
62
+ # Event Channel 管理器
63
+ "EventChannelManager",
64
+ "EventMessage",
60
65
  # 配置对象
61
66
  "MQTTConfig",
62
67
  "TLSConfig",
@@ -66,7 +71,7 @@ __all__ = [
66
71
  # 协议定义
67
72
  "RPCRequest",
68
73
  "RPCResponse",
69
- "parse_message",
74
+ # parse_message 已移除 - 内部 API
70
75
  # 异常系统
71
76
  "ErrorCode",
72
77
  "MQTTXError",
mqttxx/client.py CHANGED
@@ -1,15 +1,17 @@
1
1
  # aiomqtt 高级封装 - 基于纯 async/await 架构
2
2
 
3
3
  import asyncio
4
- import json
4
+ import os
5
5
  import ssl
6
- from typing import Callable, Optional
6
+ from typing import Callable, Optional, TYPE_CHECKING, Any
7
7
  from loguru import logger
8
8
  import aiomqtt
9
+ from paho.mqtt.matcher import MQTTMatcher
9
10
 
10
11
  from .config import MQTTConfig
11
- from .exceptions import MessageError
12
- from .protocol import parse_message
12
+
13
+ if TYPE_CHECKING:
14
+ from .client import MQTTClient
13
15
 
14
16
 
15
17
  class MQTTClient:
@@ -19,6 +21,19 @@ class MQTTClient:
19
21
  - aiomqtt 基于 paho-mqtt 封装,成熟稳定
20
22
  - 不自动重连,需要手动实现重连循环(官方推荐模式)
21
23
  - 使用 `async for message in client.messages` 异步迭代器
24
+
25
+
26
+ 并发安全:
27
+ - ✅ 单 loop 内并发调用 subscribe/unsubscribe:安全
28
+
29
+ 示例:
30
+ # ✅ 正确:单 loop
31
+ async def main():
32
+ client = MQTTClient(config)
33
+ await client.connect()
34
+ client.subscribe("topic", handler1) # 安全
35
+ client.subscribe("topic", handler2) # 安全
36
+
22
37
  """
23
38
 
24
39
  def __init__(self, config: MQTTConfig):
@@ -33,11 +48,26 @@ class MQTTClient:
33
48
  self.config = config
34
49
  self._client: Optional[aiomqtt.Client] = None
35
50
  self._subscriptions: set[str] = set() # 订阅列表(用于重连恢复)
36
- self._message_handlers: dict[str, Callable] = {} # topic → handler 映射
51
+
52
+ # Raw 订阅(使用 MQTTMatcher 进行通配符匹配)
53
+ # _raw_matcher 是核心匹配引擎(用于 iter_match)
54
+ # _raw_handlers 是辅助追踪结构(因 MQTTMatcher 不支持 getitem/contains)
55
+ self._raw_matcher = MQTTMatcher()
56
+ self._raw_handlers: dict[str, list[Callable]] = {}
57
+
37
58
  self._running = False
38
- self._connected = False # 修复 P0-2:真实连接状态标志
59
+ self._connected = False # 真实连接状态标志
39
60
  self._reconnect_task: Optional[asyncio.Task] = None
40
61
  self._message_task: Optional[asyncio.Task] = None
62
+ self._tls_context: Optional[ssl.SSLContext] = None # TLS 上下文(复用)
63
+
64
+ # 消息处理队列和 Workers(
65
+ self._message_queue: asyncio.Queue = asyncio.Queue(
66
+ maxsize=config.message_queue_maxsize
67
+ )
68
+ self._workers: list[asyncio.Task] = []
69
+ # Worker 数量:默认 CPU核数×2(IO-bound 最优)
70
+ self._num_workers = config.num_workers or (os.cpu_count() or 1) * 2
41
71
 
42
72
  async def connect(self):
43
73
  """连接到 MQTT Broker
@@ -51,14 +81,56 @@ class MQTTClient:
51
81
 
52
82
  self._running = True
53
83
 
84
+ # 创建 TLS 上下文(只创建一次,可复用)
85
+ if self.config.tls.enabled:
86
+ self._tls_context = self._create_tls_context()
87
+
88
+ # 创建 aiomqtt.Client(只创建一次,在重连循环中复用)
89
+ self._client = aiomqtt.Client(
90
+ hostname=self.config.broker_host,
91
+ port=self.config.broker_port,
92
+ username=self.config.auth.username,
93
+ password=self.config.auth.password,
94
+ identifier=self.config.client_id or None,
95
+ clean_session=self.config.clean_session,
96
+ keepalive=self.config.keepalive,
97
+ tls_context=self._tls_context,
98
+ max_queued_outgoing_messages=self.config.max_queued_messages or None,
99
+ )
100
+
101
+ # 启动消息处理 workers
102
+ for i in range(self._num_workers):
103
+ worker = asyncio.create_task(self._worker(i), name=f"mqtt_worker_{i}")
104
+ self._workers.append(worker)
105
+
54
106
  self._reconnect_task = asyncio.create_task(
55
107
  self._reconnect_loop(), name="mqtt_reconnect"
56
108
  )
57
109
 
110
+ # 等待首次连接建立(最多 60 秒)
111
+ for _ in range(600): # 60 秒超时,每次检查 0.1 秒
112
+ if self._connected:
113
+ break
114
+ await asyncio.sleep(0.1)
115
+ else:
116
+ self._running = False # 清理状态
117
+ raise TimeoutError(
118
+ f"MQTT 连接超时(60秒):{self.config.broker_host}:{self.config.broker_port}"
119
+ )
120
+
58
121
  async def disconnect(self):
59
122
  """断开连接并清理资源"""
60
123
  self._running = False
61
- self._connected = False # 修复 P0-2:标记为未连接
124
+ self._connected = False # :标记为未连接
125
+
126
+ # 等待 workers 处理完队列
127
+ if self._workers:
128
+ # 等待所有 workers 完成(最多 5 秒)
129
+ _, pending = await asyncio.wait(self._workers, timeout=5.0)
130
+ # 取消未完成的 workers
131
+ for w in pending:
132
+ w.cancel()
133
+ self._workers.clear()
62
134
 
63
135
  # 取消后台任务
64
136
  if self._reconnect_task:
@@ -68,15 +140,18 @@ class MQTTClient:
68
140
  except asyncio.CancelledError:
69
141
  pass
70
142
 
71
- # 取消消息处理任务
143
+ # 取消消息处理任务(修复 P0-D)
144
+ # 这会导致 _reconnect_loop 中的 async with 块退出,aiomqtt 自动清理连接
72
145
  if self._message_task:
73
146
  self._message_task.cancel()
74
147
  try:
75
148
  await self._message_task
76
149
  except asyncio.CancelledError:
77
150
  pass
78
- if self._client:
79
- self._client = None
151
+
152
+ # 清理 Client 和 TLS 上下文
153
+ self._client = None
154
+ self._tls_context = None
80
155
 
81
156
  logger.info("MQTT 客户端已断开")
82
157
 
@@ -110,40 +185,21 @@ class MQTTClient:
110
185
  异常处理:
111
186
  - aiomqtt.MqttError:连接/协议错误,触发重连
112
187
  - asyncio.CancelledError:任务被取消,退出循环
188
+
189
+ 关键改进:
190
+ - Client 在 connect() 中创建(只创建一次)
191
+ - 循环内只使用 async with self._client: 来连接
192
+ - 符合 aiomqtt 官方推荐模式
113
193
  """
114
194
  attempt = 0
115
195
  interval = self.config.reconnect.interval
116
196
 
117
197
  while self._running:
118
198
  try:
119
- # 创建 TLS 上下文
120
- tls_context = (
121
- self._create_tls_context() if self.config.tls.enabled else None
122
- )
123
-
124
- # region 调用代码溯源(aiomqtt.Client)
125
- # aiomqtt.Client 是异步上下文管理器
126
- # 文档:https://aiomqtt.bo3hm.com/reconnection.html
127
- # 进入上下文时自动连接
128
- # endregion
129
- async with aiomqtt.Client(
130
- hostname=self.config.broker_host,
131
- port=self.config.broker_port,
132
- username=self.config.auth.username,
133
- password=self.config.auth.password,
134
- identifier=self.config.client_id
135
- or None, # 空字符串 → None = 自动生成
136
- clean_session=self.config.clean_session,
137
- keepalive=self.config.keepalive,
138
- tls_context=tls_context,
139
- max_queued_outgoing_messages=self.config.max_queued_messages
140
- or None,
141
- ) as client:
142
- self._client = client
199
+ # 使用已创建的 Client(在 connect() 中创建)
200
+ # async with 会连接,退出时会自动断开
201
+ async with self._client:
143
202
  self._connected = True # 修复 P0-2:标记为已连接
144
- logger.success(
145
- f"MQTT 连接成功 - {self.config.broker_host}:{self.config.broker_port}"
146
- )
147
203
 
148
204
  # 重置重连计数
149
205
  attempt = 0
@@ -162,7 +218,6 @@ class MQTTClient:
162
218
 
163
219
  except aiomqtt.MqttError as e:
164
220
  logger.error(f"MQTT 连接失败: {e}")
165
- self._client = None
166
221
  self._connected = False # 修复 P0-2:标记为未连接
167
222
 
168
223
  # 检查重连次数限制
@@ -190,14 +245,14 @@ class MQTTClient:
190
245
  await asyncio.sleep(interval)
191
246
 
192
247
  async def _message_loop(self):
193
- """消息处理循环(aiomqtt 核心模式)
248
+ """消息接收循环
194
249
 
195
- 使用 async for 迭代消息,替代 gmqtt 的 on_message 回调
250
+ 使用 async for 迭代消息,将消息放入队列由 workers 处理
196
251
 
197
- 关键特性:
198
- - 非阻塞:异步迭代器自动 yield 控制权
199
- - 顺序处理:每条消息按顺序处理
200
- - 并发处理:每条消息在独立 Task 中处理(不阻塞迭代器)
252
+ 关键改进:
253
+ - 接收和处理分离:避免高吞吐下任务爆炸
254
+ - 有界队列:maxsize 可配置(默认 100k),作为保险丝
255
+ - 队列满时阻塞:形成自然背压,CPU/延迟会变明显(扩容信号)
201
256
 
202
257
  异常处理:
203
258
  - asyncio.CancelledError:任务被取消,退出循环
@@ -208,19 +263,9 @@ class MQTTClient:
208
263
  return
209
264
 
210
265
  try:
211
- # region 调用代码溯源(aiomqtt.Client.messages)
212
- # aiomqtt.Client.messages 是一个异步迭代器
213
- # 内部调用:
214
- # paho.mqtt.client.Client._handle_on_message()
215
- # → 将消息放入队列
216
- # → aiomqtt 从队列中取出消息
217
- # GitHub:https://github.com/sbtinstruments/aiomqtt/blob/main/aiomqtt/client.py#L400
218
- # endregion
219
266
  async for message in self._client.messages:
220
- # 异步处理消息(不阻塞迭代器)
221
- asyncio.create_task(
222
- self._handle_message(message), name=f"handle_{message.topic}"
223
- )
267
+ # 将消息放入队列(阻塞等待,形成自然背压)
268
+ await self._message_queue.put(message)
224
269
  except asyncio.CancelledError:
225
270
  logger.info("消息处理任务已取消")
226
271
  raise
@@ -230,138 +275,163 @@ class MQTTClient:
230
275
  except Exception as e:
231
276
  logger.exception(f"消息循环异常: {e}")
232
277
 
233
- async def _handle_message(self, message: aiomqtt.Message):
234
- """处理单条消息
235
- Args:
236
- message: aiomqtt.Message 对象
278
+ async def _worker(self, worker_id: int):
279
+ """消息处理 Worker
237
280
 
238
- 消息处理流程:
239
- 1. 检查 payload 大小
240
- 2. 解码 UTF-8
241
- 3. 解析 JSON
242
- 4. 使用 protocol.parse_message() 验证和解析
243
- 5. 路由到对应处理器
244
- """
281
+ 从队列中取消息并处理,支持并发控制
245
282
 
246
- try:
247
- # 检查 payload 大小(防御 DoS)
248
- if len(message.payload) > self.config.max_payload_size:
249
- logger.warning(
250
- f"Payload 过大,已忽略 - "
251
- f"topic: {message.topic}, size: {len(message.payload)}"
252
- )
253
- return
283
+ Args:
284
+ worker_id: Worker ID(用于日志)
254
285
 
286
+ 设计要点:
287
+ - 并发上限可控(默认 16 个 worker)
288
+ - 异常隔离(worker 崩溃不影响其他 worker)
289
+ - 优雅退出(_running=False 时退出)
290
+ """
291
+ while self._running:
255
292
  try:
256
- text = message.payload.decode("utf-8")
257
- data = json.loads(text)
258
- except UnicodeDecodeError as e:
259
- logger.error(
260
- f"UTF-8 解码失败 - topic: {message.topic}, error: {e!r}, "
261
- f"preview: {message.payload[:100]}"
262
- )
263
- return
264
- except json.JSONDecodeError as e:
265
- logger.error(
266
- f"JSON 解析失败 - topic: {message.topic}, error: {e!r}, "
267
- f"preview: {text[:100]}"
268
- )
269
- return
293
+ # 从队列取消息(带超时,避免无法退出) asyncio.Queue 是原子安全的
294
+ message = await asyncio.wait_for(self._message_queue.get(), timeout=1.0)
295
+ # 处理消息
296
+ await self._handle_message(message)
297
+ except asyncio.TimeoutError:
298
+ # 队列为空,继续等待
299
+ continue
300
+ except asyncio.CancelledError:
301
+ # Worker {worker_id} 被取消
302
+ break
303
+ except Exception as e:
304
+ logger.exception(f"Worker {worker_id} 异常: {e}")
305
+ # 继续运行,不退出
270
306
 
271
- # 解析消息类型(使用 dataclass 替代 Box)
272
- msg = parse_message(data)
307
+ def subscribe(self, pattern: str, handler: Callable[[str, bytes], Any]):
308
+ """订阅原始消息 (bytes)
273
309
 
274
- # 路由到对应处理器
275
- topic_str = str(message.topic)
276
- handler = self._message_handlers.get(topic_str)
310
+ Args:
311
+ pattern: MQTT topic pattern (支持通配符 +/#)
312
+ handler: 回调函数
313
+ - 签名: async def handler(topic: str, payload: bytes)
314
+ - topic: 实际收到消息的 topic
315
+ - payload: 原始 bytes 数据 (未解码)
277
316
 
278
- if handler:
279
- # 调用处理器
280
- if asyncio.iscoroutinefunction(handler):
281
- await handler(topic_str, msg)
282
- else:
283
- handler(topic_str, msg)
284
- else:
285
- # 修复 P0-3:RPC 请求无处理器时,立即返回错误响应(防止调用方超时)
286
- from .protocol import RPCRequest, RPCResponse
317
+ 并发行为:
318
+ - 同一 pattern 的多个 handlers 按注册顺序**顺序调用**(非并发)
319
+ - 如需并发处理,请在 handler 内部使用 asyncio.create_task()
287
320
 
288
- if isinstance(msg, RPCRequest):
289
- logger.warning(
290
- f"收到 RPC 请求但无处理器 - topic: {topic_str}, method: {msg.method}"
291
- )
292
- # 立即发送错误响应
293
- error_response = RPCResponse(
294
- request_id=msg.request_id,
295
- error=f"No RPC handler registered for topic: {topic_str}",
296
- )
297
- await self.publish(
298
- msg.reply_to, json.dumps(error_response.to_dict()), qos=1
299
- )
300
- else:
301
- logger.debug(f"收到消息(无处理器)- topic: {topic_str}")
321
+ 注意:
322
+ - 订阅在重连时自动恢复
323
+ """
324
+ if pattern not in self._raw_handlers:
325
+ self._raw_handlers[pattern] = []
326
+ self._raw_matcher[pattern] = self._raw_handlers[pattern]
302
327
 
303
- except MessageError as e:
304
- logger.error(f"消息解析失败: {e}")
305
- except Exception as e:
306
- logger.exception(f"消息处理失败: {e}")
328
+ # 立即向 MQTT broker 订阅
329
+ if self._client:
330
+ # 使用 asyncio.create_task 避免阻塞
331
+ asyncio.create_task(self._client.subscribe(pattern))
307
332
 
308
- def subscribe(self, topic: str, handler: Optional[Callable] = None):
309
- """订阅主题
333
+ self._raw_handlers[pattern].append(handler)
334
+ self._subscriptions.add(pattern)
310
335
 
311
- 修复点:
312
- - ✅ P0-1: 未连接时不崩溃,队列化订阅
336
+ logger.debug(f"订阅已注册 - pattern: {pattern}")
313
337
 
314
- Args:
315
- topic: MQTT 主题(支持通配符 +/#)
316
- handler: 消息处理器
317
- 签名:async def handler(topic: str, message: RPCRequest | RPCResponse)
318
- 或:def handler(topic: str, message: RPCRequest | RPCResponse)
338
+ @property
339
+ def raw(self) -> aiomqtt.Client:
340
+ """暴露底层 aiomqtt.Client,用于高级用法
319
341
 
320
- 设计决策:
321
- - 记录订阅到 _subscriptions set(用于重连恢复)
322
- - 如果已连接,立即订阅
323
- - 如果未连接,等待连接后自动订阅
342
+ 使用场景:
343
+ await client.raw.publish(topic, payload, qos=1, retain=False)
324
344
 
325
- 示例:
326
- # 同步处理器
327
- def my_handler(topic, message):
328
- print(f"收到消息: {topic} - {message}")
345
+ Raises:
346
+ RuntimeError: 客户端未连接
347
+ """
348
+ if not self._client:
349
+ raise RuntimeError("Client not connected")
350
+ return self._client
329
351
 
330
- client.subscribe("test/topic", my_handler)
352
+ def unsubscribe(self, pattern: str, handler: Optional[Callable] = None):
353
+ """取消订阅
331
354
 
332
- # 异步处理器
333
- async def my_async_handler(topic, message):
334
- await process_message(message)
355
+ Args:
356
+ pattern: MQTT topic pattern
357
+ handler: 要移除的 handler(None = 移除所有)
335
358
 
336
- client.subscribe("test/topic", my_async_handler)
359
+ 注意:
360
+ - 当某个 pattern 的最后一个 handler 被移除时:
361
+ - 若当前已连接,会向 broker 发送 MQTT UNSUBSCRIBE
362
+ - 无论是否连接,都会清理本地 matcher/handlers
337
363
  """
338
- # 记录订阅(用于重连恢复)
339
- self._subscriptions.add(topic)
364
+ if pattern not in self._raw_handlers:
365
+ logger.debug(f"取消订阅失败:pattern 不存在 - {pattern}")
366
+ return
340
367
 
341
- # 注册处理器
342
- if handler:
343
- self._message_handlers[topic] = handler
368
+ should_broker_unsubscribe = False
344
369
 
345
- # 如果已连接,立即订阅
346
- if self._client:
347
- asyncio.create_task(self._do_subscribe(topic))
370
+ if handler is None:
371
+ del self._raw_handlers[pattern]
372
+ del self._raw_matcher[pattern]
373
+ self._subscriptions.discard(pattern)
374
+ logger.debug(f"取消订阅(全部)- pattern: {pattern}")
375
+ should_broker_unsubscribe = True
348
376
  else:
349
- logger.info(f"订阅已队列化(等待连接)- topic: {topic}")
377
+ handlers = self._raw_handlers[pattern]
378
+ if handler in handlers:
379
+ handlers.remove(handler)
380
+
381
+ if not handlers:
382
+ del self._raw_handlers[pattern]
383
+ del self._raw_matcher[pattern]
384
+ self._subscriptions.discard(pattern)
385
+ logger.debug(
386
+ f"取消订阅(全部,最后一个 handler)- pattern: {pattern}"
387
+ )
388
+ should_broker_unsubscribe = True
389
+ else:
390
+ logger.debug(
391
+ f"取消订阅(部分)- pattern: {pattern}, 剩余 {len(handlers)} 个 handler"
392
+ )
393
+ else:
394
+ logger.debug(f"取消订阅失败:handler 不存在 - pattern: {pattern}")
395
+
396
+ if should_broker_unsubscribe and self._client and self.is_connected:
397
+ asyncio.create_task(
398
+ self._client.unsubscribe(pattern),
399
+ name=f"mqtt_unsub_{pattern}",
400
+ )
401
+ logger.debug(f"已向 broker 发送 UNSUBSCRIBE - pattern: {pattern}")
402
+
403
+ async def _handle_message(self, message: aiomqtt.Message):
404
+ """处理单条消息(传输层,只处理 bytes)
350
405
 
351
- async def _do_subscribe(self, topic: str):
352
- """执行订阅(内部方法)
406
+ 核心改变:
407
+ 1. 不再解析 JSON/协议
408
+ 2. 将 payload (bytes) 分发给 raw handlers
409
+ 3. 不再区分 RPC/Event(让上层处理)
353
410
 
354
411
  Args:
355
- topic: MQTT 主题
412
+ message: aiomqtt.Message 对象
356
413
  """
357
- if not self._client:
414
+ topic_str = str(message.topic)
415
+ payload = message.payload # bytes 类型
416
+
417
+ # 检查 payload 大小(防御 DoS)
418
+ if len(payload) > self.config.max_payload_size:
419
+ logger.warning(
420
+ f"Payload 过大,已忽略 - topic: {topic_str}, size: {len(payload)}"
421
+ )
358
422
  return
359
423
 
360
- try:
361
- await self._client.subscribe(topic)
362
- logger.success(f"订阅成功 - topic: {topic}")
363
- except aiomqtt.MqttError as e:
364
- logger.error(f"订阅失败 - topic: {topic}, error: {e}")
424
+ # === 唯一职责:将 bytes 分发给所有匹配的 handlers ===
425
+ # MQTTMatcher.iter_match 返回值列表,不是 (pattern, handlers) 元组
426
+ for handlers in self._raw_matcher.iter_match(topic_str):
427
+ for handler in handlers:
428
+ try:
429
+ if asyncio.iscoroutinefunction(handler):
430
+ await handler(topic_str, payload) # 传递 bytes!
431
+ else:
432
+ handler(topic_str, payload)
433
+ except Exception as e:
434
+ logger.exception(f"Handler 异常 - topic: {topic_str}, error: {e}")
365
435
 
366
436
  async def _restore_subscriptions(self):
367
437
  """恢复所有订阅(重连后调用)
@@ -375,32 +445,18 @@ class MQTTClient:
375
445
  if not self._subscriptions:
376
446
  return
377
447
 
378
- logger.info(f"恢复 {len(self._subscriptions)} 个订阅...")
448
+ # 创建快照,避免遍历时被修改(修复 P0-C)
449
+ topics = list(self._subscriptions)
450
+ logger.info(f"恢复 {len(topics)} 个订阅...")
379
451
 
380
- for topic in self._subscriptions:
381
- await self._do_subscribe(topic)
452
+ for topic in topics:
453
+ try:
454
+ await self._client.subscribe(topic)
455
+ except aiomqtt.MqttError as e:
456
+ logger.error(f"恢复订阅失败 - topic: {topic}, error: {e}")
382
457
 
383
458
  logger.success("订阅恢复完成")
384
459
 
385
- async def publish(self, topic: str, payload: str, qos: int = 0):
386
- """发布消息
387
- Args:
388
- topic: 目标主题
389
- payload: 消息载荷(字符串)
390
- qos: QoS 等级(0/1/2)
391
-
392
- 异常:
393
- aiomqtt.MqttError: 发布失败
394
- """
395
- if not self._client:
396
- logger.warning(f"发布失败:客户端未连接 - topic: {topic}")
397
- return
398
-
399
- try:
400
- await self._client.publish(topic, payload, qos=qos)
401
- except aiomqtt.MqttError as e:
402
- logger.error(f"发布失败 - topic: {topic}, error: {e}")
403
-
404
460
  def _create_tls_context(self) -> ssl.SSLContext:
405
461
  """创建 TLS 上下文
406
462
 
mqttxx/config.py CHANGED
@@ -119,6 +119,16 @@ class MQTTConfig:
119
119
  reconnect: 重连配置
120
120
  max_queued_messages: 最大排队消息数(0=无限)
121
121
  max_payload_size: 最大消息载荷大小(字节,防止 DoS 攻击)
122
+ message_queue_maxsize: 消息队列大小限制(默认 100,000)
123
+ - 语义:"几乎无限",仅作保险丝防止 OOM
124
+ - 队列满时行为:阻塞等待(背压信号)
125
+ - 触发背压时:CPU/延迟升高 → 扩容信号
126
+ - Python 字面量分隔符:100_000 = 100000(下划线仅用于可读性)
127
+ num_workers: Worker 数量(默认 None = CPU核数×2)
128
+ - None(默认):CPU核数 × 2(适合 IO-bound 负载)
129
+ - 自定义值:根据 handler 类型调整
130
+ - CPU-bound handler:设为 CPU核数
131
+ - IO-bound handler:设为 CPU核数 × 2~4
122
132
  log_level: 日志级别(DEBUG|INFO|WARNING|ERROR)
123
133
 
124
134
  示例:
@@ -165,6 +175,10 @@ class MQTTConfig:
165
175
  max_queued_messages: int = 0 # 0 = 无限
166
176
  max_payload_size: int = 1024 * 1024 # 1MB
167
177
 
178
+ # 消息处理(修复 P0-A)
179
+ message_queue_maxsize: int = 100_000 # 10W 条 队列最大容量(保险丝)
180
+ num_workers: Optional[int] = None # Worker 数量(None = CPU核数×2)
181
+
168
182
  # 日志级别
169
183
  log_level: str = "INFO"
170
184