# 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_startup` hooks - Handle shutdown in reverse order - Report startup failures to ASGI server --- ## ServerLifespan Class ```python 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 ```python 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 ```python 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: ```python 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: ```python 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: ```python 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`: ```python 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: ```python # 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