"""PID-namespace identity for state.db lock/lease holders. A ``pid=`` holder is relative to its writer's PID namespace; a reader in a sibling namespace (two containers on one volume, systemd ``PrivatePIDs=``) sees a live holder as absent and would reclaim its unexpired row. Holders are stamped with ``pidns=`` and a foreign stamp is never probed. Unstamped (pre-upgrade) records differ by policy: * :func:`holder_pid_checkable` — STRICT, for TTL rows (compression locks, turn leases): unstamped defers to its expiry (<= TTL; a false defer self-heals, a false reclaim ends a live turn). * :func:`persistent_record_pidns_checkable` — LEGACY, for no-expiry flock records: unstamped keeps probing so orphaned-lock cleanup still works. Platforms without PID namespaces (macOS, Windows) keep the plain pid probe; a failed Linux lookup is not cached and makes every holder unverifiable. """ from __future__ import annotations import os import re import sys from typing import NamedTuple, Optional # ``pidns=`` token inside a structured holder string (``pid=123:pidns=4026533184:turn=…``). _PIDNS_TOKEN_RE = re.compile(r"(?:^|:)pidns=(\d+)(?::|$)") class LocalPidNamespace(NamedTuple): """What this process knows about its own PID namespace. Three states, and the ``checkable`` predicates treat each differently: ``supported`` False — the platform has no namespace identity at all; ``supported`` True with an ``id`` — resolved; ``supported`` True with an ``id`` of None — resolution failed, see the module docstring. """ id: Optional[str] supported: bool _UNSUPPORTED = LocalPidNamespace(None, False) # Cached once there is a definite answer: a process cannot move itself into # another PID namespace (``setns`` applies to its future children), so a # successful read never changes for us. A failed read is NOT cached — it may # be transient, and while it lasts every structured holder is unverifiable. _LOCAL_PID_NS: Optional[LocalPidNamespace] = None def _resolve_local_pid_namespace() -> LocalPidNamespace: if sys.platform != "linux": return _UNSUPPORTED try: link = os.readlink("/proc/self/ns/pid") except OSError: return LocalPidNamespace(None, True) match = re.search(r"\[(\d+)\]", link) # "pid:[4026532534]" return LocalPidNamespace(match.group(1) if match else None, True) def _local_pid_namespace() -> LocalPidNamespace: """This process' PID-namespace identity (see :class:`LocalPidNamespace`).""" global _LOCAL_PID_NS if _LOCAL_PID_NS is None: local = _resolve_local_pid_namespace() if local.id is not None or not local.supported: _LOCAL_PID_NS = local return local return _LOCAL_PID_NS def pid_namespace_id() -> Optional[str]: """What a holder records: the ``/proc/self/ns/pid`` inode on Linux, ``None`` where there is no namespace identity or the lookup failed. A holder never asserts a namespace it cannot prove.""" return _local_pid_namespace().id def holder_namespace_token() -> str: """The ``:pidns=`` fragment to append to a new holder string, or ``""``. Callers build ``f"pid={os.getpid()}{holder_namespace_token()}:turn=…"``. """ ns = pid_namespace_id() return f":pidns={ns}" if ns else "" def _recorded_namespace(holder: str) -> Optional[str]: """The ``pidns=`` stamp from a holder string, or None when it carries none.""" match = _PIDNS_TOKEN_RE.search(holder or "") return match.group(1) if match else None def _qualify(recorded: Optional[str], *, unstamped_checkable: bool) -> bool: """The shared matrix; *unstamped_checkable* is the policy for a record that carries no namespace (see the module docstring for why there are two).""" local = _local_pid_namespace() if not local.supported: return True # single-namespace platform: a pid is evidence on its own if recorded is None: return unstamped_checkable if local.id is None: return False # our own lookup failed: unknown authority is not authority return recorded == local.id def holder_pid_checkable(holder: str) -> bool: """STRICT: may a structured holder string's ``pid=`` be probed here? The policy for TTL-bounded rows (compression locks, session turn leases): a foreign-namespace or unstamped holder is never probed — it defers to the row's own expiry instead (at most the remaining TTL; a false defer self-heals, a false reclaim ends a live turn). """ return _qualify(_recorded_namespace(holder), unstamped_checkable=False) def persistent_record_pidns_checkable(recorded: Optional[str]) -> bool: """LEGACY ROLLOUT: may a persistent record (no expiry) be probed here? The policy for the flock holder records: a foreign-namespace record is never probed, while an unstamped one keeps main's behavior — those records never expire, so refusing to probe them would permanently disable orphaned-lock cleanup (see the module docstring). """ return _qualify(recorded, unstamped_checkable=True)