Lifespan Management¶
Application Lifecycle
Unchained provides a powerful lifespan management system that allows you to handle application startup and shutdown events.
What is Lifespan?¶
Lifespan is a concept from the ASGI (Asynchronous Server Gateway Interface) specification that manages the lifecycle of an application. The lifespan protocol provides hooks for:
- Startup: Actions to perform when the server starts, before handling any requests
- Shutdown: Cleanup actions to perform when the server is shutting down
Unchained builds upon this ASGI concept to provide a clean, intuitive API for managing application lifecycle events.
Lifespan Functions Must Yield
All lifespan functions must use the yield
statement to separate startup from shutdown logic. Failing to yield will result in an error.
Ways to Define Lifespan¶
There are two ways to define lifespan handlers in Unchained:
1. Using the Decorator¶
from unchained import Unchained
app = Unchained()
@app.lifespan
def startup(app: Unchained):
# Startup code (executed before the server starts handling requests)
print("Server is starting up")
app.state.initialized = True
yield # This yield separates startup from shutdown code
# Shutdown code (executed when the server is shutting down)
print("Server is shutting down")
app.state.initialized = False
2. During Initialization¶
from unchained import Unchained
async def my_lifespan(app: Unchained):
# Startup code
print("Starting up")
yield
# Shutdown code
print("Shutting down")
# Pass the lifespan function during app initialization
app = Unchained(lifespan=my_lifespan)
Sync vs Async Lifespan¶
Unchained supports both synchronous and asynchronous lifespan handlers:
How Lifespan Works in ASGI¶
When an ASGI server (like Uvicorn or Daphne) starts, it sends a lifespan event to the application:
- Server sends
{"type": "lifespan.startup"}
to the application - Application executes all startup code (before the
yield
) - Application responds with
{"type": "lifespan.startup.complete"}
- Server starts handling HTTP requests
- When shutting down, server sends
{"type": "lifespan.shutdown"}
- Application executes all shutdown code (after the
yield
) - Application responds with
{"type": "lifespan.shutdown.complete"}
Unchained handles all of this communication for you, providing a clean API that focuses on your application logic rather than the ASGI protocol details.
Multiple Lifespan Handlers¶
You can define multiple lifespan handlers, and they will be executed in the order they are defined:
@app.lifespan
async def http_client_lifespan(app: Unchained):
app.state.http_client = httpx.AsyncClient()
yield
await app.state.http_client.aclose()
@app.lifespan
async def cache_lifespan(app: Unchained):
await app.state.cache.connect()
yield
await app.state.cache.disconnect()
On startup, the handlers execute in order (http_client_lifespan then cache_lifespan). On shutdown, they execute in reverse order (cache_lifespan cleanup then http_client_lifespan cleanup).
Common Use Cases¶
Resource Initialization and Cleanup¶
The most common use case for lifespan is initializing resources at startup and cleaning them up at shutdown:
@app.lifespan
async def initialize_resources(app: Unchained):
# Initialize resources
app.state.http_client = AsyncClient()
app.state.redis = Redis.from_url(os.getenv("REDIS_URL"))
# Return control to the server
yield
# Clean up resources
await app.state.http_client.aclose()
await app.state.redis.close()
Error Handling¶
When working with lifespan functions, it's important to handle potential errors properly:
Best Practices¶
- Always Yield: Every lifespan function must yield exactly once
- Resource Management: Always clean up resources during shutdown
- Error Handling: Use try/except/finally for robust error handling
- State Initialization: Use lifespan for initializing application state
- Ordering: Be conscious of the order of lifespan handlers
Example: Complete Application Setup¶
from unchained import Unchained
from unchained.states import BaseState
import httpx
import redis
import os
class AppState(BaseState):
debug: bool = False
api_key: str
environment: str = "development"
app = Unchained(state=AppState())
@app.lifespan
def load_config(app: Unchained):
# Load configuration
app.state.debug = os.getenv("DEBUG", "false").lower() == "true"
app.state.api_key = os.getenv("API_KEY")
app.state.environment = os.getenv("ENVIRONMENT", "development")
yield
# No cleanup needed
@app.lifespan
async def initialize_clients(app: Unchained):
# Initialize clients
app.state.http_client = httpx.AsyncClient(
headers={"Authorization": f"Bearer {app.state.api_key}"}
)
app.state.redis = redis.Redis.from_url(os.getenv("REDIS_URL"))
yield
# Clean up
await app.state.http_client.aclose()
await app.state.redis.close()