From 35a24c27e4b3bafb151f15a20b42a8ed0e85062b Mon Sep 17 00:00:00 2001 From: Karel Capek Robotics <96583804+MelkorBalrog@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:34:34 -0400 Subject: [PATCH] Fix duplicate header in service manager --- AutoML.py | 17 +++-------- HISTORY.md | 4 +++ README.md | 2 +- mainappsrc/services/__init__.py | 7 ++--- mainappsrc/services/service_manager.py | 2 ++ mainappsrc/version.py | 2 +- tests/test_launcher_threading.py | 39 ++++++++++++++++++++++++++ tests/test_thread_manager.py | 9 +++++- tools/memory_manager.py | 10 ++++--- 9 files changed, 67 insertions(+), 25 deletions(-) diff --git a/AutoML.py b/AutoML.py index d135e41e0..936160b3c 100644 --- a/AutoML.py +++ b/AutoML.py @@ -77,8 +77,8 @@ from tools.model_loader import start_cleanup_thread, stop_cleanup_thread from tools.splash_launcher import SplashLauncher from tools.trash_eater import manager_eater +from tools.thread_manager import manager as thread_manager from mainappsrc.version import VERSION -from mainappsrc.services import service_manager from mainappsrc.core.automl_core import ( AutoMLApp, FaultTreeNode, @@ -300,18 +300,9 @@ def _loader() -> Any: if module is None: # pragma: no cover - defensive return - class _AutoMLCoreService: - def __init__(self, mod: Any) -> None: - self._module = mod - - def run(self) -> None: - self._module.main() - - service_manager.request( - "automl_core", lambda: _AutoMLCoreService(module), recoverable=False, daemon=False - ) - service_manager.join("automl_core") - service_manager.release("automl_core") + thread = thread_manager.register("main_app", module.main, daemon=False) + thread.join() + thread_manager.unregister("main_app") memory_manager.cleanup() if _diagnostics_manager: diff --git a/HISTORY.md b/HISTORY.md index 7c0952ba9..92554b309 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -19,6 +19,10 @@ --> # Version History +- 0.2.208 - Run core services through monitored threads, reintroduce threaded + service manager and execute main app within thread manager. +- 0.2.207 - Remove threaded service manager and launch core directly. +- 0.2.206 - Internal version synchronisation. - 0.2.205 - Launch AutoML core through service manager and allow non-daemon service threads with join support. - 0.2.204 - Introduce threaded service manager to lazily load services, diff --git a/README.md b/README.md index 2d0146948..c4cd12a55 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -version: 0.2.206 +version: 0.2.208 Author: Miguel Marina - [LinkedIn](https://www.linkedin.com/in/progman32/) # AutoML diff --git a/mainappsrc/services/__init__.py b/mainappsrc/services/__init__.py index b55d78a1a..530cc83b9 100644 --- a/mainappsrc/services/__init__.py +++ b/mainappsrc/services/__init__.py @@ -134,6 +134,8 @@ SERVICE_MODULES = SERVICE_CLASSES __all__ = list(SERVICE_CLASSES) +from .service_manager import ServiceManager, manager as service_manager +__all__.extend(["ServiceManager", "service_manager"]) def __getattr__(name: str) -> Any: # pragma: no cover - simple delegation @@ -150,8 +152,3 @@ def __getattr__(name: str) -> Any: # pragma: no cover - simple delegation raise AttributeError(f"module 'mainappsrc.services' has no attribute {name!r}") from exc module = import_module(module_name) return getattr(module, attr_name) - - -from .service_manager import ServiceManager, manager as service_manager - -__all__.extend(["ServiceManager", "service_manager"]) diff --git a/mainappsrc/services/service_manager.py b/mainappsrc/services/service_manager.py index 827011bf5..c119a7eae 100644 --- a/mainappsrc/services/service_manager.py +++ b/mainappsrc/services/service_manager.py @@ -123,3 +123,5 @@ def _watchdog(self) -> None: # pragma: no cover - simple loop manager = ServiceManager() + +__all__ = ["ServiceManager", "manager"] diff --git a/mainappsrc/version.py b/mainappsrc/version.py index 400ddeb62..fb086b0bc 100644 --- a/mainappsrc/version.py +++ b/mainappsrc/version.py @@ -18,6 +18,6 @@ """Project version information.""" -VERSION = "0.2.206" +VERSION = "0.2.208" __all__ = ["VERSION"] diff --git a/tests/test_launcher_threading.py b/tests/test_launcher_threading.py index 0a5d19b7e..1afcacf2d 100644 --- a/tests/test_launcher_threading.py +++ b/tests/test_launcher_threading.py @@ -17,6 +17,7 @@ # along with this program. If not, see . import time +import threading import automl as launcher @@ -44,3 +45,41 @@ def wait(self): launcher.ensure_packages() elapsed = time.time() - start assert elapsed < 0.35 + + +def test_main_runs_in_thread(monkeypatch): + """The main application executes within a monitored thread.""" + + import AutoML as real_launcher + + called: list[str] = [] + + def fake_register(name, target, *a, **k): + called.append(name) + thread = threading.Thread(target=target, daemon=k.get("daemon", True)) + thread.start() + return thread + + monkeypatch.setattr(real_launcher.thread_manager, "register", fake_register) + monkeypatch.setattr(real_launcher.thread_manager, "unregister", lambda name: None) + + class DummySplash: + def __init__(self, loader, post_delay=0): + self.loader = loader + + def launch(self): + self.loader() + + monkeypatch.setattr(real_launcher, "SplashLauncher", DummySplash) + + def fake_bootstrap(): + class _Mod: + def main(self): + pass + + return _Mod() + + monkeypatch.setattr(real_launcher, "_bootstrap", fake_bootstrap) + + real_launcher.main() + assert "main_app" in called diff --git a/tests/test_thread_manager.py b/tests/test_thread_manager.py index 5b4845f94..20065e5e8 100644 --- a/tests/test_thread_manager.py +++ b/tests/test_thread_manager.py @@ -18,7 +18,8 @@ import time -from tools.thread_manager import ThreadManager +from tools.thread_manager import ThreadManager, manager as thread_manager +from tools.memory_manager import manager as memory_manager def test_thread_manager_restarts_dead_thread() -> None: @@ -32,3 +33,9 @@ def worker() -> None: time.sleep(0.15) # allow thread to run and be restarted assert runs["count"] >= 2 manager.stop_all() + + +def test_memory_manager_thread_registered() -> None: + """Memory manager uses thread manager for its background thread.""" + + assert "memory_manager" in thread_manager._threads diff --git a/tools/memory_manager.py b/tools/memory_manager.py index 16440fefa..dcc26b536 100644 --- a/tools/memory_manager.py +++ b/tools/memory_manager.py @@ -33,6 +33,8 @@ from typing import Any, Callable, Dict, Set import atexit +from .thread_manager import manager as thread_manager + try: # pragma: no cover - optional dependency import psutil except Exception: # pragma: no cover - psutil may not be installed @@ -55,8 +57,7 @@ def __init__(self, interval: float = 60.0) -> None: self._procs: Dict[str, Any] = {} self._interval = interval self._stop_event = threading.Event() - self._thread = threading.Thread(target=self._monitor, daemon=True) - self._thread.start() + self._thread = thread_manager.register("memory_manager", self._monitor, daemon=True) def lazy_load(self, key: str, loader: Callable[[], Any]) -> Any: """Return cached object for *key*, loading it if necessary.""" @@ -117,8 +118,9 @@ def _monitor(self) -> None: def shutdown(self) -> None: """Stop the monitoring thread.""" self._stop_event.set() - if self._thread.is_alive(): - self._thread.join(timeout=1) + thread = thread_manager.unregister("memory_manager") + if thread and thread.is_alive(): + thread.join(timeout=1) def lazy_import(name: str) -> Any: