Skip to content

Lifecycle

Runnable example

python/examples/lifecycle_shutdown.py — asks the engine to quit gracefully via Lifecycle.Shutdown.

Graceful shutdown is best-effort

Lifecycle.Shutdown only asks the engine to quit — the ACK confirms the request, not that the process died. On Linux (including the dev container) FrooxEngine frequently hangs during teardown and the engine never exits on its own. When you need a guaranteed stop, follow up with resoio.terminate (a forceful kill by host PID): shutdown to ask nicely, terminate to make sure.

resoio.lifecycle.LifecycleClient

LifecycleClient(socket_path: str | None = None)

Bases: _BaseClient[LifecycleStub]

Async client for the Resonite IO Lifecycle service over a UDS.

Use as an async context manager so the gRPC channel is closed deterministically. With socket_path=None the path is resolved on __aenter__ via RESONITE_IO_SOCKETRESONITE_IO_SOCKET_DIR~/.resonite-io/; resolution may raise :class:SocketNotFoundError or :class:AmbiguousSocketError.

Source code in src/resoio/_client.py
def __init__(self, socket_path: str | None = None) -> None:
    # Defer resolution to __aenter__ so env vars patched between
    # construction and connection are honoured, and so resolution
    # errors surface at the connect site.
    self._explicit_path: str | None = socket_path
    self._channel: Channel | None = None
    self._stub: TStub | None = None
    self._resolved_path: str | None = None

shutdown async

shutdown() -> ShutdownResponse

Ask the engine to quit gracefully and return the ACK.

The engine schedules Engine.RequestShutdown on its update thread and ACKs before the process tears down, so this returns promptly. It does not wait for the engine to exit; poll liveness (e.g. via :class:resoio.ConnectionClient) if you need to confirm the process is gone, or use :func:shutdown for a one-call stop that also reports the engine PID.

RETURNS DESCRIPTION
The

class:ShutdownResponse whose accepted is True when

TYPE: ShutdownResponse

ShutdownResponse

the shutdown was scheduled, or False when the engine had already

ShutdownResponse

begun shutting down (idempotent no-op).

RAISES DESCRIPTION
GRPCError

The RPC failed at the transport or server layer. Note that the connection may drop right after a successful schedule as the engine exits — callers that only want "make it stop" can treat a post-ACK disconnect as success.

Source code in src/resoio/lifecycle.py
async def shutdown(self) -> ShutdownResponse:
    """Ask the engine to quit gracefully and return the ACK.

    The engine schedules ``Engine.RequestShutdown`` on its update thread
    and ACKs before the process tears down, so this returns promptly. It
    does **not** wait for the engine to exit; poll liveness (e.g. via
    :class:`resoio.ConnectionClient`) if you need to confirm the process is
    gone, or use :func:`shutdown` for a one-call stop that also reports the
    engine PID.

    Returns:
        The :class:`ShutdownResponse` whose ``accepted`` is ``True`` when
        the shutdown was scheduled, or ``False`` when the engine had already
        begun shutting down (idempotent no-op).

    Raises:
        grpclib.exceptions.GRPCError: The RPC failed at the transport or
            server layer. Note that the connection may drop right after a
            successful schedule as the engine exits — callers that only want
            "make it stop" can treat a post-ACK disconnect as success.
    """
    stub = self._require_stub()
    return await stub.shutdown(ShutdownRequest())

resoio.lifecycle.shutdown async

shutdown(*, socket_path: str | None = None) -> int | None

Ask the running Resonite client to quit gracefully; return the engine PID.

Reads the engine's host PID from the Info RPC (for reporting), then sends Lifecycle.Shutdown. The engine schedules its own shutdown and exits asynchronously; no OS signals are sent. Because this is a pure gRPC call it works from anywhere the UDS is reachable (no host-native requirement).

This is cooperative and best-effort: it returns once the engine ACKs the request, not once the process has died, and on Linux the engine may hang during teardown without ever exiting (see the module docstring). Callers that need a guaranteed stop should follow up with :func:resoio.terminate (a forceful kill by PID) when the engine outlives the graceful request.

PARAMETER DESCRIPTION
socket_path

Explicit UDS path for the Info / Lifecycle RPCs; None resolves it via the usual env order.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
int | None

The engine's host PID, or None when no engine was reachable.

Source code in src/resoio/lifecycle.py
async def shutdown(*, socket_path: str | None = None) -> int | None:
    """Ask the running Resonite client to quit gracefully; return the engine
    PID.

    Reads the engine's host PID from the ``Info`` RPC (for reporting), then
    sends ``Lifecycle.Shutdown``. The engine schedules its own shutdown and
    exits asynchronously; no OS signals are sent. Because this is a pure gRPC
    call it works from anywhere the UDS is reachable (no host-native
    requirement).

    This is **cooperative and best-effort**: it returns once the engine ACKs the
    request, not once the process has died, and on Linux the engine may hang
    during teardown without ever exiting (see the module docstring). Callers that
    need a guaranteed stop should follow up with :func:`resoio.terminate` (a
    forceful kill by PID) when the engine outlives the graceful request.

    Args:
        socket_path: Explicit UDS path for the ``Info`` / ``Lifecycle`` RPCs;
            ``None`` resolves it via the usual env order.

    Returns:
        The engine's host PID, or ``None`` when no engine was reachable.
    """
    # Lazy import keeps the module import graph acyclic (info pulls in _client).
    from resoio.info import get_server_info

    try:
        pid = (await get_server_info(socket_path)).resonite_pid
    except Exception as exc:
        _logger.info("No reachable Resonite engine to shut down (%s).", exc)
        return None

    try:
        async with LifecycleClient(socket_path) as client:
            outcome = await client.shutdown()
        _logger.info("Lifecycle.Shutdown accepted=%s", outcome.accepted)
    except Exception as exc:
        # The engine commonly drops the connection as it exits right after
        # ACKing the schedule — benign. We already have the PID to report.
        _logger.info("Lifecycle.Shutdown connection ended (%s); engine exiting.", exc)

    return pid or None