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 <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user