AutoTeam-F / src /autoteam /runtime_resources.py
ZRainbow's picture
Harden AutoTeam runtime and free registration
90694b4
Raw
History Blame Contribute Delete
5.49 kB
"""Lightweight runtime resource probes for the AutoTeam service container."""
from __future__ import annotations
import gc
import os
import subprocess
from pathlib import Path
from typing import Any
PROC_SELF_STATUS = Path("/proc/self/status")
CGROUP_MEMORY_CURRENT = Path("/sys/fs/cgroup/memory.current")
CGROUP_MEMORY_MAX = Path("/sys/fs/cgroup/memory.max")
CGROUP_PIDS_CURRENT = Path("/sys/fs/cgroup/pids.current")
CGROUP_PIDS_MAX = Path("/sys/fs/cgroup/pids.max")
def _read_text(path: Path) -> str | None:
try:
return path.read_text(encoding="utf-8").strip()
except OSError:
return None
def _read_int(path: Path) -> int | None:
text = _read_text(path)
if not text or text == "max":
return None
try:
return int(text)
except ValueError:
return None
def _read_proc_rss_bytes() -> int | None:
text = _read_text(PROC_SELF_STATUS)
if not text:
return None
for line in text.splitlines():
if not line.startswith("VmRSS:"):
continue
parts = line.split()
if len(parts) >= 2:
try:
return int(parts[1]) * 1024
except ValueError:
return None
return None
def _process_rows() -> list[str]:
try:
result = subprocess.run(
["ps", "-eo", "state=,comm=,args="],
check=False,
capture_output=True,
text=True,
timeout=2,
)
except (OSError, subprocess.SubprocessError):
return []
if result.returncode != 0:
return []
return [line.strip() for line in result.stdout.splitlines() if line.strip()]
def _count_browser_processes(rows: list[str]) -> dict[str, int]:
total = 0
live = 0
zombie = 0
for row in rows:
lower = row.lower()
if "chrome" not in lower and "chromium" not in lower and "playwright" not in lower:
continue
total += 1
if row[:1] == "Z":
zombie += 1
else:
live += 1
return {
"browser_process_total": total,
"browser_process_live": live,
"browser_process_zombie": zombie,
}
def _bytes_to_mb(value: int | None) -> float | None:
if value is None:
return None
return round(value / 1024 / 1024, 1)
def _float_env(name: str, default: float) -> float:
try:
return float(os.environ.get(name, str(default)))
except ValueError:
return default
def _int_env(name: str, default: int) -> int:
try:
return int(os.environ.get(name, str(default)))
except ValueError:
return default
def collect_runtime_resource_snapshot() -> dict[str, Any]:
"""Collect memory and browser-process counters without external dependencies."""
rss_bytes = _read_proc_rss_bytes()
memory_current_bytes = _read_int(CGROUP_MEMORY_CURRENT)
memory_max_bytes = _read_int(CGROUP_MEMORY_MAX)
pids_current = _read_int(CGROUP_PIDS_CURRENT)
pids_max = _read_int(CGROUP_PIDS_MAX)
process_counts = _count_browser_processes(_process_rows())
memory_usage_ratio = None
if memory_current_bytes is not None and memory_max_bytes:
memory_usage_ratio = memory_current_bytes / memory_max_bytes
return {
"rss_mb": _bytes_to_mb(rss_bytes),
"cgroup_memory_mb": _bytes_to_mb(memory_current_bytes),
"cgroup_memory_limit_mb": _bytes_to_mb(memory_max_bytes),
"cgroup_memory_usage_ratio": memory_usage_ratio,
"pids_current": pids_current,
"pids_max": pids_max,
**process_counts,
}
def _fmt_mb(value: float | None) -> str:
if value is None:
return "unknown"
return f"{value:.1f}MiB"
def _fmt_ratio(value: float | None) -> str:
if value is None:
return "unknown"
return f"{value * 100:.1f}%"
def log_runtime_resource_snapshot(logger: Any, *, label: str = "runtime") -> dict[str, Any]:
"""Log a resource snapshot and run GC when memory is near the cgroup limit."""
snapshot = collect_runtime_resource_snapshot()
logger.info(
"[资源] %s rss=%s cgroup=%s/%s usage=%s pids=%s/%s browser_live=%d browser_zombie=%d",
label,
_fmt_mb(snapshot["rss_mb"]),
_fmt_mb(snapshot["cgroup_memory_mb"]),
_fmt_mb(snapshot["cgroup_memory_limit_mb"]),
_fmt_ratio(snapshot["cgroup_memory_usage_ratio"]),
snapshot["pids_current"] if snapshot["pids_current"] is not None else "unknown",
snapshot["pids_max"] if snapshot["pids_max"] is not None else "unknown",
snapshot["browser_process_live"],
snapshot["browser_process_zombie"],
)
ratio = snapshot["cgroup_memory_usage_ratio"]
warn_ratio = _float_env("AUTOTEAM_MEMORY_WARN_RATIO", 0.85)
if ratio is not None and ratio >= warn_ratio:
collected = gc.collect()
logger.warning(
"[资源] %s cgroup memory usage %s >= %.1f%%, gc.collect reclaimed %d objects",
label,
_fmt_ratio(ratio),
warn_ratio * 100,
collected,
)
zombie_threshold = _int_env("AUTOTEAM_ZOMBIE_WARN_THRESHOLD", 20)
if snapshot["browser_process_zombie"] >= zombie_threshold:
logger.warning(
"[资源] %s browser zombie processes=%d >= %d; container init/reaper should be enabled",
label,
snapshot["browser_process_zombie"],
zombie_threshold,
)
return snapshot