Files
sentiment-engine/MALKHUT/malkhut/clock/deadnode.py

95 lines
2.9 KiB
Python
Raw Normal View History

"""
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