atlas-iac/services/hermes/scripts/ai_usage_polling.py

106 lines
3.5 KiB
Python
Raw Normal View History

2026-08-23 00:14:58 -03:00
#!/usr/bin/env python3
"""Bounded polling lifecycle and progress health for the AI usage exporter."""
from __future__ import annotations
import threading
import time
from typing import Any
POLL_STARTUP_GRACE_SECONDS = 45
POLL_PROGRESS_BUDGET_SECONDS = 120
class PollingEngine:
"""Run isolated provider polls and track bounded forward progress."""
def __init__(
self,
collector: Any,
*,
interval: float,
startup_grace: float = POLL_STARTUP_GRACE_SECONDS,
progress_timeout: float | None = None,
clock: Any = time.monotonic,
) -> None:
self.collector = collector
self.interval = interval
self.startup_grace = startup_grace
self.progress_timeout = (
interval + POLL_PROGRESS_BUDGET_SECONDS
if progress_timeout is None
else progress_timeout
)
self._clock = clock
self._started_at = clock()
self._last_progress: float | None = None
self._progress_lock = threading.Lock()
self._thread: threading.Thread | None = None
def _mark_progress(self) -> None:
"""Record completion of one bounded provider attempt."""
with self._progress_lock:
self._last_progress = self._clock()
def poll_once(self) -> None:
"""Refresh every provider even if another provider fails unexpectedly."""
for provider in ("openai", "anthropic"):
started = time.time()
monotonic_started = time.monotonic()
try:
self.collector.refresh_provider(provider)
except Exception as error:
print(
f"{provider} quota collection isolated: {type(error).__name__}",
flush=True,
)
try:
self.collector.record_failure(
provider,
started=started,
monotonic_started=monotonic_started,
)
except Exception as record_error:
print(
f"{provider} quota failure accounting deferred: "
f"{type(record_error).__name__}",
flush=True,
)
finally:
self._mark_progress()
def run(self) -> None:
"""Poll forever without allowing a cycle-level exception to stop the thread."""
while True:
try:
self.poll_once()
except Exception as error:
print(
f"quota polling cycle deferred: {type(error).__name__}", flush=True
)
time.sleep(self.interval)
def start(self) -> None:
"""Start the daemon poller exactly once."""
if self._thread is not None:
return
self._thread = threading.Thread(
target=self.run,
name="ai-usage-poller",
daemon=True,
)
self._thread.start()
def is_healthy(self) -> bool:
"""Report thread liveness and progress, independent of provider success."""
thread = self._thread
if thread is None or not thread.is_alive():
return False
now = self._clock()
with self._progress_lock:
last_progress = self._last_progress
if last_progress is None:
return now - self._started_at <= self.startup_grace
return now - last_progress <= self.progress_timeout