backend/app/services/resource_manager.pypython · @kibot
20/9/2026, 17:22:33"""Gestor de recursos para controlar transcodificaciones concurrentes en NetFlux.
Proporciona:
- Semaforo para limitar transcodificaciones simultaneas
- Monitoreo de uso de CPU y memoria
- Cleanup de procesos zombie
- Logging de uso de recursos
"""
import os
import psutil
import threading
import logging
import signal
import time
from typing import Dict, Optional
from contextlib import contextmanager
from datetime import datetime, timedelta
logger = logging.getLogger(__name__)
class TranscodeResourceManager:
"""Gestor avanzado de recursos para transcodificacion."""
def __init__(self, max_concurrent: int = 2, max_memory_mb: int = 2048):
self.max_concurrent = max_concurrent
self.max_memory_mb = max_memory_mb
self._active_transcodes: Dict[str, Dict] = {}
self._lock = threading.Lock()
self._semaphore = threading.Semaphore(max_concurrent)
self._processes: list = []
def acquire(self, transcode_id: str) -> bool:
acquired = self._semaphore.acquire(blocking=False)
if acquired:
with self._lock:
self._active_transcodes[transcode_id] = {
"started_at": datetime.utcnow(),
"pid": os.getpid(),
"memory_mb": self._get_current_memory_mb(),
}
logger.info(f"Transcode {transcode_id} iniciado. Activos: {len(self._active_transcodes)}")
return acquired
def release(self, transcode_id: str) -> None:
with self._lock:
self._active_transcodes.pop(transcode_id, None)
self._semaphore.release()
logger.info(f"Transcode {transcode_id} liberado. Activos: {len(self._active_transcodes)}")
def _get_current_memory_mb(self) -> float:
try:
process = psutil.Process(os.getpid())
return process.memory_info().rss / 1024 / 1024
except Exception:
return 0.0
def get_active_count(self) -> int:
with self._lock:
return len(self._active_transcodes)
def get_resource_usage(self) -> Dict:
cpu_percent = psutil.cpu_percent(interval=0.1)
mem = psutil.virtual_memory()
with self._lock:
active = len(self._active_transcodes)
return {
"active_transcodes": active,
"max_concurrent": self.max_concurrent,
"cpu_percent": cpu_percent,
"memory_percent": mem.percent,
"memory_used_mb": mem.used / 1024 / 1024,
"memory_total_mb": mem.total / 1024 / 1024,
"transcodes": dict(self._active_transcodes),
}
def cleanup_zombies(self) -> int:
cleaned = 0
for proc in self._processes[:]:
try:
if proc.poll() is not None:
self._processes.remove(proc)
cleaned += 1
except Exception:
pass
if cleaned > 0:
logger.info(f"Limpiados {cleaned} procesos zombie")
return cleaned
@contextmanager
def transcode_slot(self, transcode_id: str = "unknown"):
if not self.acquire(transcode_id):
raise RuntimeError(
f"Maximo de transcodificaciones concurrentes ({self.max_concurrent}) alcanzado."
)
try:
yield self
finally:
self.release(transcode_id)
def wait_for_slot(self, transcode_id: str, timeout: int = 60) -> bool:
start = time.time()
while time.time() - start < timeout:
if self.acquire(transcode_id):
return True
time.sleep(1)
return False
resource_manager = TranscodeResourceManager(
max_concurrent=int(os.getenv("TRANSCODE_MAX_CONCURRENT", "2")),
max_memory_mb=int(os.getenv("TRANSCODE_MAX_MEMORY_MB", "2048")),
)