Lifespan
Version: 1.0.0 Status: SOURCE OF TRUTH Last Updated: 2025-12-03
Overview
ServerLifespan manages startup and shutdown sequences for AsgiServer.
Responsibilities:
Initialize server resources (config, logger, executors)
Call sub-app
on_startuphooksHandle shutdown in reverse order
Report startup failures to ASGI server
ServerLifespan Class
class ServerLifespan:
"""Manages AsgiServer startup/shutdown lifecycle."""
def __init__(self, server: AsgiServer) -> None:
self.server = server
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
"""Handle ASGI lifespan events."""
while True:
message = await receive()
msg_type = message["type"]
if msg_type == "lifespan.startup":
try:
await self._startup()
await send({"type": "lifespan.startup.complete"})
except Exception as e:
await send({
"type": "lifespan.startup.failed",
"message": str(e),
})
return
elif msg_type == "lifespan.shutdown":
await self._shutdown()
await send({"type": "lifespan.shutdown.complete"})
return
Startup Sequence
async def _startup(self) -> None:
"""Execute startup sequence."""
# 1. Server resources first
await self._init_config()
await self._init_logger()
await self._init_executors()
# 2. Sub-apps in mount order
for path, app_handler in self.server.apps.items():
app = app_handler["app"]
if hasattr(app, "on_startup"):
self.server.logger.info(f"Starting app at {path}")
await app.on_startup()
self.server._started = True
self.server.logger.info("Server started")
Shutdown Sequence
async def _shutdown(self) -> None:
"""Execute shutdown sequence."""
self.server.logger.info("Server shutting down")
# 1. Sub-apps in REVERSE order
for path in reversed(list(self.server.apps.keys())):
app_handler = self.server.apps[path]
app = app_handler["app"]
if hasattr(app, "on_shutdown"):
self.server.logger.info(f"Stopping app at {path}")
await app.on_shutdown()
# 2. Server resources last
self.server.shutdown() # Executors
self.server._started = False
self.server.logger.info("Server stopped")
Startup/Shutdown Order
Phase |
Startup Order |
Shutdown Order |
|---|---|---|
1 |
Config |
Sub-apps (reverse) |
2 |
Logger |
Executors |
3 |
Executors |
Logger |
4 |
Sub-apps (mount order) |
Config |
Principle: Resources are shut down in reverse order of creation. Sub-apps may depend on server resources, so server shuts down last.
Sub-App Hooks
Apps can define async hooks:
class MyApp(AsgiServerEnabler):
async def on_startup(self) -> None:
"""Called when server starts."""
self.db = await create_db_pool()
self.binder.logger.info("Database pool created")
async def on_shutdown(self) -> None:
"""Called when server stops."""
await self.db.close()
self.binder.logger.info("Database pool closed")
async def __call__(self, scope, receive, send):
# ... handle requests
Error Handling
Startup Failure
If any startup step fails, server reports failure:
try:
await self._startup()
await send({"type": "lifespan.startup.complete"})
except Exception as e:
self.server.logger.error(f"Startup failed: {e}")
await send({
"type": "lifespan.startup.failed",
"message": str(e),
})
return # Don't wait for shutdown
Shutdown Errors
Shutdown continues even if one app fails:
for path in reversed(list(self.server.apps.keys())):
app = self.server.apps[path]["app"]
if hasattr(app, "on_shutdown"):
try:
await app.on_shutdown()
except Exception as e:
self.server.logger.error(f"Shutdown error for {path}: {e}")
# Continue with other apps
ASGI Lifespan Protocol
Lifespan events follow ASGI spec:
Server Uvicorn
------ -------
<-- lifespan.startup
startup sequence
lifespan.startup.complete -->
(server running)
<-- lifespan.shutdown
shutdown sequence
lifespan.shutdown.complete -->
Usage with AsgiServer
ServerLifespan is automatically created by AsgiServer:
class AsgiServer(RoutingClass):
def __init__(self, ...):
self.lifespan = ServerLifespan(self)
async def __call__(self, scope, receive, send):
if scope["type"] == "lifespan":
await self.lifespan(scope, receive, send)
return
# ... handle http/websocket
Testing Without Lifespan
For testing, you can skip lifespan:
# Direct call without lifespan
server = AsgiServer()
server.mount("/api", api_app)
# Call directly
await server(scope, receive, send)
# Or manually trigger startup
await server.lifespan._startup()
try:
await server(scope, receive, send)
finally:
await server.lifespan._shutdown()
Copyright: Softwell S.r.l. (2025) License: Apache License 2.0