Skip to content

Commit 90e7f53

Browse files
committed
Updated package v4.21
1 parent 437fdcc commit 90e7f53

File tree

2 files changed

+64
-11
lines changed

2 files changed

+64
-11
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
All notable changes to LocalLab will be documented in this file.
44

5+
## 0.4.21 - 2025-03-12
6+
7+
### Fixed
8+
9+
- Fixed critical issue with server not shutting down properly when Ctrl+C is pressed
10+
- Improved signal handling in ServerWithCallback class to ensure clean shutdown
11+
- Enhanced main_loop method to respond faster to shutdown signals
12+
- Implemented more robust server shutdown process with proper resource cleanup
13+
- Added additional logging during shutdown to help diagnose issues
14+
- Increased shutdown timeout to allow proper cleanup of all resources
15+
- Fixed multiple shutdown attempts when Ctrl+C is pressed repeatedly
16+
- Ensured all server components are properly closed during shutdown
17+
518
## 0.4.20 - 2025-03-12
619

720
### Fixed

locallab/server.py

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,22 @@ def signal_handler(signum, frame):
188188

189189
# Exit after a short delay to allow cleanup
190190
def delayed_exit():
191-
time.sleep(2) # Give some time for cleanup
191+
# Give more time for cleanup - increased from 2 to 5 seconds
192+
# This allows the server to complete its shutdown process
193+
time.sleep(5)
194+
195+
# Check if we need to force exit
196+
try:
197+
# Import here to avoid circular imports
198+
from .core.app import app
199+
if hasattr(app, "state") and hasattr(app.state, "server") and app.state.server:
200+
logger.debug("Server still running after timeout, forcing exit")
201+
else:
202+
logger.debug("Server shutdown completed successfully")
203+
except Exception:
204+
pass
205+
206+
# Force exit if we're still running
192207
sys.exit(0)
193208

194209
threading.Thread(target=delayed_exit, daemon=True).start()
@@ -479,7 +494,16 @@ async def serve(self, sock=None):
479494
class ServerWithCallback(uvicorn.Server):
480495
def install_signal_handlers(self):
481496
# Override to prevent uvicorn from installing its own handlers
482-
pass
497+
# but still handle the should_exit flag for clean shutdown
498+
499+
def handle_exit(signum, frame):
500+
self.should_exit = True
501+
logger.debug(f"Signal {signum} received in ServerWithCallback, setting should_exit=True")
502+
503+
# Register our own minimal signal handlers that just set should_exit
504+
# This allows the main process signal handler to handle the actual shutdown
505+
signal.signal(signal.SIGINT, handle_exit)
506+
signal.signal(signal.SIGTERM, handle_exit)
483507

484508
async def startup(self, sockets=None):
485509
"""Override the startup method to add error handling for lifespan startup."""
@@ -536,34 +560,50 @@ async def main_loop(self):
536560
try:
537561
# Use asyncio.sleep to keep the server running
538562
while not self.should_exit:
539-
await asyncio.sleep(0.1)
563+
# Check more frequently to respond to shutdown signals faster
564+
await asyncio.sleep(0.05)
565+
566+
# Check if we've received a shutdown signal
567+
if self.should_exit:
568+
logger.debug("Shutdown signal detected in main_loop")
569+
break
540570
except Exception as e:
541571
logger.error(f"Error in main loop: {str(e)}")
542572
logger.debug(f"Main loop error details: {traceback.format_exc()}")
543573
# Set should_exit to True to initiate shutdown
544574
self.should_exit = True
575+
576+
logger.debug("Exiting main_loop")
545577

546578
async def shutdown(self, sockets=None):
547579
"""Override the shutdown method to add error handling for lifespan shutdown."""
580+
logger.debug("Starting server shutdown process")
581+
582+
# First, shut down all servers
548583
if self.servers:
549584
for server in self.servers:
550585
try:
551-
# Check if the server has a shutdown method
552-
if hasattr(server, 'shutdown'):
553-
await server.shutdown()
554-
else:
555-
logger.warning(f"Server object {type(server)} has no shutdown method, skipping")
586+
server.close()
587+
await server.wait_closed()
588+
logger.debug("Server closed successfully")
556589
except Exception as e:
557-
logger.error(f"Error shutting down server: {str(e)}")
558-
logger.debug(f"Server shutdown error details: {traceback.format_exc()}")
590+
logger.error(f"Error closing server: {str(e)}")
591+
logger.debug(f"Server close error details: {traceback.format_exc()}")
559592

593+
# Then, shut down the lifespan
560594
if self.lifespan is not None:
561595
try:
562596
await self.lifespan.shutdown()
597+
logger.debug("Lifespan shutdown completed")
563598
except Exception as e:
564599
logger.error(f"Error during lifespan shutdown: {str(e)}")
565600
logger.debug(f"Lifespan shutdown error details: {traceback.format_exc()}")
566-
logger.warning("Continuing shutdown despite lifespan error")
601+
602+
# Clear all references to help with garbage collection
603+
self.servers = []
604+
self.lifespan = None
605+
606+
logger.debug("Server shutdown process completed")
567607

568608
async def serve(self, sockets=None):
569609
self.config.setup_event_loop()

0 commit comments

Comments
 (0)