ErisPulse 2.7.1.dev2__tar.gz → 2.7.1.dev3__tar.gz
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.
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/PKG-INFO +1 -1
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/pyproject.toml +1 -1
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/base.py +4 -4
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/command.py +16 -8
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/wrapper.py +68 -5
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/constants.py +26 -3
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/en.py +2 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/ja.py +2 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/ru.py +2 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/zh_cn.py +2 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/zh_tw.py +2 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/frame_config.py +6 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/memory.py +13 -4
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/sdk.py +216 -36
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/.gitignore +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/LICENSE +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/README.pypi.md +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/base.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/cli.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/create.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/doctor.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/init.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/install.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/language.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/list.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/list_remote.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/run.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/self_update.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/types.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/uninstall.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/commands/upgrade.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/console.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/constants.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/hints.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/en.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/ja.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/ru.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/zh_cn.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/i18n/locales/zh_tw.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/registry.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/display.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/file_watcher.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/package_manager.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/CLI/utils/scaffold_text.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/adapter.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/client.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/config_schema.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/converter.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/errors.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/i18n_schema.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/kv_builder.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/manager.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/module.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/router.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/send_builder.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/send_rules.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/storage.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Bases/websocket.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/message.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/message_builder.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/meta.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/notice.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/request.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/Event/session_type.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/adapter.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/error.css +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/error.html +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/root.css +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/assets/root.html +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/client.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/config.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/constants.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/i18n/locales/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/lifecycle.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/logger.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/master.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/module.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/router.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/Core/storage.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/__main__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/adapter.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/bases/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/bases/finder.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/finders/module.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/adapter.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/bases/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/bases/loader.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/module.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/strategy.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/loaders/strict.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/py.typed +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/__init__.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/config_schema.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/context.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/diagnostics.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/exceptions.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/hints.py +0 -0
- {erispulse-2.7.1.dev2 → erispulse-2.7.1.dev3}/src/ErisPulse/runtime/tasks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ErisPulse
|
|
3
|
-
Version: 2.7.1.
|
|
3
|
+
Version: 2.7.1.dev3
|
|
4
4
|
Summary: Event-driven modular async bot framework — write once, deploy to any platform | 事件驱动的模块化异步机器人框架,一次编写,多平台部署
|
|
5
5
|
Project-URL: Homepage, https://github.com/ErisPulse/ErisPulse
|
|
6
6
|
Project-URL: Documentation, https://www.erisdev.com#docs
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ErisPulse"
|
|
7
|
-
version = "2.7.1-dev.
|
|
7
|
+
version = "2.7.1-dev.3"
|
|
8
8
|
description = "Event-driven modular async bot framework — write once, deploy to any platform | 事件驱动的模块化异步机器人框架,一次编写,多平台部署"
|
|
9
9
|
readme = "README.pypi.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -288,7 +288,7 @@ class BaseEventHandler:
|
|
|
288
288
|
"elapsed_ms": round(_elapsed_0 * 1000, 2),
|
|
289
289
|
"processed": event.is_processed(),
|
|
290
290
|
})
|
|
291
|
-
if event.
|
|
291
|
+
if event.is_stopped():
|
|
292
292
|
break
|
|
293
293
|
continue
|
|
294
294
|
|
|
@@ -325,8 +325,8 @@ class BaseEventHandler:
|
|
|
325
325
|
"handler": _h_name,
|
|
326
326
|
"owner": _h_owner,
|
|
327
327
|
})
|
|
328
|
-
|
|
329
|
-
|
|
328
|
+
# _processed / _propagation_stopped 已由上方字段合并循环传播,
|
|
329
|
+
# 此处不再调用 mark_processed()(其默认会触发 _propagation_stopped 副作用)
|
|
330
330
|
|
|
331
331
|
# 冲突告警:同一 field 被多个同优先级 handler 修改
|
|
332
332
|
for field, mods in _modified_tracker.items():
|
|
@@ -345,7 +345,7 @@ class BaseEventHandler:
|
|
|
345
345
|
)
|
|
346
346
|
)
|
|
347
347
|
|
|
348
|
-
if event.
|
|
348
|
+
if event.is_stopped():
|
|
349
349
|
break
|
|
350
350
|
|
|
351
351
|
# 输出事件链路追踪日志
|
|
@@ -14,7 +14,10 @@ ErisPulse 命令处理模块
|
|
|
14
14
|
import asyncio
|
|
15
15
|
import inspect
|
|
16
16
|
from collections.abc import Awaitable, Callable
|
|
17
|
-
from typing import Any
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .wrapper import Event
|
|
18
21
|
|
|
19
22
|
from ...runtime import get_event_config
|
|
20
23
|
from ...runtime.context import current_owner, handler_waits
|
|
@@ -375,6 +378,11 @@ class CommandHandler:
|
|
|
375
378
|
|
|
376
379
|
:param event: 消息事件数据
|
|
377
380
|
"""
|
|
381
|
+
# 防御性归一化:确保 event 为 Event 实例,使 mark_processed 等方法可用
|
|
382
|
+
from .wrapper import Event as _Event
|
|
383
|
+
|
|
384
|
+
if not isinstance(event, _Event):
|
|
385
|
+
event = _Event(event)
|
|
378
386
|
|
|
379
387
|
# 检查是否已经被其他处理器标记为已处理
|
|
380
388
|
if event.get("_processed"):
|
|
@@ -399,7 +407,7 @@ class CommandHandler:
|
|
|
399
407
|
)
|
|
400
408
|
return
|
|
401
409
|
|
|
402
|
-
async def _process_text_for_command(event:
|
|
410
|
+
async def _process_text_for_command(event: "Event", text: str) -> bool:
|
|
403
411
|
"""
|
|
404
412
|
处理文本内容,尝试匹配并执行命令
|
|
405
413
|
|
|
@@ -501,7 +509,7 @@ class CommandHandler:
|
|
|
501
509
|
return
|
|
502
510
|
|
|
503
511
|
async def _try_execute_command(
|
|
504
|
-
self, event:
|
|
512
|
+
self, event: "Event", original_text: str, check_text: str, prefix: str
|
|
505
513
|
) -> bool:
|
|
506
514
|
"""
|
|
507
515
|
尝试执行命令
|
|
@@ -611,8 +619,8 @@ class CommandHandler:
|
|
|
611
619
|
|
|
612
620
|
event["command"] = command_info
|
|
613
621
|
|
|
614
|
-
#
|
|
615
|
-
event
|
|
622
|
+
# 标记事件已被处理(认领 + 阻断,阻止低优先级处理器再介入)
|
|
623
|
+
event.mark_processed()
|
|
616
624
|
|
|
617
625
|
# 钩子: 命令匹配
|
|
618
626
|
from ..lifecycle import lifecycle
|
|
@@ -694,7 +702,7 @@ class CommandHandler:
|
|
|
694
702
|
)
|
|
695
703
|
return False
|
|
696
704
|
|
|
697
|
-
async def _check_pending_reply(self, event:
|
|
705
|
+
async def _check_pending_reply(self, event: "Event"):
|
|
698
706
|
"""
|
|
699
707
|
检查是否是等待回复的消息
|
|
700
708
|
|
|
@@ -745,8 +753,8 @@ class CommandHandler:
|
|
|
745
753
|
# 清理等待信息
|
|
746
754
|
del self._waiting_replies[wait_key]
|
|
747
755
|
|
|
748
|
-
#
|
|
749
|
-
event
|
|
756
|
+
# 标记事件已被处理(认领 + 阻断,阻止低优先级处理器再介入)
|
|
757
|
+
event.mark_processed()
|
|
750
758
|
|
|
751
759
|
async def _send_permission_denied(self, event: dict[str, Any]):
|
|
752
760
|
"""
|
|
@@ -1684,11 +1684,14 @@ class Event(dict):
|
|
|
1684
1684
|
|
|
1685
1685
|
def to_dict(self) -> dict[str, Any]:
|
|
1686
1686
|
"""
|
|
1687
|
-
|
|
1687
|
+
转换为字典(过滤内部键)
|
|
1688
|
+
|
|
1689
|
+
过滤以 ``_`` 开头的内部键(如 ``_processed``/``_propagation_stopped``),
|
|
1690
|
+
只返回事件数据。
|
|
1688
1691
|
|
|
1689
1692
|
:return: 事件数据字典
|
|
1690
1693
|
"""
|
|
1691
|
-
return
|
|
1694
|
+
return {k: v for k, v in self.items() if not k.startswith("_")}
|
|
1692
1695
|
|
|
1693
1696
|
def is_processed(self) -> bool:
|
|
1694
1697
|
"""
|
|
@@ -1698,11 +1701,71 @@ class Event(dict):
|
|
|
1698
1701
|
"""
|
|
1699
1702
|
return self.get("_processed", False)
|
|
1700
1703
|
|
|
1701
|
-
def mark_processed(self):
|
|
1704
|
+
def mark_processed(self, claim: bool = True, stop: bool = True):
|
|
1702
1705
|
"""
|
|
1703
|
-
|
|
1706
|
+
标记事件为已处理
|
|
1707
|
+
|
|
1708
|
+
认领与阻断是两个**正交**的语义,可独立控制:
|
|
1709
|
+
|
|
1710
|
+
- **认领(claim)**:标记事件已被本处理器处理(写入 ``_processed``)。
|
|
1711
|
+
命令分发器看到已认领的事件会跳过去重,避免同一消息被多个命令处理器
|
|
1712
|
+
重复处理。典型场景:命令匹配成功后认领,阻止命令分发器再介入。
|
|
1713
|
+
- **阻断(stop)**:阻止事件向更低优先级处理器传播(写入
|
|
1714
|
+
``_propagation_stopped``)。低优先级处理器(如 ``on_message``)将不再
|
|
1715
|
+
看到该事件。典型场景:高优先级处理器已完整处理事件,不希望低优先级再执行。
|
|
1716
|
+
|
|
1717
|
+
参数组合:
|
|
1718
|
+
- ``mark_processed()``:认领 + 阻断(默认)
|
|
1719
|
+
- ``mark_processed(stop=False)``:仅认领,不阻断 —— 事件继续流向低优先级观察者
|
|
1720
|
+
- ``mark_processed(claim=False)``:仅阻断,不认领 —— 低优先级不执行,但不做去重
|
|
1721
|
+
|
|
1722
|
+
:param claim: 是否认领事件(标记已处理,命令分发去重)(默认 True)
|
|
1723
|
+
:param stop: 是否阻断向低优先级处理器传播(默认 True)
|
|
1724
|
+
|
|
1725
|
+
:example:
|
|
1726
|
+
>>> event.mark_processed() # 认领 + 阻断(默认)
|
|
1727
|
+
>>> event.mark_processed(stop=False) # 仅认领,低优先级仍能看到
|
|
1728
|
+
>>> event.mark_processed(claim=False) # 仅阻断,不标记已处理
|
|
1729
|
+
"""
|
|
1730
|
+
if claim:
|
|
1731
|
+
self["_processed"] = True
|
|
1732
|
+
if stop:
|
|
1733
|
+
self["_propagation_stopped"] = True
|
|
1734
|
+
|
|
1735
|
+
def done(self, claim: bool = True, stop: bool = True):
|
|
1736
|
+
"""
|
|
1737
|
+
标记事件完成(:meth:`mark_processed` 的别名)
|
|
1738
|
+
|
|
1739
|
+
与 :meth:`mark_processed` 完全等价,提供更简洁的写法。
|
|
1740
|
+
|
|
1741
|
+
:param claim: 是否认领事件(标记已处理,命令分发去重)(默认 True)
|
|
1742
|
+
:param stop: 是否阻断向低优先级处理器传播(默认 True)
|
|
1743
|
+
|
|
1744
|
+
:example:
|
|
1745
|
+
>>> @command("help")
|
|
1746
|
+
... async def help_cmd(event):
|
|
1747
|
+
... event.done() # 认领 + 阻断(命令处理完的标准做法)
|
|
1748
|
+
>>>
|
|
1749
|
+
>>> @message.on_message(priority=50)
|
|
1750
|
+
... async def observer(event):
|
|
1751
|
+
... event.done(stop=False) # 仅认领,低优先级仍能看到(日志/统计)
|
|
1752
|
+
>>>
|
|
1753
|
+
>>> @message.on_message(priority=100)
|
|
1754
|
+
... async def firewall(event):
|
|
1755
|
+
... if denied(event):
|
|
1756
|
+
... event.done(claim=False) # 仅阻断,不标记已处理
|
|
1757
|
+
"""
|
|
1758
|
+
self.mark_processed(claim=claim, stop=stop)
|
|
1759
|
+
|
|
1760
|
+
def is_stopped(self) -> bool:
|
|
1761
|
+
"""
|
|
1762
|
+
事件传播是否已被阻断(是否已停止向低优先级处理器传播)
|
|
1763
|
+
|
|
1764
|
+
对应 ``done(stop=True)`` / ``mark_processed(stop=True)`` 的阻断效果。
|
|
1765
|
+
|
|
1766
|
+
:return: 是否已阻断传播
|
|
1704
1767
|
"""
|
|
1705
|
-
self
|
|
1768
|
+
return self.get("_propagation_stopped", False)
|
|
1706
1769
|
|
|
1707
1770
|
# ==================== 魔术方法 ====================
|
|
1708
1771
|
|
|
@@ -635,14 +635,34 @@ DEFAULT_PROACTIVE_GC_INTERVAL_SECS: Final[int] = 300
|
|
|
635
635
|
|
|
636
636
|
# 主动 GC 分代(generation)。
|
|
637
637
|
# 运行时行为。gc.collect(generation) 只回收指定代及更年轻的对象:0=最年轻代(最轻量),
|
|
638
|
-
# 1=中年代,2
|
|
638
|
+
# 1=中年代,2=最老代(覆盖最全,等价于全量 gc.collect())。默认 0 使常规轮次保持轻量;
|
|
639
|
+
# 深度回收由 proactive_gc_full_every 周期性触发。
|
|
639
640
|
# 修改影响: 设低减少单次扫描开销但回收不彻底,设高(2)覆盖更全但单次更重。
|
|
640
|
-
DEFAULT_PROACTIVE_GC_GENERATION: Final[int] =
|
|
641
|
+
DEFAULT_PROACTIVE_GC_GENERATION: Final[int] = 0
|
|
641
642
|
|
|
642
643
|
# 主动 GC 全量收集周期(每 N 轮做一次全量 gc.collect())。
|
|
643
644
|
# 运行时行为。0 表示禁用(仅按 generation 回收),N>0 表示每 N 轮额外触发一次全量回收。
|
|
645
|
+
# 全量回收受 proactive_gc_memory_growth_mb 内存增长门限约束,无增长时跳过。
|
|
644
646
|
# 修改影响: 设低(如 5)确保周期性深度回收,设高或 0 减少长尾暂停。
|
|
645
|
-
DEFAULT_PROACTIVE_GC_FULL_EVERY: Final[int] =
|
|
647
|
+
DEFAULT_PROACTIVE_GC_FULL_EVERY: Final[int] = 20
|
|
648
|
+
|
|
649
|
+
# 主动 GC 全量回收的内存增长门限(MB)。
|
|
650
|
+
# 运行时行为。周期性全量回收前,对比上次全量后的内存基线(优先 tracemalloc,其次 RSS),
|
|
651
|
+
# 仅当增长超过此值才真正触发 gc.collect(),避免内存稳定时空转。0 表示不设门限(每轮都回收)。
|
|
652
|
+
# 修改影响: 设小更频繁深度回收(更多暂停),设大更保守(内存峰值略高但 CPU 开销更低)。
|
|
653
|
+
DEFAULT_PROACTIVE_GC_MEMORY_GROWTH_MB: Final[int] = 32
|
|
654
|
+
|
|
655
|
+
# 主动 GC 空闲门控(仅在事件处理器空闲时执行 Python GC)。
|
|
656
|
+
# 运行时行为。开启后,若 adapter 存在未完成的 pending handler task(事件洪峰),
|
|
657
|
+
# 本轮跳过 Python GC,避免 GC 停顿与消息处理竞争。内部资源回收不受影响。
|
|
658
|
+
# 修改影响: 开启降低洪峰期延迟,但可能推迟内存回收。
|
|
659
|
+
DEFAULT_PROACTIVE_GC_IDLE_ONLY: Final[bool] = False
|
|
660
|
+
|
|
661
|
+
# 主动 GC 常规轮次的 gen0 垃圾量下限。
|
|
662
|
+
# 运行时行为。gc.get_count()[0] 低于此值时跳过 Python GC(无意义空转),
|
|
663
|
+
# 高于此值才按 generation 回收。0 表示始终回收。
|
|
664
|
+
# 修改影响: 设高(如 700=跟随 Python 自动回收节奏)更多轮次被跳过,设低更积极回收。
|
|
665
|
+
DEFAULT_PROACTIVE_GC_GEN0_MIN: Final[int] = 500
|
|
646
666
|
|
|
647
667
|
# shutdown 时等待在途事件处理器完成的最长时间(秒)。
|
|
648
668
|
# 运行时行为。适配器关闭时取消所有 pending handler tasks 后等待它们退出的耐心时间。
|
|
@@ -755,8 +775,11 @@ __all__ = [
|
|
|
755
775
|
"DEFAULT_MODULE_PRIORITY",
|
|
756
776
|
"DEFAULT_OFFLINE_BOT_EXPIRY_SECS",
|
|
757
777
|
"DEFAULT_PROACTIVE_GC_FULL_EVERY",
|
|
778
|
+
"DEFAULT_PROACTIVE_GC_GEN0_MIN",
|
|
758
779
|
"DEFAULT_PROACTIVE_GC_GENERATION",
|
|
780
|
+
"DEFAULT_PROACTIVE_GC_IDLE_ONLY",
|
|
759
781
|
"DEFAULT_PROACTIVE_GC_INTERVAL_SECS",
|
|
782
|
+
"DEFAULT_PROACTIVE_GC_MEMORY_GROWTH_MB",
|
|
760
783
|
"DEFAULT_RATE_LIMIT_CLEANUP_INTERVAL_SECS",
|
|
761
784
|
"DEFAULT_RATE_LIMIT_MAX_REQUESTS",
|
|
762
785
|
"DEFAULT_RATE_LIMIT_WINDOW_SECS",
|
|
@@ -51,6 +51,8 @@ TRANSLATIONS = {
|
|
|
51
51
|
"core.sdk.uninit.critical_error": "SDK uninitialization critical error: {error}",
|
|
52
52
|
"core.sdk.uninit.client_close_failed": "Failed to close HTTP client connection pool: {error}",
|
|
53
53
|
"core.sdk.gc.collected": "[GC] Python collected {collected} object(s), evicted {evicted} offline bot(s)",
|
|
54
|
+
"core.sdk.gc.skipped": "[GC] Round skipped (reason: {reason})",
|
|
55
|
+
"core.sdk.gc.config_changed": "[GC] Proactive GC config changed, restarting task",
|
|
54
56
|
# ==================== SDK Environment Preparation ====================
|
|
55
57
|
"core.sdk.prepare.start": "Starting initialization",
|
|
56
58
|
"core.sdk.prepare.starting": "Preparing initialization environment...",
|
|
@@ -51,6 +51,8 @@ TRANSLATIONS = {
|
|
|
51
51
|
"core.sdk.uninit.critical_error": "SDK 非初期化の重大なエラー: {error}",
|
|
52
52
|
"core.sdk.uninit.client_close_failed": "HTTP クライアント接続プールのクローズに失敗しました: {error}",
|
|
53
53
|
"core.sdk.gc.collected": "[GC] Python が {collected} 個のオブジェクトを回収し、{evicted} 個のオフライン Bot を削除しました",
|
|
54
|
+
"core.sdk.gc.skipped": "[GC] このラウンドはスキップされました(理由: {reason})",
|
|
55
|
+
"core.sdk.gc.config_changed": "[GC] アクティブ GC 設定が変更されたため、タスクを再起動します",
|
|
54
56
|
# ==================== SDK 環境準備 ====================
|
|
55
57
|
"core.sdk.prepare.start": "初期化を開始します",
|
|
56
58
|
"core.sdk.prepare.starting": "初期化環境を準備しています...",
|
|
@@ -51,6 +51,8 @@ TRANSLATIONS = {
|
|
|
51
51
|
"core.sdk.uninit.critical_error": "Критическая ошибка деинициализации SDK: {error}",
|
|
52
52
|
"core.sdk.uninit.client_close_failed": "Не удалось закрыть пул соединений HTTP-клиента: {error}",
|
|
53
53
|
"core.sdk.gc.collected": "[GC] Python собрал {collected} объект(ов), удалено {evicted} автономных бот(ов)",
|
|
54
|
+
"core.sdk.gc.skipped": "[GC] Цикл пропущен (причина: {reason})",
|
|
55
|
+
"core.sdk.gc.config_changed": "[GC] Конфигурация активного GC изменена, перезапуск задачи",
|
|
54
56
|
# ==================== Подготовка окружения SDK ====================
|
|
55
57
|
"core.sdk.prepare.start": "Начало инициализации",
|
|
56
58
|
"core.sdk.prepare.starting": "Подготовка окружения инициализации...",
|
|
@@ -51,6 +51,8 @@ TRANSLATIONS = {
|
|
|
51
51
|
"core.sdk.uninit.critical_error": "SDK反初始化严重错误: {error}",
|
|
52
52
|
"core.sdk.uninit.client_close_failed": "关闭 HTTP 客户端连接池失败: {error}",
|
|
53
53
|
"core.sdk.gc.collected": "[GC] Python 回收了 {collected} 个对象,清理了 {evicted} 个离线 Bot",
|
|
54
|
+
"core.sdk.gc.skipped": "[GC] 本轮跳过回收(原因: {reason})",
|
|
55
|
+
"core.sdk.gc.config_changed": "[GC] 主动 GC 配置已变更,重启回收任务",
|
|
54
56
|
# ==================== SDK 环境准备 ====================
|
|
55
57
|
"core.sdk.prepare.start": "开始初始化",
|
|
56
58
|
"core.sdk.prepare.starting": "准备初始化环境...",
|
|
@@ -51,6 +51,8 @@ TRANSLATIONS = {
|
|
|
51
51
|
"core.sdk.uninit.critical_error": "SDK反初始化嚴重錯誤: {error}",
|
|
52
52
|
"core.sdk.uninit.client_close_failed": "關閉 HTTP 客戶端連線池失敗: {error}",
|
|
53
53
|
"core.sdk.gc.collected": "[GC] Python 回收了 {collected} 個物件,清理了 {evicted} 個離線 Bot",
|
|
54
|
+
"core.sdk.gc.skipped": "[GC] 本輪略過回收(原因: {reason})",
|
|
55
|
+
"core.sdk.gc.config_changed": "[GC] 主動 GC 配置已變更,重新啟動回收任務",
|
|
54
56
|
# ==================== SDK 環境準備 ====================
|
|
55
57
|
"core.sdk.prepare.start": "開始初始化",
|
|
56
58
|
"core.sdk.prepare.starting": "準備初始化環境...",
|
|
@@ -22,8 +22,11 @@ from ..Core.constants import (
|
|
|
22
22
|
DEFAULT_MESSAGE_IGNORE_SELF,
|
|
23
23
|
DEFAULT_OFFLINE_BOT_EXPIRY_SECS,
|
|
24
24
|
DEFAULT_PROACTIVE_GC_FULL_EVERY,
|
|
25
|
+
DEFAULT_PROACTIVE_GC_GEN0_MIN,
|
|
25
26
|
DEFAULT_PROACTIVE_GC_GENERATION,
|
|
27
|
+
DEFAULT_PROACTIVE_GC_IDLE_ONLY,
|
|
26
28
|
DEFAULT_PROACTIVE_GC_INTERVAL_SECS,
|
|
29
|
+
DEFAULT_PROACTIVE_GC_MEMORY_GROWTH_MB,
|
|
27
30
|
DEFAULT_SERVER_AUTO_START,
|
|
28
31
|
DEFAULT_SERVER_HOST,
|
|
29
32
|
DEFAULT_SERVER_PORT,
|
|
@@ -83,6 +86,9 @@ DEFAULT_ERISPULSE_CONFIG = {
|
|
|
83
86
|
"proactive_gc_interval": DEFAULT_PROACTIVE_GC_INTERVAL_SECS,
|
|
84
87
|
"proactive_gc_generation": DEFAULT_PROACTIVE_GC_GENERATION,
|
|
85
88
|
"proactive_gc_full_every": DEFAULT_PROACTIVE_GC_FULL_EVERY,
|
|
89
|
+
"proactive_gc_memory_growth_mb": DEFAULT_PROACTIVE_GC_MEMORY_GROWTH_MB,
|
|
90
|
+
"proactive_gc_idle_only": DEFAULT_PROACTIVE_GC_IDLE_ONLY,
|
|
91
|
+
"proactive_gc_gen0_min": DEFAULT_PROACTIVE_GC_GEN0_MIN,
|
|
86
92
|
"offline_bot_expiry": DEFAULT_OFFLINE_BOT_EXPIRY_SECS,
|
|
87
93
|
},
|
|
88
94
|
"i18n": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
from collections import OrderedDict
|
|
15
16
|
from typing import Any
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
@@ -21,8 +22,12 @@ __all__ = [
|
|
|
21
22
|
"snapshot",
|
|
22
23
|
]
|
|
23
24
|
|
|
24
|
-
#
|
|
25
|
-
_prev_rss
|
|
25
|
+
# 快照标签的最大缓存数。防止使用动态 label 调用 snapshot() 时
|
|
26
|
+
# _prev_rss 无限增长,超出部分按最早写入顺序淘汰。
|
|
27
|
+
_MAX_SNAPSHOT_LABELS = 128
|
|
28
|
+
|
|
29
|
+
# 各 label 上一次快照的 RSS(MB),用于计算增量。按写入顺序淘汰。
|
|
30
|
+
_prev_rss: OrderedDict[str, float] = OrderedDict()
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
def get_rss_mb() -> float | None:
|
|
@@ -81,10 +86,14 @@ def snapshot(label: str = "") -> dict[str, Any]:
|
|
|
81
86
|
|
|
82
87
|
delta: float | None = None
|
|
83
88
|
key = label or "__default__"
|
|
84
|
-
if rss is not None and key in _prev_rss:
|
|
85
|
-
delta = rss - _prev_rss[key]
|
|
86
89
|
if rss is not None:
|
|
90
|
+
if key in _prev_rss:
|
|
91
|
+
delta = rss - _prev_rss[key]
|
|
92
|
+
# 记录并限制缓存上限,避免动态标签导致无界增长
|
|
87
93
|
_prev_rss[key] = rss
|
|
94
|
+
_prev_rss.move_to_end(key)
|
|
95
|
+
while len(_prev_rss) > _MAX_SNAPSHOT_LABELS:
|
|
96
|
+
_prev_rss.popitem(last=False)
|
|
88
97
|
|
|
89
98
|
return {
|
|
90
99
|
"label": label,
|
|
@@ -24,8 +24,11 @@ from typing import TYPE_CHECKING, Any
|
|
|
24
24
|
|
|
25
25
|
from .Core.constants import (
|
|
26
26
|
DEFAULT_PROACTIVE_GC_FULL_EVERY,
|
|
27
|
+
DEFAULT_PROACTIVE_GC_GEN0_MIN,
|
|
27
28
|
DEFAULT_PROACTIVE_GC_GENERATION,
|
|
29
|
+
DEFAULT_PROACTIVE_GC_IDLE_ONLY,
|
|
28
30
|
DEFAULT_PROACTIVE_GC_INTERVAL_SECS,
|
|
31
|
+
DEFAULT_PROACTIVE_GC_MEMORY_GROWTH_MB,
|
|
29
32
|
DEFAULT_UNINIT_TIMEOUT_SECS,
|
|
30
33
|
HARD_RESTART_EXIT_CODE,
|
|
31
34
|
LIFECYCLE_TIMER_CORE_INIT,
|
|
@@ -193,6 +196,7 @@ class SDK:
|
|
|
193
196
|
self._initializer: SDK.Initializer | None = None
|
|
194
197
|
self._initialized: bool = False
|
|
195
198
|
self._gc_task: asyncio.Task | None = None # 主动 GC 后台任务
|
|
199
|
+
self._gc_config_snapshot: tuple | None = None # 主动 GC 配置快照(变更检测)
|
|
196
200
|
self._shutdown_event: asyncio.Event | None = None # 优雅关闭事件
|
|
197
201
|
|
|
198
202
|
@property
|
|
@@ -753,6 +757,15 @@ class SDK:
|
|
|
753
757
|
adapter_manager.clear()
|
|
754
758
|
module_manager.clear()
|
|
755
759
|
|
|
760
|
+
# 6.5 立即回收一次,尽早释放模块/适配器实例与循环引用,
|
|
761
|
+
# 降低 uninit 与 hard_restart 之间的内存驻留
|
|
762
|
+
try:
|
|
763
|
+
import gc
|
|
764
|
+
|
|
765
|
+
gc.collect()
|
|
766
|
+
except Exception:
|
|
767
|
+
pass
|
|
768
|
+
|
|
756
769
|
# 获取清理耗时
|
|
757
770
|
uninit_duration = self.lifecycle.stop_timer(LIFECYCLE_TIMER_CORE_UNINIT)
|
|
758
771
|
|
|
@@ -871,66 +884,97 @@ class SDK:
|
|
|
871
884
|
定期执行 Python GC 和内部资源回收(离线 Bot 清理等),
|
|
872
885
|
防止长期运行时的内存增长。
|
|
873
886
|
|
|
874
|
-
GC
|
|
887
|
+
GC 行为由多项框架配置控制(均支持热更新,变更时即时重启任务):
|
|
875
888
|
|
|
876
889
|
- ``proactive_gc_interval``: 回收间隔秒数(0 禁用)
|
|
877
|
-
- ``proactive_gc_generation``:
|
|
878
|
-
- ``proactive_gc_full_every``: 每 N
|
|
890
|
+
- ``proactive_gc_generation``: 常规轮次回收分代(0/1/2,钳制到 0..2)
|
|
891
|
+
- ``proactive_gc_full_every``: 每 N 轮做一次全量回收(0 禁用)
|
|
892
|
+
- ``proactive_gc_memory_growth_mb``: 全量回收的内存增长门限(0 不设限)
|
|
893
|
+
- ``proactive_gc_idle_only``: 事件洪峰时跳过 Python GC(避免停顿竞争)
|
|
894
|
+
- ``proactive_gc_gen0_min``: gen0 垃圾量下限,低于则跳过回收(空转轮次零开销)
|
|
879
895
|
|
|
880
896
|
初始化阶段已调用 ``gc.freeze()`` 将框架对象移入永久代,
|
|
881
897
|
此处 ``gc.collect()`` 仅扫描运行期新建对象。
|
|
882
898
|
"""
|
|
883
|
-
# 停止已有的 GC
|
|
899
|
+
# 停止已有的 GC 任务(含反注册配置钩子)
|
|
884
900
|
self._stop_proactive_gc()
|
|
885
901
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
fw = get_framework_config()
|
|
894
|
-
interval = fw.get("proactive_gc_interval", interval)
|
|
895
|
-
generation = fw.get("proactive_gc_generation", generation)
|
|
896
|
-
full_every = fw.get("proactive_gc_full_every", full_every)
|
|
897
|
-
except Exception:
|
|
898
|
-
pass
|
|
899
|
-
return interval, generation, full_every
|
|
902
|
+
# 注册配置变更钩子:proactive_gc_* 变化时即时重启任务。
|
|
903
|
+
# 修复旧实现"interval 切到 0 后无法再启用"的问题。
|
|
904
|
+
try:
|
|
905
|
+
self.lifecycle.register("config.set", self._on_gc_config_event)
|
|
906
|
+
self.lifecycle.register("config.updated", self._on_gc_config_event)
|
|
907
|
+
except Exception:
|
|
908
|
+
pass
|
|
900
909
|
|
|
901
|
-
|
|
910
|
+
self._gc_config_snapshot = self._read_gc_config()
|
|
911
|
+
if self._gc_config_snapshot[0] <= 0:
|
|
902
912
|
return # 配置禁用
|
|
903
913
|
|
|
904
914
|
async def _gc_loop():
|
|
905
915
|
import gc
|
|
906
916
|
|
|
907
917
|
round_count = 0
|
|
918
|
+
memory_baseline: float | None = None
|
|
908
919
|
while True:
|
|
909
920
|
try:
|
|
910
|
-
#
|
|
911
|
-
|
|
921
|
+
# 每轮重新读取配置,支持热更新;常规变更已由配置钩子即时重启任务
|
|
922
|
+
cfg = self._read_gc_config()
|
|
923
|
+
interval = cfg[0]
|
|
912
924
|
if interval <= 0:
|
|
913
925
|
# 配置运行时禁用了 GC,停止循环
|
|
914
926
|
break
|
|
915
927
|
await asyncio.sleep(interval)
|
|
916
928
|
round_count += 1
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
collected = gc.collect()
|
|
921
|
-
else:
|
|
922
|
-
collected = gc.collect(generation)
|
|
923
|
-
# 2. 内部资源回收
|
|
929
|
+
|
|
930
|
+
# 1. 内部资源回收(廉价,始终执行)
|
|
931
|
+
evicted = 0
|
|
924
932
|
try:
|
|
925
|
-
|
|
926
|
-
evicted = adapter_mgr._evict_offline_bots()
|
|
927
|
-
if collected > 0 or evicted > 0:
|
|
928
|
-
self.logger.trace(
|
|
929
|
-
i18n.t("core.sdk.gc.collected", collected=collected, evicted=evicted)
|
|
930
|
-
)
|
|
933
|
+
evicted = self.adapter._evict_offline_bots()
|
|
931
934
|
except Exception:
|
|
932
935
|
pass
|
|
933
|
-
|
|
936
|
+
|
|
937
|
+
# 2. 空闲门控:事件洪峰时跳过 Python GC,避免停顿与消息处理竞争
|
|
938
|
+
if cfg[4] and self._has_handler_backlog():
|
|
939
|
+
self.logger.trace(
|
|
940
|
+
i18n.t("core.sdk.gc.skipped", reason="busy")
|
|
941
|
+
)
|
|
942
|
+
if evicted > 0:
|
|
943
|
+
self.logger.trace(
|
|
944
|
+
i18n.t(
|
|
945
|
+
"core.sdk.gc.collected", collected=0, evicted=evicted
|
|
946
|
+
)
|
|
947
|
+
)
|
|
948
|
+
continue
|
|
949
|
+
|
|
950
|
+
# 3. 垃圾量门控:gen0 无足够垃圾时跳过回收(空转轮次近乎零开销)
|
|
951
|
+
if gc.get_count()[0] < cfg[5]:
|
|
952
|
+
if evicted > 0:
|
|
953
|
+
self.logger.trace(
|
|
954
|
+
i18n.t(
|
|
955
|
+
"core.sdk.gc.collected", collected=0, evicted=evicted
|
|
956
|
+
)
|
|
957
|
+
)
|
|
958
|
+
continue
|
|
959
|
+
|
|
960
|
+
# 4. 周期性全量(受内存增长门限约束)或常规分代回收
|
|
961
|
+
is_full = cfg[2] > 0 and round_count % cfg[2] == 0
|
|
962
|
+
if is_full:
|
|
963
|
+
collected, memory_baseline = self._run_full_gc_collection(
|
|
964
|
+
gc, memory_baseline, cfg[3]
|
|
965
|
+
)
|
|
966
|
+
else:
|
|
967
|
+
collected = gc.collect(cfg[1])
|
|
968
|
+
|
|
969
|
+
if collected > 0 or evicted > 0:
|
|
970
|
+
self.logger.trace(
|
|
971
|
+
i18n.t(
|
|
972
|
+
"core.sdk.gc.collected",
|
|
973
|
+
collected=collected,
|
|
974
|
+
evicted=evicted,
|
|
975
|
+
)
|
|
976
|
+
)
|
|
977
|
+
# 5. 内存快照(TRACE),便于长期观察内存变化趋势
|
|
934
978
|
try:
|
|
935
979
|
from .runtime.memory import log_snapshot
|
|
936
980
|
|
|
@@ -951,12 +995,148 @@ class SDK:
|
|
|
951
995
|
def _stop_proactive_gc(self) -> None:
|
|
952
996
|
"""
|
|
953
997
|
{!--< internal-use >!--}
|
|
954
|
-
停止主动 GC
|
|
998
|
+
停止主动 GC 后台任务,并反注册配置变更钩子
|
|
955
999
|
"""
|
|
1000
|
+
try:
|
|
1001
|
+
self.lifecycle.unregister("config.set", self._on_gc_config_event)
|
|
1002
|
+
self.lifecycle.unregister("config.updated", self._on_gc_config_event)
|
|
1003
|
+
except Exception:
|
|
1004
|
+
pass
|
|
1005
|
+
self._gc_config_snapshot = None
|
|
956
1006
|
if self._gc_task is not None and not self._gc_task.done():
|
|
957
1007
|
self._gc_task.cancel()
|
|
958
1008
|
self._gc_task = None
|
|
959
1009
|
|
|
1010
|
+
@staticmethod
|
|
1011
|
+
def _read_gc_config() -> tuple[float, int, int, int, bool, int]:
|
|
1012
|
+
"""
|
|
1013
|
+
{!--< internal-use >!--}
|
|
1014
|
+
读取并钳制主动 GC 相关框架配置
|
|
1015
|
+
|
|
1016
|
+
:return: ``(interval, generation, full_every, growth_mb, idle_only, gen0_min)``
|
|
1017
|
+
元组,值均已钳制到合法范围;``interval`` 为秒(支持小数)
|
|
1018
|
+
"""
|
|
1019
|
+
interval = DEFAULT_PROACTIVE_GC_INTERVAL_SECS
|
|
1020
|
+
generation = DEFAULT_PROACTIVE_GC_GENERATION
|
|
1021
|
+
full_every = DEFAULT_PROACTIVE_GC_FULL_EVERY
|
|
1022
|
+
growth_mb = DEFAULT_PROACTIVE_GC_MEMORY_GROWTH_MB
|
|
1023
|
+
idle_only = DEFAULT_PROACTIVE_GC_IDLE_ONLY
|
|
1024
|
+
gen0_min = DEFAULT_PROACTIVE_GC_GEN0_MIN
|
|
1025
|
+
try:
|
|
1026
|
+
from .runtime import get_framework_config
|
|
1027
|
+
|
|
1028
|
+
fw = get_framework_config()
|
|
1029
|
+
raw_interval = fw.get("proactive_gc_interval", interval)
|
|
1030
|
+
interval = float(raw_interval) if raw_interval is not None else interval
|
|
1031
|
+
generation = int(fw.get("proactive_gc_generation", generation))
|
|
1032
|
+
full_every = int(fw.get("proactive_gc_full_every", full_every))
|
|
1033
|
+
growth_mb = int(fw.get("proactive_gc_memory_growth_mb", growth_mb))
|
|
1034
|
+
idle_only = bool(fw.get("proactive_gc_idle_only", idle_only))
|
|
1035
|
+
gen0_min = int(fw.get("proactive_gc_gen0_min", gen0_min))
|
|
1036
|
+
except Exception:
|
|
1037
|
+
pass
|
|
1038
|
+
return (
|
|
1039
|
+
max(0.0, float(interval)),
|
|
1040
|
+
max(0, min(2, generation)),
|
|
1041
|
+
max(0, full_every),
|
|
1042
|
+
max(0, growth_mb),
|
|
1043
|
+
idle_only,
|
|
1044
|
+
max(0, gen0_min),
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
def _on_gc_config_event(self, _data: dict) -> None:
|
|
1048
|
+
"""
|
|
1049
|
+
{!--< internal-use >!--}
|
|
1050
|
+
``config.set`` / ``config.updated`` 回调:proactive_gc_* 配置变化时重启 GC 任务
|
|
1051
|
+
|
|
1052
|
+
相比旧实现"每轮重读",此钩子使配置变更(含 0→N 重新启用)即时生效。
|
|
1053
|
+
从后台线程(如 config watcher)触发时,调度回主事件循环再重启。
|
|
1054
|
+
"""
|
|
1055
|
+
try:
|
|
1056
|
+
current = self._read_gc_config()
|
|
1057
|
+
if (
|
|
1058
|
+
self._gc_config_snapshot is None
|
|
1059
|
+
or current == self._gc_config_snapshot
|
|
1060
|
+
):
|
|
1061
|
+
return
|
|
1062
|
+
except Exception:
|
|
1063
|
+
return
|
|
1064
|
+
|
|
1065
|
+
def _restart() -> None:
|
|
1066
|
+
try:
|
|
1067
|
+
self._start_proactive_gc()
|
|
1068
|
+
except Exception:
|
|
1069
|
+
pass
|
|
1070
|
+
|
|
1071
|
+
try:
|
|
1072
|
+
# 当前线程有运行中的事件循环:直接重启
|
|
1073
|
+
asyncio.get_running_loop()
|
|
1074
|
+
_restart()
|
|
1075
|
+
except RuntimeError:
|
|
1076
|
+
# 非事件循环线程(如 config watcher):调度回主循环执行
|
|
1077
|
+
from .runtime.tasks import _get_main_loop
|
|
1078
|
+
|
|
1079
|
+
main_loop = _get_main_loop()
|
|
1080
|
+
if main_loop is not None and main_loop.is_running():
|
|
1081
|
+
main_loop.call_soon_threadsafe(_restart)
|
|
1082
|
+
else:
|
|
1083
|
+
# 无可用主循环:跳过本次重启,循环自身每轮重读配置兜底
|
|
1084
|
+
pass
|
|
1085
|
+
|
|
1086
|
+
@staticmethod
|
|
1087
|
+
def _has_handler_backlog() -> bool:
|
|
1088
|
+
"""
|
|
1089
|
+
{!--< internal-use >!--}
|
|
1090
|
+
事件处理器洪峰检测
|
|
1091
|
+
|
|
1092
|
+
:return: 存在未完成的 pending handler task 时返回 True
|
|
1093
|
+
"""
|
|
1094
|
+
try:
|
|
1095
|
+
from .Core.adapter import adapter as _adapter
|
|
1096
|
+
|
|
1097
|
+
return len(getattr(_adapter, "_pending_handler_tasks", ())) > 0
|
|
1098
|
+
except Exception:
|
|
1099
|
+
return False
|
|
1100
|
+
|
|
1101
|
+
@staticmethod
|
|
1102
|
+
def _run_full_gc_collection(
|
|
1103
|
+
gc_module: Any, baseline: float | None, growth_mb: int
|
|
1104
|
+
) -> tuple[int, float | None]:
|
|
1105
|
+
"""
|
|
1106
|
+
{!--< internal-use >!--}
|
|
1107
|
+
执行一次全量回收(受内存增长门限约束)
|
|
1108
|
+
|
|
1109
|
+
优先使用 tracemalloc 追踪值,不可用则回退 RSS。当 ``growth_mb > 0``
|
|
1110
|
+
且距上次全量回收基线增长不足时跳过回收,避免内存稳定时空转。
|
|
1111
|
+
|
|
1112
|
+
:param gc_module: ``gc`` 模块(便于测试注入)
|
|
1113
|
+
:param baseline: 上次全量回收后的内存基线(MB),None 表示首次
|
|
1114
|
+
:param growth_mb: 内存增长门限(MB),0 表示不设门限
|
|
1115
|
+
:return: ``(collected, 新基线)``
|
|
1116
|
+
"""
|
|
1117
|
+
current: float | None = None
|
|
1118
|
+
try:
|
|
1119
|
+
from .runtime.memory import get_rss_mb, get_traced_mb
|
|
1120
|
+
|
|
1121
|
+
current = get_traced_mb()
|
|
1122
|
+
if current is None:
|
|
1123
|
+
current = get_rss_mb()
|
|
1124
|
+
except Exception:
|
|
1125
|
+
pass
|
|
1126
|
+
|
|
1127
|
+
if current is not None:
|
|
1128
|
+
if growth_mb > 0 and baseline is not None:
|
|
1129
|
+
# 内存稳定:增长未达门限,跳过本次全量回收
|
|
1130
|
+
if (current - baseline) < growth_mb:
|
|
1131
|
+
return 0, baseline
|
|
1132
|
+
baseline = current
|
|
1133
|
+
|
|
1134
|
+
try:
|
|
1135
|
+
collected = gc_module.collect()
|
|
1136
|
+
except Exception:
|
|
1137
|
+
collected = 0
|
|
1138
|
+
return collected, baseline
|
|
1139
|
+
|
|
960
1140
|
def dump_state(self) -> dict:
|
|
961
1141
|
"""
|
|
962
1142
|
导出框架当前运行状态的快照
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|