Source code for ralph.process.manager._singleton

"""Module-level ProcessManager singleton and related helpers."""

from __future__ import annotations

import atexit
import contextlib
from typing import TYPE_CHECKING

from loguru import logger

from ralph.process.manager._process_manager import ProcessManager
from ralph.process.manager._process_manager_runtime import load_psutil_module as _load_psutil_module
from ralph.process.manager._process_termination_error import ProcessTerminationError

if TYPE_CHECKING:
    from collections.abc import Generator

    from ralph.process.manager._process_manager_policy import ProcessManagerPolicy


class _ProcessManagerState:
    """Mutable holder for module-level singleton and registration flag."""

    instance: ProcessManager | None = None
    atexit_registered: bool = False


_DEFAULT_PSUTIL = _load_psutil_module()

_pm_state = _ProcessManagerState()


def _atexit_shutdown() -> None:
    try:
        pm = _pm_state.instance
        if pm is None:
            return
        pm.shutdown_all(grace_period_s=0.5)
    except BaseException:
        pass


[docs] def get_process_manager(*, policy: ProcessManagerPolicy | None = None) -> ProcessManager: """Return the module-level ProcessManager singleton, creating it on first call.""" if _pm_state.instance is None: _pm_state.instance = ProcessManager(policy=policy, psutil=_DEFAULT_PSUTIL) if not _pm_state.atexit_registered: atexit.register(_atexit_shutdown) _pm_state.atexit_registered = True return _pm_state.instance
def reset_process_manager() -> None: """Replace the singleton with a fresh instance. Call from test teardown. The prior instance, if any, is shut down BEFORE the singleton reference is nulled so a dangling tracked record (a real process the previous test started) is reaped instead of being orphaned. Shutting down BEFORE nulling prevents any new caller (including a late shutdown triggered by a child of the prior manager) from seeing the prior instance via ``get_process_manager`` while it is still being torn down. Any exception raised by ``shutdown_all`` is swallowed (test teardown must never raise) so a refusing-to-die process cannot break the suite. """ prior = _pm_state.instance if prior is not None: with contextlib.suppress(BaseException): prior.shutdown_all(grace_period_s=0.5) _pm_state.instance = None @contextlib.contextmanager def process_phase_scope(phase_name: str) -> Generator[None, None, None]: """Context manager that tears down all processes labeled 'phase:<phase_name>' on exit.""" try: yield finally: pm = get_process_manager() try: pm.shutdown_all_for_label( f"phase:{phase_name}", grace_period_s=pm.policy.default_grace_period_s, ) except ProcessTerminationError: logger.error("phase:{} cleanup failed", phase_name) raise