uvicorn icon indicating copy to clipboard operation
uvicorn copied to clipboard

Should server shutdown after receiving "lifespan.shutdown.failed"?

Open euri10 opened this issue 10 months ago • 5 comments

Discussed in https://github.com/encode/uvicorn/discussions/2298

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:

image

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.

[!IMPORTANT]

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

euri10 avatar Apr 16 '24 06:04 euri10

the issue and patch seems legit to me, can you submit a PR @peterschutt ?

euri10 avatar Apr 16 '24 06:04 euri10

Thanks @euri10 - I'll polish up the patch and submit a PR.

peterschutt avatar Apr 17 '24 00:04 peterschutt

I don't think this is a bug. My interpretation from the spec is that once the "lifespan.startup.complete" is sent, your application is the only task running.

Also, this is the same behavior as the other ASGI servers.

I think what you want is something like this:

import anyio
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route

import uvicorn


async def infinite_task():
    while True:
        await anyio.sleep(1)
        print("I am alive!")


async def health(request: Request):
    return JSONResponse({"status": "ok"})


app = Starlette(routes=[Route("/health", health)])


async def main():
    async with anyio.create_task_group() as tg:
        tg.start_soon(infinite_task)

        config = uvicorn.Config(app=app, host="0.0.0.0", port=8002)
        server = uvicorn.Server(config=config)
        await server.serve()


if __name__ == "__main__":
    anyio.run(main, backend_options={"use_uvloop": True})

Kludex avatar Apr 17 '24 17:04 Kludex

Thanks @Kludex - that pattern will work just fine!

once the "lifespan.startup.complete" is sent, your application is the only task running.

Do you feel there is a disconnect between the spec, and how some frameworks advertise lifespan? For example, from starlette's docs (ref):

Consider using anyio.create_task_group() for managing asynchronous tasks.

And from litestar (ref):

This can be useful when dealing with long running tasks

And lastly, from a framework perspective, can we do any better than returning the "lifespan.shutdown.failed" message, in the case where an exception is caught within the lifespan?

peterschutt avatar Apr 17 '24 20:04 peterschutt

Do you feel there is a disconnect between the spec, and how some frameworks advertise lifespan? For example, from starlette's docs (ref):

I'm not sure.

And lastly, from a framework perspective, can we do any better than returning the "lifespan.shutdown.failed" message, in the case where an exception is caught within the lifespan?

I don't know.

Kludex avatar May 12 '24 15:05 Kludex

@euri10 can we close this please?

peterschutt avatar May 12 '24 18:05 peterschutt