""" T19 DeadNode Reaper — cleans up orphaned iceoryx2 segments. Per T19 ANNEX B pre-condition #1: "DeadNode reaper (or minimum orphan-sweep on host start) — cleanup currently leans on Drop; crashed nodes orphan iox2 segments. Small task, mandatory." This is a mandatory pre-condition before prod reliance on iceoryx2 transport. """ from __future__ import annotations import logging import os import time from typing import List, Optional LOGGER = logging.getLogger("malkhut.clock.deadnode") class DeadNodeReaper: """ Sweeps orphaned iceoryx2 segments on host start. iceoryx2 segments are backed by files in /dev/shm/ or a configured path. When a node crashes, its Drop destructor may not run, leaving orphans. This reaper: 1. Lists all iox2-managed segments 2. Checks if the owning process is alive 3. Removes segments whose owner is dead Must run before any iceoryx2 service starts. """ def __init__(self, shm_path: str = "/dev/shm", prefix: str = "iox_") -> None: self._shm_path = shm_path self._prefix = prefix self._reaped_count: int = 0 def sweep(self) -> List[str]: """ Sweep orphaned segments. Returns list of removed segment paths. Call this on host start before any iceoryx2 service. """ removed: List[str] = [] try: entries = os.listdir(self._shm_path) except OSError as e: LOGGER.warning("Cannot list %s: %s", self._shm_path, e) return removed for entry in entries: if not entry.startswith(self._prefix): continue path = os.path.join(self._shm_path, entry) # Extract PID from segment name if possible # iceoryx2 segment names: iox2_{service}_{uid}_{id} # We check if any process holds the segment open if self._is_orphaned(path): try: os.unlink(path) self._reaped_count += 1 removed.append(path) LOGGER.info("Reaped orphaned segment: %s", path) except OSError as e: LOGGER.warning("Failed to reap %s: %s", path, e) return removed def _is_orphaned(self, path: str) -> bool: """ Check if a segment file is orphaned. Simplified check: if the file is older than a threshold and no process has it open, it's orphaned. A production implementation would use /proc/{pid}/fd to check open file descriptors. """ try: stat = os.stat(path) age_s = time.time() - stat.st_mtime # Segments older than 300s with no recent access are suspicious if age_s > 300: return True except OSError: return False return False @property def reaped_count(self) -> int: return self._reaped_count