Spaces:
Sleeping
Sleeping
Cyber Catalyst Team
feat: integrate virtual multi-repo second brain, context engine (ACE), watchdog, and quantized llama-cpp SwarmLLM
12ab90a | # -*- coding: utf-8 -*- | |
| """ | |
| survival_watchdog.py β HF Space Survival & Resource Monitor | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Two threats to a Hugging Face Free Space: | |
| 1. OOM crash (RAM > 16 GB β container killed without warning) | |
| 2. Idle sleep (HF puts a Space to sleep after ~48h of inactivity; | |
| the wake-up latency is 30-60 seconds, breaking loops) | |
| This module runs as a background asyncio task and: | |
| - Monitors psutil every 60 s | |
| - If RAM > RAM_KILL_THRESHOLD: kills the heaviest non-essential process | |
| - If CPU < CPU_IDLE_THRESHOLD for > IDLE_GRACE_MINUTES: fires a | |
| compute spike (1M-iteration sum) to reset HF's idle timer | |
| - Exposes live metrics as a JSON-serialisable dict for /api/metrics | |
| """ | |
| import os | |
| import asyncio | |
| import logging | |
| import time | |
| from typing import Dict, Any | |
| import psutil | |
| logger = logging.getLogger("survival_watchdog") | |
| RAM_KILL_THRESHOLD = float(os.environ.get("WD_RAM_KILL_PCT", "88")) # % | |
| CPU_IDLE_THRESHOLD = float(os.environ.get("WD_CPU_IDLE_PCT", "4")) # % | |
| IDLE_GRACE_MINUTES = int(os.environ.get("WD_IDLE_GRACE_MIN", "8")) # minutes | |
| CHECK_INTERVAL_SECS = int(os.environ.get("WD_CHECK_SECS", "60")) # seconds | |
| # Processes that should NEVER be killed (guarded by name prefix) | |
| PROTECTED_PROCESS_NAMES = { | |
| "python", "uvicorn", "node", "npm", "git", "bash", "sh", | |
| } | |
| _metrics_snapshot: Dict[str, Any] = {} | |
| def get_metrics() -> Dict[str, Any]: | |
| """Return the latest resource snapshot (called by /api/metrics endpoint).""" | |
| return _metrics_snapshot | |
| class SurvivalWatchdog: | |
| def __init__(self, own_pid: int = None): | |
| self.own_pid = own_pid or os.getpid() | |
| self._idle_ticks = 0 # consecutive checks where CPU < threshold | |
| # ββ Core Monitor Loop βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def run(self): | |
| """Main loop β runs forever as an asyncio background task.""" | |
| logger.info("[Watchdog] Started. RAM kill threshold: %.0f%% | CPU idle threshold: %.0f%%", | |
| RAM_KILL_THRESHOLD, CPU_IDLE_THRESHOLD) | |
| while True: | |
| try: | |
| await self._check() | |
| except Exception as e: | |
| logger.error(f"[Watchdog] Error in check loop: {e}") | |
| await asyncio.sleep(CHECK_INTERVAL_SECS) | |
| async def _check(self): | |
| global _metrics_snapshot | |
| # ββ Collect βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| vm = psutil.virtual_memory() | |
| cpu = psutil.cpu_percent(interval=1) | |
| disk = psutil.disk_usage("/tmp") | |
| net = psutil.net_io_counters() | |
| ram_used_gb = vm.used / (1024 ** 3) | |
| ram_total_gb = vm.total / (1024 ** 3) | |
| ram_pct = vm.percent | |
| _metrics_snapshot = { | |
| "cpu_percent": round(cpu, 1), | |
| "ram_used_gb": round(ram_used_gb, 2), | |
| "ram_total_gb": round(ram_total_gb, 2), | |
| "ram_percent": round(ram_pct, 1), | |
| "ram_free_gb": round((vm.total - vm.used) / (1024 ** 3), 2), | |
| "disk_used_gb": round(disk.used / (1024 ** 3), 2), | |
| "disk_free_gb": round(disk.free / (1024 ** 3), 2), | |
| "net_sent_mb": round(net.bytes_sent / (1024 ** 2), 1), | |
| "net_recv_mb": round(net.bytes_recv / (1024 ** 2), 1), | |
| "timestamp": time.time(), | |
| "idle_ticks": self._idle_ticks, | |
| "status": "ok", | |
| } | |
| # ββ OOM Defence βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if ram_pct >= RAM_KILL_THRESHOLD: | |
| logger.warning( | |
| "[Watchdog] β RAM at %.1f%% (%.2f/%.2f GB) β initiating OOM defence.", | |
| ram_pct, ram_used_gb, ram_total_gb | |
| ) | |
| killed = self._kill_heaviest_safe() | |
| _metrics_snapshot["oom_kill"] = killed | |
| _metrics_snapshot["status"] = "oom_defence" | |
| # ββ Idle Sleep Defence ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| idle_grace_ticks = (IDLE_GRACE_MINUTES * 60) // CHECK_INTERVAL_SECS | |
| if cpu < CPU_IDLE_THRESHOLD: | |
| self._idle_ticks += 1 | |
| else: | |
| self._idle_ticks = 0 | |
| if self._idle_ticks >= idle_grace_ticks: | |
| logger.info( | |
| "[Watchdog] Space has been idle for ~%d min β firing CPU wake-up spike.", | |
| IDLE_GRACE_MINUTES | |
| ) | |
| await self._cpu_spike() | |
| self._idle_ticks = 0 | |
| _metrics_snapshot["status"] = "wake_spike_fired" | |
| logger.debug( | |
| "[Watchdog] CPU=%.1f%% | RAM=%.1f%% (%.2fGB free) | Idle ticks=%d", | |
| cpu, ram_pct, _metrics_snapshot["ram_free_gb"], self._idle_ticks | |
| ) | |
| # ββ OOM Kill βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _kill_heaviest_safe(self) -> str: | |
| """ | |
| Finds the non-protected process using the most RAM and kills it. | |
| Returns a string description of what was killed (or 'none'). | |
| """ | |
| candidates = [] | |
| for proc in psutil.process_iter(["pid", "name", "memory_percent"]): | |
| try: | |
| info = proc.info | |
| pid = info["pid"] | |
| name = (info["name"] or "").lower() | |
| mem = info["memory_percent"] or 0.0 | |
| if pid == self.own_pid: | |
| continue | |
| if any(name.startswith(p) for p in PROTECTED_PROCESS_NAMES): | |
| continue | |
| candidates.append((mem, pid, name)) | |
| except (psutil.NoSuchProcess, psutil.AccessDenied): | |
| pass | |
| if not candidates: | |
| logger.warning("[Watchdog] No safe kill candidates found β RAM is used by protected processes.") | |
| return "none" | |
| candidates.sort(reverse=True) | |
| mem_pct, pid, name = candidates[0] | |
| try: | |
| psutil.Process(pid).kill() | |
| logger.warning("[Watchdog] Killed '%s' (PID %d, %.1f%% RAM) for OOM defence.", name, pid, mem_pct) | |
| return f"{name}:{pid}" | |
| except Exception as e: | |
| logger.error(f"[Watchdog] Failed to kill PID {pid}: {e}") | |
| return "kill_failed" | |
| # ββ CPU Spike (Idle Prevention) βββββββββββββββββββββββββββββββββββββββββββ | |
| async def _cpu_spike(self): | |
| """ | |
| Runs a CPU-bound task in an executor so it doesn't block the event loop. | |
| Uses a 1-million-iteration sum β takes ~50 ms on 2 vCPUs. | |
| Just enough to reset HF's idle detector without burning quota. | |
| """ | |
| loop = asyncio.get_event_loop() | |
| await loop.run_in_executor(None, lambda: sum(i * i for i in range(1_000_000))) | |
| logger.debug("[Watchdog] CPU spike complete.") | |