UVClock (clock/host.py): T19 event-driven reactor, asyncio dispatch. Events (clock/events.py): Scan, Tick, Timer, BarFire, Stale. Staleness watchdog (clock/staleness.py): T19 Staleness Law enforcement. DeadNode reaper (clock/deadnode.py): iox2 orphan sweep on startup.
95 lines
2.9 KiB
Python
95 lines
2.9 KiB
Python
"""
|
|
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
|