fix(dashboard): use module-level singleton to prevent port conflicts on refresh
When Streamlit refreshes/reruns, session state is lost but the old simulation server thread keeps running on ports 5001-5003. This caused "address already in use" errors when trying to start a new server. Solution: Use a module-level singleton for the simulation server that persists across Streamlit reruns. The get_or_create_server() function checks if a server is already running before creating a new one. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,12 @@ from py_dvt_ate.tests.thermal.tempco import TempCoTest
|
|||||||
# Thread pool for background test execution
|
# Thread pool for background test execution
|
||||||
_test_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="test_runner")
|
_test_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="test_runner")
|
||||||
|
|
||||||
|
# Module-level singleton for simulation server
|
||||||
|
# This ensures only one server instance exists across all Streamlit reruns,
|
||||||
|
# preventing "address already in use" errors when the page refreshes
|
||||||
|
_simulation_server: SimulationServer | None = None
|
||||||
|
_server_thread: threading.Thread | None = None
|
||||||
|
|
||||||
# Idle pause configuration
|
# Idle pause configuration
|
||||||
# Physics engine pauses after IDLE_PAUSE_SECONDS of no activity (default: 30s)
|
# Physics engine pauses after IDLE_PAUSE_SECONDS of no activity (default: 30s)
|
||||||
IDLE_PAUSE_SECONDS = int(os.environ.get("IDLE_PAUSE_SECONDS", "30"))
|
IDLE_PAUSE_SECONDS = int(os.environ.get("IDLE_PAUSE_SECONDS", "30"))
|
||||||
@@ -101,12 +107,22 @@ class TestProgress:
|
|||||||
return time.time() - self.started_at
|
return time.time() - self.started_at
|
||||||
|
|
||||||
|
|
||||||
def start_embedded_server() -> tuple[SimulationServer, threading.Thread]:
|
def get_or_create_server() -> SimulationServer:
|
||||||
"""Start an embedded simulation server in a background thread.
|
"""Get or create the simulation server singleton.
|
||||||
|
|
||||||
|
Uses module-level singleton to ensure only one server instance exists
|
||||||
|
across all Streamlit reruns, preventing "address already in use" errors.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (server instance, thread running the server).
|
The simulation server instance.
|
||||||
"""
|
"""
|
||||||
|
global _simulation_server, _server_thread
|
||||||
|
|
||||||
|
# Return existing server if it's running
|
||||||
|
if _simulation_server is not None and _simulation_server.is_running:
|
||||||
|
return _simulation_server
|
||||||
|
|
||||||
|
# Create new server
|
||||||
server = SimulationServer(
|
server = SimulationServer(
|
||||||
ServerConfig(
|
ServerConfig(
|
||||||
host="127.0.0.1",
|
host="127.0.0.1",
|
||||||
@@ -151,27 +167,34 @@ def start_embedded_server() -> tuple[SimulationServer, threading.Thread]:
|
|||||||
if server_error:
|
if server_error:
|
||||||
st.error(f"Server startup error: {server_error[0]}")
|
st.error(f"Server startup error: {server_error[0]}")
|
||||||
|
|
||||||
return server, thread
|
# Store in module-level singleton
|
||||||
|
_simulation_server = server
|
||||||
|
_server_thread = thread
|
||||||
|
|
||||||
|
return server
|
||||||
|
|
||||||
|
|
||||||
def _update_activity() -> None:
|
def _update_activity() -> None:
|
||||||
"""Update activity timestamp and resume physics if paused."""
|
"""Update activity timestamp and resume physics if paused."""
|
||||||
global _last_activity_time
|
global _last_activity_time, _simulation_server
|
||||||
_last_activity_time = time.time()
|
_last_activity_time = time.time()
|
||||||
|
|
||||||
# Resume physics if it was paused
|
# Resume physics if it was paused (use singleton as fallback)
|
||||||
if "server" in st.session_state:
|
server = st.session_state.get("server") or _simulation_server
|
||||||
server = st.session_state.server
|
if server is not None and server.paused:
|
||||||
if server.paused:
|
server.paused = False
|
||||||
server.paused = False
|
print("Physics engine resumed (user activity detected)")
|
||||||
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."""
|
||||||
if "server" not in st.session_state:
|
global _simulation_server
|
||||||
|
|
||||||
|
# Get or create the server singleton (survives Streamlit reruns)
|
||||||
|
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..."):
|
||||||
st.session_state.server, st.session_state.server_thread = start_embedded_server()
|
server = get_or_create_server()
|
||||||
|
st.session_state.server = server
|
||||||
|
|
||||||
# Verify server started correctly
|
# Verify server started correctly
|
||||||
if st.session_state.server.physics_engine is None:
|
if st.session_state.server.physics_engine is None:
|
||||||
@@ -181,15 +204,17 @@ def init_session_state() -> None:
|
|||||||
# Start idle checker to pause physics when no one's viewing
|
# Start idle checker to pause physics when no one's viewing
|
||||||
_start_idle_checker(st.session_state.server)
|
_start_idle_checker(st.session_state.server)
|
||||||
|
|
||||||
# Register cleanup
|
# Register cleanup (only once, for the singleton)
|
||||||
def cleanup() -> None:
|
def cleanup() -> None:
|
||||||
if hasattr(st.session_state, "server") and st.session_state.server is not None:
|
global _simulation_server
|
||||||
|
if _simulation_server is not None:
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(st.session_state.server.stop())
|
loop.run_until_complete(_simulation_server.stop())
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
loop.close()
|
loop.close()
|
||||||
|
_simulation_server = None
|
||||||
atexit.register(cleanup)
|
atexit.register(cleanup)
|
||||||
|
|
||||||
if "instruments" not in st.session_state:
|
if "instruments" not in st.session_state:
|
||||||
|
|||||||
Reference in New Issue
Block a user