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")
|
_test_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="test_runner")
|
||||||
|
|
||||||
|
|
||||||
# Idle pause configuration
|
# Idle shutdown configuration
|
||||||
# Physics engine pauses after IDLE_PAUSE_SECONDS of no activity (default: 30s)
|
# Server stops after IDLE_SHUTDOWN_SECONDS of no activity (default: 5 minutes)
|
||||||
IDLE_PAUSE_SECONDS = int(os.environ.get("IDLE_PAUSE_SECONDS", "30"))
|
# Next visitor gets a fresh simulation instance
|
||||||
|
IDLE_SHUTDOWN_SECONDS = int(os.environ.get("IDLE_SHUTDOWN_SECONDS", "300"))
|
||||||
_last_activity_time: float = time.time()
|
_last_activity_time: float = time.time()
|
||||||
_idle_checker_started = False
|
_idle_checker_started = False
|
||||||
_server_ref: SimulationServer | None = None # Reference for idle checker thread
|
_server_ref: SimulationServer | None = None # Reference for idle checker thread
|
||||||
|
|
||||||
|
|
||||||
def _idle_checker() -> None:
|
def _idle_checker() -> None:
|
||||||
"""Background thread that pauses physics when idle."""
|
"""Background thread that stops server when idle."""
|
||||||
global _last_activity_time, _server_ref
|
global _last_activity_time, _server_ref
|
||||||
while True:
|
while True:
|
||||||
time.sleep(5) # Check every 5 seconds
|
time.sleep(10) # Check every 10 seconds
|
||||||
if _server_ref is None:
|
if _server_ref is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
idle_seconds = time.time() - _last_activity_time
|
idle_seconds = time.time() - _last_activity_time
|
||||||
if idle_seconds > IDLE_PAUSE_SECONDS and not _server_ref.paused:
|
if idle_seconds > IDLE_SHUTDOWN_SECONDS:
|
||||||
_server_ref.paused = True
|
print(f"Stopping server (idle for {idle_seconds:.0f}s)")
|
||||||
print(f"Physics engine paused (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:
|
def _start_idle_checker(server: SimulationServer) -> None:
|
||||||
@@ -161,19 +184,21 @@ def get_or_create_server() -> SimulationServer:
|
|||||||
|
|
||||||
|
|
||||||
def _update_activity() -> None:
|
def _update_activity() -> None:
|
||||||
"""Update activity timestamp and resume physics if paused."""
|
"""Update activity timestamp to prevent idle shutdown."""
|
||||||
global _last_activity_time
|
global _last_activity_time
|
||||||
_last_activity_time = time.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:
|
def init_session_state() -> None:
|
||||||
"""Initialise Streamlit session state."""
|
"""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)
|
# 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:
|
if "server" not in st.session_state or st.session_state.server is None:
|
||||||
with st.spinner("Starting simulation server..."):
|
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.error("Failed to start simulation server. Please refresh the page.")
|
||||||
st.stop()
|
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)
|
_start_idle_checker(st.session_state.server)
|
||||||
|
|
||||||
if "instruments" not in st.session_state:
|
if "instruments" not in st.session_state:
|
||||||
|
|||||||
Reference in New Issue
Block a user