From 1ec05ea289add670d853f4eda90d2b2814b56014 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Thu, 29 Jan 2026 23:57:23 +0000 Subject: [PATCH] feat(dashboard): auto-stop server after 5 minutes idle Replace pause-on-idle with full server shutdown after IDLE_SHUTDOWN_SECONDS (default 5 minutes). Next visitor gets a fresh simulation instance. - Idle checker stops server and clears st.cache_resource - init_session_state detects stopped server and recreates fresh state - Clears instruments and history for clean restart Configurable via IDLE_SHUTDOWN_SECONDS environment variable. Co-Authored-By: Claude Opus 4.5 --- src/py_dvt_ate/app/dashboard/app.py | 57 +++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/py_dvt_ate/app/dashboard/app.py b/src/py_dvt_ate/app/dashboard/app.py index 00d5587..68fe1e1 100644 --- a/src/py_dvt_ate/app/dashboard/app.py +++ b/src/py_dvt_ate/app/dashboard/app.py @@ -28,26 +28,49 @@ from py_dvt_ate.tests.thermal.tempco import TempCoTest _test_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="test_runner") -# Idle pause configuration -# Physics engine pauses after IDLE_PAUSE_SECONDS of no activity (default: 30s) -IDLE_PAUSE_SECONDS = int(os.environ.get("IDLE_PAUSE_SECONDS", "30")) +# Idle shutdown configuration +# Server stops after IDLE_SHUTDOWN_SECONDS of no activity (default: 5 minutes) +# Next visitor gets a fresh simulation instance +IDLE_SHUTDOWN_SECONDS = int(os.environ.get("IDLE_SHUTDOWN_SECONDS", "300")) _last_activity_time: float = time.time() _idle_checker_started = False _server_ref: SimulationServer | None = None # Reference for idle checker thread def _idle_checker() -> None: - """Background thread that pauses physics when idle.""" + """Background thread that stops server when idle.""" global _last_activity_time, _server_ref while True: - time.sleep(5) # Check every 5 seconds + time.sleep(10) # Check every 10 seconds if _server_ref is None: continue idle_seconds = time.time() - _last_activity_time - if idle_seconds > IDLE_PAUSE_SECONDS and not _server_ref.paused: - _server_ref.paused = True - print(f"Physics engine paused (idle for {idle_seconds:.0f}s)") + if idle_seconds > IDLE_SHUTDOWN_SECONDS: + print(f"Stopping server (idle for {idle_seconds:.0f}s)") + _stop_server() + break # Exit checker thread - new one starts with new server + + +def _stop_server() -> None: + """Stop the server and clear caches for fresh restart.""" + global _server_ref, _idle_checker_started + if _server_ref is not None: + # Stop the server + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(_server_ref.stop()) + except Exception: + pass + finally: + loop.close() + _server_ref = None + + # Clear Streamlit's cached server so next visitor gets fresh instance + get_or_create_server.clear() + + # Reset idle checker flag so new one can start + _idle_checker_started = False def _start_idle_checker(server: SimulationServer) -> None: @@ -161,19 +184,21 @@ def get_or_create_server() -> SimulationServer: def _update_activity() -> None: - """Update activity timestamp and resume physics if paused.""" + """Update activity timestamp to prevent idle shutdown.""" global _last_activity_time _last_activity_time = time.time() - # Resume physics if it was paused - server = st.session_state.get("server") - if server is not None and server.paused: - server.paused = False - print("Physics engine resumed (user activity detected)") - def init_session_state() -> None: """Initialise Streamlit session state.""" + # Check if existing server was stopped by idle checker + if "server" in st.session_state and st.session_state.server is not None: + if not st.session_state.server.is_running: + # Server was stopped - clear stale state for fresh start + st.session_state.server = None + st.session_state.pop("instruments", None) + st.session_state.pop("history", None) + # Get or create the server singleton (survives Streamlit reruns via st.cache_resource) if "server" not in st.session_state or st.session_state.server is None: with st.spinner("Starting simulation server..."): @@ -189,7 +214,7 @@ def init_session_state() -> None: st.error("Failed to start simulation server. Please refresh the page.") st.stop() - # Start idle checker to pause physics when no one's viewing + # Start idle checker to stop server when no one's viewing _start_idle_checker(st.session_state.server) if "instruments" not in st.session_state: