Originally posted by peterschutt April 5, 2024
If the shutdown failure message isn't initiated by the app receiving a "lifespan.shutdown" from uvicorn, then the app continues to run after the "lifespan.shutdown.failure" message is received.
Reproducer:
import asyncio
import contextlib
from collections.abc import AsyncIterator
import anyio
import uvicorn
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
async def homepage(_) -> JSONResponse:
return JSONResponse({'hello': 'world'})
async def sleep_and_raise() -> None:
await asyncio.sleep(1)
raise RuntimeError("An error occurred")
@contextlib.asynccontextmanager
async def lifespan(_: Starlette) -> AsyncIterator[None]:
async with contextlib.AsyncExitStack() as stack:
tg = await stack.enter_async_context(anyio.create_task_group())
tg.start_soon(sleep_and_raise)
yield
app = Starlette(debug=True, routes=[Route('/', homepage)], lifespan=lifespan)
if __name__ == "__main__":
uvicorn.run(app, lifespan="on")
After the error occurs in the lifespan task, the app continues to serve:
Given that apps like starlette and litestar encourage use of the lifespan
context for orchestration of things that should have a lifespan equivalent to the application object, then I think it would make sense for the app to stop if something has failed within that lifespan after the app has sent "startup.complete" but before the server has sent "shutdown" to the app.
The spec seems to agree:
If a server sees this it should log/print the message provided and then terminate.
Pay now to fund the work behind this issue.
Get updates on progress being made.
Maintainer is rewarded once the issue is completed.
You're funding impactful open source efforts
You want to contribute to this effort
You want to get funding like this too