Ctrl K
ComunidadGuías…

app/lifecycle.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Restarting the application from inside it.

A process cannot restart itself; it can stop and rely on whatever started it.
`supervision()` reports which of three situations this is — Kubernetes, Docker
with a restart policy, or neither — and the page words the button accordingly
rather than pretending they are the same (decisions.md #74).

The stop is a SIGTERM to our own process from a background thread, a moment
after the response has gone out, so uvicorn shuts down gracefully and the
browser has the page telling it what happened.
"""

from __future__ import annotations

import logging
import os
import signal
import threading
from dataclasses import dataclass
from pathlib import Path

log = logging.getLogger(__name__)

# Long enough for the response to be written and the socket flushed, short
# enough that nobody wonders whether the button worked.
STOP_DELAY_SECONDS = 0.75


@dataclass(frozen=True)
class Supervision:
    """Whether stopping will result in starting again."""

    restarts: bool          # will something bring us back?
    certain: bool           # …and do we know that, or are we inferring it?
    platform: str           # what we think we are running under
    detail: str             # the sentence shown next to the button


def _in_container() -> bool:
    if Path("/.dockerenv").exists():
        return True
    # cgroup v1 names the runtime in the path; v2 exposes this file only inside
    # a container. Either is a good enough signal, and neither is load-bearing:
    # being wrong downgrades the wording, it does not break anything.
    try:
        return "docker" in Path("/proc/1/cgroup").read_text(encoding="utf-8") or Path(
            "/run/.containerenv").exists()
    except OSError:
        return False


def supervision() -> Supervision:
    if os.environ.get("KUBERNETES_SERVICE_HOST"):
        return Supervision(
            restarts=True, certain=True, platform="Kubernetes",
            detail="Kubernetes starts a replacement immediately, so the "
                   "application will be back within a few seconds.",
        )
    if _in_container():
        return Supervision(
            restarts=True, certain=False, platform="a container",
            detail="Your container runtime will start it again if this "
                   "container has a restart policy — `restart: unless-stopped` "
                   "in a compose file, or `--restart` on `docker run`. Without "
                   "one, the application will stop and stay stopped.",
        )
    return Supervision(
        restarts=False, certain=True, platform="a plain process",
        detail="Nothing here will start it again: this looks like the "
               "application running directly rather than under a container "
               "runtime or service manager. Restarting will stop it, and you "
               "will need to start it yourself.",
    )


def request_stop(reason: str) -> None:
    """Ask this process to shut down gracefully, a moment from now.

    Separated out so tests can replace it: calling the real one under pytest
    would take the test runner with it.
    """
    log.info("restart requested (%s); stopping in %.2fs", reason, STOP_DELAY_SECONDS)

    def _stop() -> None:  # pragma: no cover - sends SIGTERM to this very process; running it under pytest would kill the test session. Verified by hand instead, and `request_stop` is stubbed wherever a route is tested.
        # SIGTERM rather than sys.exit: this runs on a worker thread, and only
        # a signal reaches uvicorn's shutdown handling from here.
        os.kill(os.getpid(), signal.SIGTERM)

    timer = threading.Timer(STOP_DELAY_SECONDS, _stop)
    timer.daemon = True
    timer.start()