engine/session updates

This commit is contained in:
John Lancaster
2026-07-31 22:15:51 -05:00
parent cd11ea8255
commit 0dc06f72ca
12 changed files with 767 additions and 457 deletions
+12 -20
View File
@@ -63,7 +63,7 @@ The engine is a long-lived factory and pool, not a single database connection. T
- Create one `AsyncEngine` per process and database configuration in the normal case.
- Dispose it explicitly in an awaitable shutdown path; garbage collection cannot reliably await async driver cleanup.
- Configure `async_sessionmaker` once and call it to create short-lived sessions.
- Configure `async_sessionmaker` once inside the engine lifecycle and call it to create short-lived sessions.
- Close each session deterministically with `async with` or a FastAPI dependency that yields once.
See [engine lifecycle](references/engine.md) and [session management](references/session.md).
@@ -92,7 +92,7 @@ See [transaction boundaries](references/transactions.md).
FastAPI lifespan owns resources shared by many requests. A dependency with one `yield` owns request-scoped resources and runs cleanup after use. These are related context-manager mechanisms but solve different lifetime problems.
Use `AsyncExitStack` when lifespan acquires a variable, conditional, or mixed collection of context-managed resources. It records cleanup as resources are acquired and unwinds callbacks in reverse order. A single engine with one cleanup callback can use a plain `try/finally`; `AsyncExitStack` is a composition tool, not a requirement.
Use `AsyncExitStack` when lifespan acquires a variable, conditional, or mixed collection of context-managed resources. It records cleanup as resources are acquired and unwinds callbacks in reverse order. A single engine should use the direct engine context manager; `AsyncExitStack` is a composition tool, not a requirement.
See [engine lifecycle](references/engine.md).
@@ -138,34 +138,26 @@ See [database testing and fixture data](references/testing.md).
This example shows the ownership boundaries. Adapt state storage and dependency wiring to the application's conventions.
```python
from contextlib import AsyncExitStack, asynccontextmanager
from collections.abc import AsyncGeneratorr
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from fastapi import FastAPI, Request
from sqlmodel.ext.asyncio.session import AsyncSession
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
async with AsyncExitStack() as stack:
engine = create_async_engine(settings.database_url)
stack.push_async_callback(engine.dispose)
session_factory = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
app.state.session_factory = session_factory
async with engine_scope(settings.database_url) as engine:
app.state.session_factory = create_session_factory(engine)
yield
async def get_session() -> AsyncGenerator[AsyncSession]:
async with app.state.session_factory() as session:
yield session
async def get_session(request: Request) -> AsyncGenerator[AsyncSession]:
async with request.app.state.session_factory() as session:
yield session
```
For direct construction without `AsyncExitStack`, put `await engine.dispose()` in a `finally` block. For background work that outlives a request, create a new session inside that task instead of retaining the request's session.
`engine_scope()` and `create_session_factory()` are defined in the engine and session references. For background work that outlives a request, inject the shared factory and create a new session inside that task instead of retaining the request's session.
## Explanation Procedure