Files
hermes-agent/gateway/readiness.py
kshitijk4poor 1572836184 perf(gateway): parse readiness and bundled-manifest YAML with the C loader
Same class as the config/manifest loader swap earlier in this stack. The
/ready probe re-read and pure-Python-parsed config.yaml on every poll, inside
the aiohttp handler (110 KB seeded config: 2332 -> 487 ms per probe on a
loaded runner). It now uses utils.load_yaml_file_readonly: the C loader, and
repeat probes reuse the parse until the file signature changes. Parse errors
are not cached, so an edit that breaks or fixes the file shows on the next
probe. The bundled-platform manifest reader was the one startup manifest
reader left on yaml.safe_load. managed_scope imports fast_safe_load at module
level next to file_signature instead of lazily.
2026-09-23 21:26:49 +05:30

123 lines
5.5 KiB
Python

"""Bounded, non-destructive readiness probes for authenticated health surfaces."""
from __future__ import annotations
import shutil
import sqlite3
from contextlib import closing
from pathlib import Path
from typing import Any
from hermes_constants import get_hermes_home
from utils import load_yaml_file_readonly
_DISK_DEGRADED_PERCENT = 90.0
_CONNECTED_STATES = {"connected", "running", "ok"}
def _check(status: str, detail: str | None = None, **extra: Any) -> dict[str, Any]:
return {"status": status, **({"detail": detail} if detail else {}), **extra}
def _probe_state_db(home: Path) -> dict[str, Any]:
"""Read-only schema probe plus the process-wide corruption latch (``hermes_state_health``).
The schema read only catches an unreadable header or schema; damage deeper in the file
surfaces when a reader or writer touches it, and those publish into the latch. Reporting
the latch here is what makes readiness and ``/api/status`` agree with the session list
(#72046). ``detail="corrupt"`` is the one reason string consumers key off."""
from hermes_state_health import STORAGE_CORRUPT, note_storage_error, storage_state
path = home / "state.db"
if not path.exists():
return _check("ok", "not initialized")
if storage_state(path) == STORAGE_CORRUPT:
return _check("degraded", STORAGE_CORRUPT)
try:
# Read-only schema query: catches unreadable/corrupt DBs without competing with
# writers. ``closing`` is required — sqlite3's context manager only commits/rolls
# back, never closes, so a bare ``with connect()`` leaks a connection per poll.
with closing(sqlite3.connect(f"file:{path.as_posix()}?mode=ro", uri=True, timeout=1.0)) as conn:
# A readiness probe must never compete with normal state writers. See #69567, #69678.
conn.execute("PRAGMA query_only = ON")
conn.execute("SELECT name FROM sqlite_master LIMIT 1").fetchone()
return _check("ok")
except Exception as exc:
if note_storage_error(path, exc):
return _check("degraded", STORAGE_CORRUPT)
return _check("degraded", type(exc).__name__)
def _probe_config(home: Path) -> dict[str, Any]:
path = home / "config.yaml"
if not path.exists():
return _check("ok", "using defaults")
try:
raw = load_yaml_file_readonly(path)
except Exception as exc:
return _check("degraded", f"invalid config ({type(exc).__name__})")
return _check("ok") if raw is None or isinstance(raw, dict) else _check("degraded", "top level is not a mapping")
def _probe_disk(home: Path) -> dict[str, Any]:
try:
usage = shutil.disk_usage(home)
except Exception as exc:
return _check("degraded", type(exc).__name__)
used_pct = round((usage.used / usage.total) * 100, 1) if usage.total else 0.0
return _check("degraded" if used_pct >= _DISK_DEGRADED_PERCENT else "ok", used_percent=used_pct, free_bytes=usage.free)
def _probe_gateway(runtime_status: dict[str, Any]) -> dict[str, Any]:
state = str(runtime_status.get("gateway_state") or "unknown")
platforms = runtime_status.get("platforms")
platforms = platforms if isinstance(platforms, dict) else {}
connected = sum(
isinstance(v, dict) and str(v.get("state") or v.get("status") or "").lower() in _CONNECTED_STATES
for v in platforms.values()
)
return _check("ok" if state in {"running", "draining"} else "degraded", state=state,
connected_platforms=connected, platforms=len(platforms))
def _probe_session_store(runtime_status: dict[str, Any], state_db_probe: dict[str, Any]) -> dict[str, Any]:
"""Report the running gateway cache state, not an independent reopen. A corrupt store is
unavailable whatever the cache says: an open handle on a damaged file is not a working one."""
if state_db_probe.get("detail") == "corrupt":
return _check("unavailable", "corrupt")
runtime_store = runtime_status.get("session_store")
state = str(runtime_store.get("status") or "unknown") if isinstance(runtime_store, dict) else ""
if state in {"ok", "unavailable", "retrying"}:
return _check(state)
# Older gateways publish no cache state: fall back to the state_db probe.
return _check("ok" if state_db_probe.get("status") == "ok" else "unavailable")
def collect_runtime_readiness(
*, configured_model: str, runtime_status: dict[str, Any] | None, active_api_runs: int = 0,
process_completion_queue_depth: int = 0, active_delegations: int = 0,
) -> dict[str, Any]:
"""Bounded readiness diagnostics, no runtime mutation. Even authenticated, probes
expose status and counts only: never config values, credentials, paths, payloads."""
home = get_hermes_home()
runtime = runtime_status if isinstance(runtime_status, dict) else {}
state_db_probe = _probe_state_db(home)
checks = {
"state_db": state_db_probe,
"session_store": _probe_session_store(runtime, state_db_probe),
"config": _probe_config(home),
"model": _check("ok" if str(configured_model or "").strip() else "degraded"),
"disk": _probe_disk(home),
"gateway": _probe_gateway(runtime),
"background_queues": _check(
"ok", active_api_runs=max(0, int(active_api_runs)),
process_completions=max(0, int(process_completion_queue_depth)),
active_delegations=max(0, int(active_delegations)),
),
}
return {"status": "ok" if all(c.get("status") == "ok" for c in checks.values()) else "degraded", "checks": checks}
__all__ = ["collect_runtime_readiness"]