diff --git a/app/runtime/event/registry.py b/app/runtime/event/registry.py index 554145dbd..003f9f01c 100644 --- a/app/runtime/event/registry.py +++ b/app/runtime/event/registry.py @@ -29,11 +29,19 @@ class EventRegistry: self._disabled_handlers = disabled_handlers self._disabled_classes = disabled_classes + @staticmethod + def _module_name(target: Callable | type) -> str: + """优先读取对象保留的模块名,避免卸载缓存后事件标识漂移。""" + module_name = getattr(target, "__module__", None) + if module_name: + return module_name + module = inspect.getmodule(target) + return module.__name__ if module else "unknown_module" + @staticmethod def handler_identifier(target: Callable | type) -> str: """返回包含模块和限定名的稳定处理器标识。""" - module = inspect.getmodule(target) - module_name = module.__name__ if module else "unknown_module" + module_name = EventRegistry._module_name(target) return f"{module_name}.{target.__qualname__}" @classmethod @@ -46,8 +54,7 @@ class EventRegistry: qualname_parts = handler.__qualname__.split(".") if len(qualname_parts) <= 1: return None - module = inspect.getmodule(handler) - module_name = module.__name__ if module else "unknown_module" + module_name = cls._module_name(handler) return f"{module_name}.{'.'.join(qualname_parts[:-1])}" def is_handler_enabled(self, handler: Callable) -> bool: diff --git a/tests/test_event_runtime_components.py b/tests/test_event_runtime_components.py index 5605749ac..0ef669a38 100644 --- a/tests/test_event_runtime_components.py +++ b/tests/test_event_runtime_components.py @@ -10,7 +10,9 @@ from app.runtime.event.binding import ( EventBindingResolver, EventHandlerBinding, ) +from app.runtime.event.dispatch import EventDispatcher from app.runtime.event.errors import EventErrorPolicy +from app.runtime.event.registry import EventRegistry from app.runtime.events import Event from app.runtime.extensions.plugin import manager as plugin_manager_module from app.runtime.extensions.plugin.manager import PluginManager @@ -85,14 +87,69 @@ def test_unloaded_module_class_handler_is_skipped() -> None: resolvers=lambda: {}, ) assert binding.resolve(residual_handler) is None - # 模块卸载后 identifier 回退为 unknown_module 前缀,与线上日志一致 assert binding.unresolved_handlers() == ( - "unknown_module._ResidualPlugin.reload", + f"{fake_name}._ResidualPlugin.reload", ) finally: sys.modules.pop(fake_name, None) +def test_quiesced_handler_stays_disabled_after_module_unload() -> None: + """事件快照中的旧 handler 在模块卸载后仍应按已禁用状态跳过。""" + fake_name = "tests._fake_quiesced_plugin" + fake_module = types.ModuleType(fake_name) + sys.modules[fake_name] = fake_module + try: + exec( + "class _QuiescedPlugin:\n" + " def reload(self, event):\n" + " raise AssertionError('quiesced handler must not run')\n", + fake_module.__dict__, + ) + residual_handler = fake_module._QuiescedPlugin.reload + broadcast_subscribers: dict = {} + disabled_handlers: set[str] = set() + disabled_classes: set[str] = set() + registry = EventRegistry( + lock=threading.Lock(), + broadcast_subscribers=lambda: broadcast_subscribers, + chain_subscribers=lambda: {}, + disabled_handlers=lambda: disabled_handlers, + disabled_classes=lambda: disabled_classes, + ) + registry.add(EventType.PluginReload, residual_handler, 0) + registry.disable(fake_module._QuiescedPlugin) + del sys.modules[fake_name] + + assert EventRegistry.handler_identifier(residual_handler) == ( + f"{fake_name}._QuiescedPlugin.reload" + ) + assert EventRegistry.handler_class_identifier(residual_handler) == ( + f"{fake_name}._QuiescedPlugin" + ) + assert not registry.is_handler_enabled(residual_handler) + + binding = EventBindingResolver( + lock=threading.Lock(), + resolvers=lambda: {}, + ) + dispatcher = EventDispatcher( + registry=registry, + binding_resolver=binding, + event_factory=Event, + error_handler=lambda **_kwargs: None, + async_handle_sink=lambda _handle: True, + sync_handle_sink=lambda _callback, _args: True, + ) + dispatcher.dispatch_broadcast_strict( + Event(EventType.PluginReload, {"plugin_id": "_QuiescedPlugin"}), + lambda _awaitable: None, + ) + assert binding.unresolved_handlers() == () + finally: + sys.modules.pop(fake_name, None) + + def test_unloaded_module_decorator_wrapped_method_is_skipped() -> None: """装饰器包装的类方法限定名含 ,模块卸载后也必须跳过而非直调。""" fake_name = "tests._fake_unloaded_decorated_plugin" @@ -121,7 +178,7 @@ def test_unloaded_module_decorator_wrapped_method_is_skipped() -> None: ) assert binding.resolve(residual_handler) is None assert binding.unresolved_handlers() == ( - "unknown_module._deco..wrapper", + f"{fake_name}._deco..wrapper", ) finally: sys.modules.pop(fake_name, None)