Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| """Idle-session reaper for Gradio demo apps. | |
| Gradio's ``demo.unload()`` callback is JavaScript-based and won't fire on | |
| browser crash, network drop, or mobile tab kill. This module runs a | |
| background thread that closes sessions after a configurable period of | |
| inactivity, preventing orphaned sessions from inflating metrics. | |
| """ | |
| import threading | |
| import time | |
| from collections.abc import Callable | |
| class SessionReaper: | |
| """Track per-session activity and reap idle sessions. | |
| Args: | |
| timeout_s: Seconds of inactivity before a session is reaped. | |
| on_expired: Called (outside any lock) with the session hash of each | |
| reaped session. The callback is responsible for any cleanup | |
| (decrementing counters, logging ``session_end``, etc.). | |
| check_interval_s: How often the reaper thread wakes up to scan. | |
| """ | |
| def __init__( | |
| self, | |
| timeout_s: float, | |
| on_expired: Callable[[str], None], | |
| check_interval_s: float = 30.0, | |
| ) -> None: | |
| self._timeout_s = timeout_s | |
| self._on_expired = on_expired | |
| self._check_interval_s = check_interval_s | |
| self._sessions: dict[str, float] = {} | |
| self._lock = threading.Lock() | |
| self._shutting_down = False | |
| self._thread = threading.Thread(target=self._run, daemon=True) | |
| self._thread.start() | |
| def add(self, session_hash: str) -> None: | |
| """Register a new session and record its initial activity.""" | |
| with self._lock: | |
| self._sessions[session_hash] = time.monotonic() | |
| def remove(self, session_hash: str) -> bool: | |
| """Unregister a session. Returns ``True`` if it was tracked.""" | |
| with self._lock: | |
| return self._sessions.pop(session_hash, None) is not None | |
| def touch(self, session_hash: str) -> bool: | |
| """Update last-activity timestamp. Returns ``False`` if unknown.""" | |
| with self._lock: | |
| if session_hash not in self._sessions: | |
| return False | |
| self._sessions[session_hash] = time.monotonic() | |
| return True | |
| def is_active(self, session_hash: str) -> bool: | |
| with self._lock: | |
| return session_hash in self._sessions | |
| def active_count(self) -> int: | |
| with self._lock: | |
| return len(self._sessions) | |
| def shutdown(self) -> None: | |
| """Signal the reaper thread to stop.""" | |
| self._shutting_down = True | |
| def _run(self) -> None: | |
| while not self._shutting_down: | |
| time.sleep(self._check_interval_s) | |
| now = time.monotonic() | |
| with self._lock: | |
| stale = [ | |
| sh | |
| for sh, ts in self._sessions.items() | |
| if now - ts >= self._timeout_s | |
| ] | |
| for sh in stale: | |
| del self._sessions[sh] | |
| # Callbacks outside the lock to avoid deadlock with the caller. | |
| for sh in stale: | |
| self._on_expired(sh) | |