Skip to content

Commit da8b4d7

Browse files
committed
fix warnings on fastapi render server stop
stopping the event loop did not allow the server to clean up extraneous tasks that might still be dangling. see here: Kludex/uvicorn#742
1 parent 4cf7c8c commit da8b4d7

File tree

2 files changed

+51
-9
lines changed

2 files changed

+51
-9
lines changed

src/idom/server/base.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def __init__(
3030
) -> None:
3131
self._app: Optional[_App] = None
3232
self._root_component_constructor = constructor
33-
self._daemonized = False
33+
self._daemon_thread: Optional[Thread] = None
3434
self._config = self._create_config(config)
3535
self._server_did_start = Event()
3636

@@ -47,7 +47,7 @@ def run(self, host: str, port: int, *args: Any, **kwargs: Any) -> None:
4747
self.register(app)
4848
else: # pragma: no cover
4949
app = self._app
50-
if not self._daemonized: # pragma: no cover
50+
if self._daemon_thread is None: # pragma: no cover
5151
return self._run_application(self._config, app, host, port, args, kwargs)
5252
else:
5353
return self._run_application_in_thread(
@@ -56,11 +56,11 @@ def run(self, host: str, port: int, *args: Any, **kwargs: Any) -> None:
5656

5757
def daemon(self, *args: Any, **kwargs: Any) -> Thread:
5858
"""Run the standalone application in a seperate thread."""
59-
self._daemonized = True
59+
self._daemon_thread = thread = Thread(
60+
target=lambda: self.run(*args, **kwargs), daemon=True
61+
)
6062

61-
thread = Thread(target=lambda: self.run(*args, **kwargs), daemon=True)
6263
thread.start()
63-
6464
self.wait_until_server_start()
6565

6666
return thread

src/idom/server/fastapi.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import asyncio
22
import json
33
import logging
4+
import sys
45
import uuid
56
from threading import Event
67
from typing import Any, Dict, Optional, Tuple, Type, Union, cast
78

8-
import uvicorn
99
from fastapi import APIRouter, FastAPI, Request, WebSocket
1010
from fastapi.middleware.cors import CORSMiddleware
1111
from fastapi.responses import RedirectResponse
1212
from fastapi.staticfiles import StaticFiles
1313
from mypy_extensions import TypedDict
1414
from starlette.websockets import WebSocketDisconnect
15+
from uvicorn.config import Config as UvicornConfig
16+
from uvicorn.server import Server as UvicornServer
17+
from uvicorn.supervisors.multiprocess import Multiprocess
18+
from uvicorn.supervisors.statreload import StatReload as ChangeReload
1519

1620
from idom.config import IDOM_CLIENT_BUILD_DIR
1721
from idom.core.dispatcher import (
@@ -42,10 +46,13 @@ class FastApiRenderServer(AbstractRenderServer[FastAPI, Config]):
4246
"""Base ``sanic`` extension."""
4347

4448
_dispatcher_type: Type[AbstractDispatcher]
49+
_server: UvicornServer
4550

46-
def stop(self) -> None:
51+
def stop(self, timeout: float = 3) -> None:
4752
"""Stop the running application"""
48-
self._loop.call_soon_threadsafe(self._loop.stop)
53+
self._server.should_exit
54+
if self._daemon_thread is not None:
55+
self._daemon_thread.join(timeout)
4956

5057
def _create_config(self, config: Optional[Config]) -> Config:
5158
new_config: Config = {
@@ -137,7 +144,10 @@ def _run_application(
137144
args: Tuple[Any, ...],
138145
kwargs: Dict[str, Any],
139146
) -> None:
140-
uvicorn.run(app, host=host, port=port, loop="asyncio", *args, **kwargs)
147+
self._server = UvicornServer(
148+
UvicornConfig(app, host=host, port=port, loop="asyncio", *args, **kwargs)
149+
)
150+
_run_uvicorn_server(self._server)
141151

142152
def _run_application_in_thread(
143153
self,
@@ -199,3 +209,35 @@ async def _run_dispatcher(
199209
msg = f"SharedClientState server does not support per-client view parameters {params}"
200210
raise ValueError(msg)
201211
await self._dispatcher.run(send, recv, uuid.uuid4().hex, join=True)
212+
213+
214+
def _run_uvicorn_server(server: UvicornServer) -> None:
215+
# The following was copied from the uvicorn source with minimal modification. We
216+
# shouldn't need to do this, but unfortunately there's no easy way to gain access to
217+
# the server instance so you can stop it.
218+
# BUG: https://github.com/encode/uvicorn/issues/742
219+
config = server.config
220+
221+
if (config.reload or config.workers > 1) and not isinstance(
222+
server.config.app, str
223+
): # pragma: no cover
224+
logger = logging.getLogger("uvicorn.error")
225+
logger.warning(
226+
"You must pass the application as an import string to enable 'reload' or "
227+
"'workers'."
228+
)
229+
sys.exit(1)
230+
231+
if config.should_reload: # pragma: no cover
232+
sock = config.bind_socket()
233+
supervisor = ChangeReload(config, target=server.run, sockets=[sock])
234+
supervisor.run()
235+
elif config.workers > 1: # pragma: no cover
236+
sock = config.bind_socket()
237+
supervisor = Multiprocess(config, target=server.run, sockets=[sock])
238+
supervisor.run()
239+
else:
240+
import asyncio
241+
242+
asyncio.set_event_loop(asyncio.new_event_loop())
243+
server.run()

0 commit comments

Comments
 (0)