Uvicorn: Uvicorn cannot be shutdown programmatically

Created on 3 Aug 2020  Â·  8Comments  Â·  Source: encode/uvicorn

There is no documented way to shutdown uvicorn in python:

ex:

instance = uvicorn.run("example:app", host="127.0.0.1", port=5000, log_level="info")
instance.shutdown()

How do we shutdown uvicorn?

enhancement user experience

Most helpful comment

Hi,

Not documented indeed, but a multithreaded approach should do…

import contextlib
import time
import threading
import uvicorn

class Server(uvicorn.Server):
    def install_signal_handlers(self):
        pass

    @contextlib.contextmanager
    def run_in_thread(self):
        thread = threading.Thread(target=self.run)
        thread.start()
        try:
            while not self.started:
                time.sleep(1e-3)
            yield
        finally:
            self.should_exit = True
            thread.join()

config = Config("example:app", host="127.0.0.1", port=5000, log_level="info")
server = Server(config=config)

with server.run_in_thread():
    # Server started.
    ...
# Server stopped.

Very handy to run a live test server locally using a pytest fixture…

# conftest.py
import pytest

@pytest.fixture(scope="session")
def server():
    server = ...
    with server.run_in_thread():
        yield

All 8 comments

Hi,

Not documented indeed, but a multithreaded approach should do…

import contextlib
import time
import threading
import uvicorn

class Server(uvicorn.Server):
    def install_signal_handlers(self):
        pass

    @contextlib.contextmanager
    def run_in_thread(self):
        thread = threading.Thread(target=self.run)
        thread.start()
        try:
            while not self.started:
                time.sleep(1e-3)
            yield
        finally:
            self.should_exit = True
            thread.join()

config = Config("example:app", host="127.0.0.1", port=5000, log_level="info")
server = Server(config=config)

with server.run_in_thread():
    # Server started.
    ...
# Server stopped.

Very handy to run a live test server locally using a pytest fixture…

# conftest.py
import pytest

@pytest.fixture(scope="session")
def server():
    server = ...
    with server.run_in_thread():
        yield

I do appreciate the reply/code, but:

Why is this not built in and documented?
Why do I have to implement this code?

It's a webserver. Run_in_thread() and shutdown() are core functionality...am I wrong?

I'm following @florimondmanca s approach to run pact tests on the provider side. However when using this i get the following error from uvicorn's Server.run method:

RuntimeError: There is no current event loop in thread 'Thread-7'.

Any idea on that? Happens with python 3.6.9 as well as 3.7.7.

another edit:

I got it to work using

config = Config("example:app", host="127.0.0.1", port=5000, log_level="info", loop="asyncio")

I do appreciate the reply/code, but:

Why is this not built in and documented?
Why do I have to implement this code?

It's a webserver. Run_in_thread() and shutdown() are core functionality...am I wrong?

Fully agreed, this question of how to run and stop unicorn pops up on stack overflow as well, it's quite obscure as it is now

I appreciate the feedback here, but then this brings the question — what would folks expect the usage API to be for something like this?

I think a nice approach for allowing a programmatic shutdown of Uvicorn would be to exit the space of .run() and embrace concurrency, that is the async serve() method. It's actually super easy to spin up a concurrent task, and then clean it up whenever the server isn't needed anymore. (We could also very well make it easier to do this multithreaded thing, and having with server.run_in_thread() built in, or something.)

But then there's the question of A/ waiting for the server to startup (without having to reinvent the wheel each time), and B/ triggering the server shutdown and have it clean up gracefully (straight up cancelling a task is very... brutal).

_Personally_, I'd love this kind of API:

async with open_task_group() as tg:
    tg.start_soon(server.serve)
    await server.wait_started()
    # ...
    # Cause serve() to terminate soon, allowing the task
    # to finish cleanly when exiting the context manager.
    server.shutdown_soon().

Here open_task_group() refers to some kind of trio-like nursery API. On asyncio this could be a simplified helper, like this:

@asynccontextmanager
async def start_concurrently(async_fn):
    task = asyncio.create_task(async_fn())
    try:
        yield
    finally:
        await task

It could be used like this:

async with start_concurrently(server.serve):
    await server.wait_started()
    # ...
    server.shutdown_soon()

I'm also happy to discuss a threaded equivalent API, for use cases where an event loop isn't readily available or practical (for example, when testing the server using a sync HTTP client).

Then we need to figure out how to make these APIs come to life. :-) Maybe this means we'll need sync/async equivalents, like .wait_started() vs .await_started(), etc.

That's quite an elaborate pondering, thank you for that! I'd be willing to use all these APIs:)

Just for curiosity

If I understand correctly, having the server.shutdown_soon() implicit in the context itself like in your original example would be considered confusing? At least for my simple use cases, wait_started would immediatelly follow start and context teardown would always implicitly include shutdown_soon anyway - if it's not a part of the lib, I'd write a facade/subclass for it - I think it's simpler to just have something along the lines of

with running_server():
    ... # Make requests to the server, etc.

Well, yes, if we add a .serve_concurrently() method that does the "wait for started on enter" and "shutdown soon then await tasks on exit" thing, that's also an option. Probably also more convenient to use than having to deal with library concurrency APIs. (Let's face it, unlike trio asyncio doesn't offer safe enough APIs that would allow us to tell people to rely on them.)

And then a .run_in_thread() equivalent for threaded contexts.

Now that we know this is the kind of thing we want to have eventually, anyone that'd like to figure out internals and ways to implement this is very much welcome to. :) I'll reopen since I think this is indeed a valuable thing to add.

For people that stop by here and want to try out florimondmanca's multithreaded approach and are a bit new to _uvicorn_, _Config_ in the code comes from _uvicorn_. Configuration below also includes plazmakeks' addition in the case of running into a runtime error described by them.

config = uvicorn.Config(app, host="127.0.0.1", port=5000, log_level="info", loop="asyncio")

Thanks for this discussion all.

Was this page helpful?
0 / 5 - 0 ratings