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: