#!/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