# tractor: structured concurrent "actors". # Copyright 2018-eternity Tyler Goodlet. # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . ''' Post-mortem subactor cleanup primitives — things the parent runtime has to clean up because the dead-or-SIGKILL'd child couldn't. Sibling of `tractor._testing._reap` which is the test-harness equivalent (orphan-pid + leaked-shm + leaked-UDS-sock sweeper fixtures). This module is the spawn-layer counterpart, called inline from `hard_kill` and the broader subactor reap path. Today this is just `unlink_uds_bind_addrs()`. As future post-mortem cleanup needs surface (e.g. `/dev/shm` segment unlink for hard-crashed actors, leaked-pidfile cleanup), they land here too. Future-work TODO — authoritative UDS bind-addr tracking ------------------------------------------------------- `unlink_uds_bind_addrs()` currently has two cleanup paths: 1. Explicit `bind_addrs` (when parent set them at spawn time) 2. **Convention-based reconstruction** in the platform default UDS bindspace — for the common case where the subactor self-assigned a random sock via `UDSAddress.get_random()`. Path (2) delegates filename reconstruction to `tractor.ipc._uds.UDSAddress.get_sockname()`. If the subactor binds to a non-default `bindspace`/`filedir` — we'll silently fail to unlink. A more authoritative approach would be: - Subactors register their bound UDS sockpaths in a per-process registry inside `tractor.ipc._uds` at `start_listener()` time. - The subactor reports its bound sockpath(s) back to the parent over IPC immediately post-bind (extension to `SpawnSpec` reply / a new handshake msg). - Parent caches the subactor's authoritative sockpaths. - `unlink_uds_bind_addrs()` checks the cache FIRST, falls back to convention-reconstruction if the subactor died before reporting (which is the SIGKILL case this fn primarily exists for). Tracked as future work in #454 (the parent UDS-leak issue this module addresses); a separate issue may be filed if/when the registry impl is scoped. See also #452 — the discovery-client `CLOSE_WAIT` TCP fd leak. Different bug class but same broader theme of "fork-spawn unmasked latent cleanup gaps". ''' from __future__ import annotations import os from pathlib import Path from typing import TYPE_CHECKING import trio from tractor.discovery._addr import ( UnwrappedAddress, wrap_address, ) from tractor.ipc._uds import UDSAddress from tractor.log import get_logger if TYPE_CHECKING: from tractor.runtime._runtime import Actor log = get_logger('tractor') def unlink_uds_bind_addrs( proc: trio.Process, *, bind_addrs: list[UnwrappedAddress] | None = None, subactor: Actor | None = None, ) -> None: ''' Best-effort post-mortem cleanup of any UDS sock-files a hard-killed subactor was bound to. SIGKILL bypasses Python execution → the subactor's `_serve_ipc_eps` `finally:` block (which normally calls `os.unlink(addr.sockpath)`) never runs. Without this parent-side cleanup, the dead subactor's platform-default UDS socket file accumulates on the filesystem (see issue #454 + the autouse `_track_orphaned_uds_per_test` fixture). Two cleanup paths, in order: 1. **Explicit `bind_addrs`** — when the parent set the subactor's bind addrs at spawn time, unlink each UDS-flavored sockpath directly. 2. **Self-assigned reconstruction** — when `bind_addrs` is empty (the common case: subactor picked its own random sock via `UDSAddress.get_random()`), reconstruct the path from `(subactor.aid.name, proc.pid)` using the same `UDSAddress.get_sockname()` helper. We can do this because the subactor uses its OWN `os.getpid()` at bind time, which equals `proc.pid` from the parent's view. Idempotent: `FileNotFoundError` (graceful exit already-unlinked, or sock never bound under early- spawn cancel) is silenced; other `OSError`s log a warning but never raise. TCP / non-UDS bind addrs are skipped. ''' sockpaths: list[str] = [] # path 1: explicit bind_addrs set at spawn time for unwrapped in (bind_addrs or ()): try: addr = wrap_address(unwrapped) except Exception: log.exception( f'Failed to wrap addr for UDS post-kill cleanup ' f'— skipping {unwrapped!r}\n' ) continue if isinstance(addr, UDSAddress): sockpaths.append(str(addr.sockpath)) # path 2: reconstruct from subactor name + proc pid # for the random-self-assign case (bind_addrs=None) # # TODO authoritative tracking — see module docstring. if ( not sockpaths and subactor is not None and proc.pid is not None ): try: sockname: Path = UDSAddress.get_sockname( name=subactor.aid.name, pid=proc.pid, bindspace=UDSAddress.def_bindspace, ) except Exception: log.exception( f'Failed to reconstruct UDS sock-file for ' f'post-kill cleanup — skipping\n' f' |_{proc}\n' f' |_{subactor.aid}\n' ) return sockpath: str = str( UDSAddress.def_bindspace / sockname ) sockpaths.append(sockpath) for sockpath in sockpaths: try: os.unlink(sockpath) log.runtime( f'Unlinked orphaned UDS sock-file post-SIGKILL\n' f' |_{proc}\n' f' |_{sockpath}\n' ) except FileNotFoundError: # raced — subactor cleaned up before SIGKILL, # OR sockfile never bound (early-spawn cancel), # OR transport wasn't UDS this run. pass except OSError as exc: log.warning( f'Failed to unlink subactor UDS sock-file ' f'post-SIGKILL\n' f' |_{proc}\n' f' |_{sockpath}\n' f' |_{exc!r}\n' )