Fix server initialization race condition with proper event signaling

This commit is contained in:
2025-11-06 09:52:35 +00:00
parent 15db602430
commit 824f4f587c

View File

@@ -61,25 +61,34 @@ def start_embedded_server() -> tuple[SimulationServer, threading.Thread]:
) )
) )
server_ready = threading.Event()
def run_server() -> None: def run_server() -> None:
"""Run the async server in a new event loop.""" """Run the async server in a new event loop."""
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
try: try:
loop.run_until_complete(server.start()) loop.run_until_complete(server.start())
# Signal that server is ready
server_ready.set()
# Keep the event loop running # Keep the event loop running
loop.run_forever() loop.run_forever()
except Exception as e: except Exception as e:
st.error(f"Server error: {e}") st.error(f"Server error: {e}")
server_ready.set() # Signal even on error
finally: finally:
loop.run_until_complete(server.stop()) try:
loop.run_until_complete(server.stop())
except Exception:
pass
loop.close() loop.close()
thread = threading.Thread(target=run_server, daemon=True) thread = threading.Thread(target=run_server, daemon=True)
thread.start() thread.start()
# Wait a moment for server to start # Wait for server to be fully started (up to 5 seconds)
time.sleep(0.5) if not server_ready.wait(timeout=5.0):
st.error("Server failed to start within timeout")
return server, thread return server, thread
@@ -87,13 +96,22 @@ def start_embedded_server() -> tuple[SimulationServer, threading.Thread]:
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: if "server" not in st.session_state:
st.session_state.server, st.session_state.server_thread = start_embedded_server() with st.spinner("Starting simulation server..."):
st.session_state.server, st.session_state.server_thread = start_embedded_server()
# Verify server started correctly
if st.session_state.server.physics_engine is None:
st.error("Failed to start simulation server. Please refresh the page.")
st.stop()
# Register cleanup # Register cleanup
def cleanup() -> None: def cleanup() -> None:
if hasattr(st.session_state, "server") and st.session_state.server is not None: if hasattr(st.session_state, "server") and st.session_state.server is not None:
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
loop.run_until_complete(st.session_state.server.stop()) try:
loop.run_until_complete(st.session_state.server.stop())
except Exception:
pass
loop.close() loop.close()
atexit.register(cleanup) atexit.register(cleanup)
@@ -308,7 +326,24 @@ def display_controls() -> None:
@st.fragment(run_every=0.1) @st.fragment(run_every=0.1)
def simulation_display() -> None: def simulation_display() -> None:
"""Fragment that displays and updates simulation state.""" """Fragment that displays and updates simulation state."""
# Check if server is initialized
if "server" not in st.session_state:
st.warning("⏳ Initializing simulation server...")
return
server: SimulationServer = st.session_state.server server: SimulationServer = st.session_state.server
# Get current state from physics engine (for visualization)
engine: PhysicsEngine | None = server.physics_engine
if engine is None:
st.error("❌ Physics engine not available. The server may not have started correctly. Try refreshing the page.")
return
# Check if server is running
if not server.is_running:
st.warning("⚠️ Server is not running. Try refreshing the page.")
return
instruments: InstrumentSet = st.session_state.instruments instruments: InstrumentSet = st.session_state.instruments
history: SimulationHistory = st.session_state.history history: SimulationHistory = st.session_state.history
@@ -319,12 +354,6 @@ def simulation_display() -> None:
if st.session_state.running: if st.session_state.running:
step_simulation() step_simulation()
# Get current state from physics engine (for visualization)
engine: PhysicsEngine | None = server.physics_engine
if engine is None:
st.error("Physics engine not available")
return
thermal = engine.get_thermal_state() thermal = engine.get_thermal_state()
electrical = engine.get_electrical_state() electrical = engine.get_electrical_state()