fix: 修复批量插件重载窗口期残留 handler 直调导致 missing event TypeError

插件重载 stop 阶段清除 sys.modules 缓存后,若广播事件命中注册表中残留的旧类方法声明,
EventBindingResolver.owner_class 因 inspect.getmodule 返回 None 而解析失败,原实现会
直接返回原始函数并绕过实例绑定,旧签名无 event 参数时抛出 TypeError 并刷屏通知。

- binding.py:owner_class 解析失败时区分类方法声明与自由函数;类方法声明记入
  unresolved 诊断并跳过执行,等待重载完成后新 handler 按标识覆盖自愈;
  自由函数保持直调路径不变。
- 收敛 binding-miss 与新路径的未命中记账逻辑到 _record_unresolved,首次未命中才告警。
- 新增回归测试:模块卸载后残留类方法被跳过、自由函数直调不受影响。

Closes #6351
This commit is contained in:
jxxghp
2026-08-18 21:40:13 +08:00
parent 4cc06ce7ad
commit 282b922f1b
2 changed files with 80 additions and 9 deletions
+27 -9
View File
@@ -57,6 +57,12 @@ class EventBindingResolver:
return "", names[0]
return names[0], names[1]
@staticmethod
def is_class_method_declaration(handler: Callable) -> bool:
"""判断处理器是否声明在类体内(限定名含类前缀且非局部闭包)。"""
parts = handler.__qualname__.split(".")
return len(parts) >= 2 and "<locals>" not in parts
@staticmethod
def owner_class(handler: Callable) -> Optional[Type[Any]]:
"""从处理器对象本身解析声明类,不按字符串动态导入模块。"""
@@ -75,6 +81,14 @@ class EventBindingResolver:
return None
return owner if isinstance(owner, type) else None
def _record_unresolved(self, identifier: str, reason: str) -> None:
"""首次未命中时记录告警,避免重载窗口内重复刷屏。"""
with self._lock:
first_miss = identifier not in self._unresolved
self._unresolved.add(identifier)
if first_miss:
logger.warning(reason, identifier)
def resolve(
self,
handler: Callable,
@@ -87,6 +101,15 @@ class EventBindingResolver:
self.parse_handler_names(handler)[1],
)
if owner_class is None:
# 插件重载会先清除模块缓存,窗口期内残留的旧类方法声明无法定位声明类;
# 此时直接调用原始函数会绕过实例绑定(旧签名可能与事件调用约定不一致),
# 因此按未绑定处理跳过,等待重载完成后按新 handler 注册自愈。
if self.is_class_method_declaration(handler):
self._record_unresolved(
EventRegistry.handler_identifier(handler),
"事件处理器所属模块已卸载或声明类不可解析,跳过执行:%s",
)
return None
binding = EventHandlerBinding(
instance=None,
owner_name=EventRegistry.handler_identifier(handler),
@@ -105,15 +128,10 @@ class EventBindingResolver:
resolver_name = name
break
if binding is None:
identifier = EventRegistry.handler_identifier(handler)
with self._lock:
first_miss = identifier not in self._unresolved
self._unresolved.add(identifier)
if first_miss:
logger.warning(
"事件处理器未绑定显式 resolver,已跳过:%s",
identifier,
)
self._record_unresolved(
EventRegistry.handler_identifier(handler),
"事件处理器未绑定显式 resolver,已跳过:%s",
)
return None
logger.debug(
"事件处理器绑定:%s -> %s",