-
Notifications
You must be signed in to change notification settings - Fork 225
Description
ASGI lifespan events are designed to handle setup and subsequent teardown of resource like a database connection, but does not provide any persistence to store this state to requests.
Starlette implements an Application.state
namespace (docs) which modifies the application object in place. Quart suggest to store data in the app's namespace directly.
This is not ideal because it gives an otherwise stateless thing state, and there is also no correlation between this state and the event loop / ASGI state, which can lead to inconsistent state.
Here's an artificial but not absurd example of how this could lead to a confusing user experience:
import asyncio
from contextlib import asynccontextmanager
from dataclasses import dataclass
from typing import AsyncIterator
from starlette.applications import Starlette
from starlette.testclient import TestClient
from starlette.requests import Request
from starlette.responses import Response
from starlette.routing import Route
@dataclass
class Connection:
loop: asyncio.AbstractEventLoop
async def run(self) -> None:
if asyncio.get_event_loop() is not self.loop:
raise Exception("Some obtuse error")
@asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[None]:
app.state.db = Connection(asyncio.get_event_loop())
yield
async def endpoint(request: Request) -> Response:
await request.app.state.db.run()
return Response()
app = Starlette(
routes=[Route("/", endpoint)],
lifespan=lifespan,
)
def some_test_using_lifespans() -> None:
with TestClient(app):
pass
def some_test_where_the_user_forgets_to_use_the_lifespan() -> None:
TestClient(app).get("/")
if __name__ == "__main__":
some_test_using_lifespans()
some_test_where_the_user_forgets_to_use_the_lifespan()
Here an honest mistake ends up leaking state between tests, and maybe giving the user an obtuse error about event loops and such.
I think it would be beneficial if the spec provided a namespace scoped to each ASGI lifespan / request. This namespace would basically be an empty dict that gets persisted from the lifespan event into each request. I think it would make sense to model this like contextvar propagation into tasks: every request gets a copy of the namespace from the lifespan (unless there was no lifespan, in which it's a brand new dict or maybe None).
Then state can be naturally stored in this namespace.
Since the ASGI server manages this namespace (and it already manages it's own state because of lifespans), the application can be stateless and neither the framework nor the users have to worry about clearing state at the end of a lifespan or anything like.
This can easily be backwards compatible: it's a new key in the scope and we bump the minor version of the ASGI version so that frameworks can check if this is supported or not.