Skip to content

Commit f45c2ec

Browse files
committed
Updated package v4.15
1 parent f4c2600 commit f45c2ec

File tree

4 files changed

+90
-14
lines changed

4 files changed

+90
-14
lines changed

CHANGELOG.md

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

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

5+
## 0.4.15 - 2025-03-11
6+
7+
### Fixed
8+
9+
- Fixed critical error: "'NoneType' object has no attribute 'startup'"
10+
- Implemented NoopLifespan class as a fallback when all lifespan initialization attempts fail
11+
- Ensured server can start even when lifespan initialization fails
12+
- Added proper error handling for startup and shutdown events
13+
- Enhanced server stability across different environments and uvicorn versions
14+
- Added robust error recovery during server startup process
15+
- Overrode uvicorn's startup and shutdown methods to add additional error handling
16+
- Improved logging for lifespan-related errors to aid in troubleshooting
17+
- Added graceful fallback mechanisms for all critical server operations
18+
- Ensured clean server shutdown even when lifespan shutdown fails
19+
520
## 0.4.14 - 2025-03-11
621

722
### Fixed

locallab/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
LocalLab - A lightweight AI inference server for running LLMs locally
33
"""
44

5-
__version__ = "0.4.14"
5+
__version__ = "0.4.15"
66

77
# Only import what's necessary initially, lazy-load the rest
88
from .logger import get_logger

locallab/server.py

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,66 @@ def delayed_exit():
194194
threading.Thread(target=delayed_exit, daemon=True).start()
195195

196196

197+
class NoopLifespan:
198+
"""A no-operation lifespan implementation that provides the required methods but doesn't do anything."""
199+
200+
def __init__(self, app):
201+
self.app = app
202+
203+
async def startup(self):
204+
"""No-op startup method."""
205+
logger.warning("Using NoopLifespan - server may not handle startup/shutdown events properly")
206+
pass
207+
208+
async def shutdown(self):
209+
"""No-op shutdown method."""
210+
pass
211+
212+
197213
class ServerWithCallback(uvicorn.Server):
198214
def install_signal_handlers(self):
199215
# Override to prevent uvicorn from installing its own handlers
200216
pass
201217

218+
async def startup(self, sockets=None):
219+
"""Override the startup method to add error handling for lifespan startup."""
220+
if self.should_exit:
221+
return
222+
223+
if sockets is not None:
224+
self.servers = []
225+
for socket in sockets:
226+
server = await self.config.server_class(config=self.config, server=self)
227+
await server.start()
228+
self.servers.append(server)
229+
else:
230+
self.servers = [await self.config.server_class(config=self.config, server=self)]
231+
await self.servers[0].start()
232+
233+
if self.lifespan is not None:
234+
try:
235+
await self.lifespan.startup()
236+
except Exception as e:
237+
logger.error(f"Error during lifespan startup: {str(e)}")
238+
logger.debug(f"Lifespan startup error details: {traceback.format_exc()}")
239+
# Replace with NoopLifespan if startup fails
240+
self.lifespan = NoopLifespan(self.config.app)
241+
logger.warning("Replaced failed lifespan with NoopLifespan")
242+
243+
async def shutdown(self, sockets=None):
244+
"""Override the shutdown method to add error handling for lifespan shutdown."""
245+
if self.servers:
246+
for server in self.servers:
247+
await server.shutdown()
248+
249+
if self.lifespan is not None:
250+
try:
251+
await self.lifespan.shutdown()
252+
except Exception as e:
253+
logger.error(f"Error during lifespan shutdown: {str(e)}")
254+
logger.debug(f"Lifespan shutdown error details: {traceback.format_exc()}")
255+
logger.warning("Continuing shutdown despite lifespan error")
256+
202257
async def serve(self, sockets=None):
203258
self.config.setup_event_loop()
204259

@@ -296,19 +351,25 @@ async def serve(self, sockets=None):
296351
logger.info("Using LifespanState from uvicorn.lifespan.state")
297352
except (ImportError, AttributeError, TypeError) as e:
298353
logger.debug(f"Failed to import or initialize LifespanState: {str(e)}")
299-
# Fallback to no lifespan
300-
self.lifespan = None
301-
logger.warning("Could not initialize lifespan - server may not handle startup/shutdown events properly")
302-
303-
await self.startup(sockets=sockets)
354+
# Fallback to NoopLifespan
355+
self.lifespan = NoopLifespan(self.config.app)
356+
logger.warning("Using NoopLifespan - server may not handle startup/shutdown events properly")
304357

305-
# Call our callback before processing requests
306-
# We need to access the on_startup function from the outer scope
307-
if hasattr(self, 'on_startup_callback') and callable(self.on_startup_callback):
308-
self.on_startup_callback()
309-
310-
await self.main_loop()
311-
await self.shutdown()
358+
try:
359+
await self.startup(sockets=sockets)
360+
361+
# Call our callback before processing requests
362+
# We need to access the on_startup function from the outer scope
363+
if hasattr(self, 'on_startup_callback') and callable(self.on_startup_callback):
364+
self.on_startup_callback()
365+
366+
await self.main_loop()
367+
await self.shutdown()
368+
except Exception as e:
369+
logger.error(f"Error during server operation: {str(e)}")
370+
logger.debug(f"Server error details: {traceback.format_exc()}")
371+
# Re-raise to allow proper error handling
372+
raise
312373

313374

314375
def start_server(use_ngrok: bool = None, port: int = None, ngrok_auth_token: Optional[str] = None):

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name="locallab",
8-
version="0.4.14",
8+
version="0.4.15",
99
packages=find_packages(include=["locallab", "locallab.*"]),
1010
install_requires=[
1111
"fastapi>=0.95.0,<1.0.0",

0 commit comments

Comments
 (0)