From 3e2854ac30583db239db9713d8ecb02025acc70f Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 29 Sep 2024 20:00:00 +0100 Subject: [PATCH 01/13] add socket-load-balance flag --- tests/test_subprocess.py | 4 ++-- uvicorn/_subprocess.py | 44 +++++++++++++++++++++++++++++++++++----- uvicorn/config.py | 2 ++ uvicorn/main.py | 19 +++++++++++++---- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/tests/test_subprocess.py b/tests/test_subprocess.py index 93191bacb..7b5d0ef40 100644 --- a/tests/test_subprocess.py +++ b/tests/test_subprocess.py @@ -3,7 +3,7 @@ import socket from unittest.mock import patch -from uvicorn._subprocess import SpawnProcess, get_subprocess, subprocess_started +from uvicorn._subprocess import SocketSharePickle, SpawnProcess, get_subprocess, subprocess_started from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope from uvicorn.config import Config @@ -36,7 +36,7 @@ def test_subprocess_started() -> None: with patch("tests.test_subprocess.server_run") as mock_run: with patch.object(config, "configure_logging") as mock_config_logging: - subprocess_started(config, server_run, [fdsock], None) + subprocess_started(config, server_run, [SocketSharePickle(fdsock)], None) mock_run.assert_called_once() mock_config_logging.assert_called_once() diff --git a/uvicorn/_subprocess.py b/uvicorn/_subprocess.py index 1c06844de..ea99681b6 100644 --- a/uvicorn/_subprocess.py +++ b/uvicorn/_subprocess.py @@ -7,9 +7,9 @@ import multiprocessing import os +import socket import sys from multiprocessing.context import SpawnProcess -from socket import socket from typing import Callable from uvicorn.config import Config @@ -18,10 +18,39 @@ spawn = multiprocessing.get_context("spawn") +class SocketSharePickle: + def __init__(self, sock: socket.socket): + self._sock = sock + + def get(self) -> socket.socket: + return self._sock + + +class SocketShareRebind: + def __init__(self, sock: socket.socket): + if (sys.platform == "linux" and hasattr(socket, "SO_REUSEPORT")) or hasattr(socket, "SO_REUSEPORT_LB"): + raise RuntimeError("socket_load_balance not supported") + sock.setsockopt(socket.SOL_SOCKET, getattr(socket, "SO_REUSEPORT_LB", socket.SO_REUSEPORT), 1) + self._family = sock.family + self._sockname = sock.getsockname() + + def get(self) -> socket.socket: + try: + sock = socket.socket(family=self._family) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.SOL_SOCKET, getattr(socket, "SO_REUSEPORT_LB", socket.SO_REUSEPORT), 1) + + sock.bind(self._sockname) + return sock + except BaseException: + sock.close() + raise + + def get_subprocess( config: Config, target: Callable[..., None], - sockets: list[socket], + sockets: list[socket.socket], ) -> SpawnProcess: """ Called in the parent process, to instantiate a new child process instance. @@ -41,10 +70,15 @@ def get_subprocess( except (AttributeError, OSError): stdin_fileno = None + socket_shares: list[SocketShareRebind] | list[SocketSharePickle] + if config.socket_load_balance: + socket_shares = [SocketShareRebind(s) for s in sockets] + else: + socket_shares = [SocketSharePickle(s) for s in sockets] kwargs = { "config": config, "target": target, - "sockets": sockets, + "sockets": socket_shares, "stdin_fileno": stdin_fileno, } @@ -54,7 +88,7 @@ def get_subprocess( def subprocess_started( config: Config, target: Callable[..., None], - sockets: list[socket], + sockets: list[SocketSharePickle] | list[SocketShareRebind], stdin_fileno: int | None, ) -> None: """ @@ -77,7 +111,7 @@ def subprocess_started( try: # Now we can call into `Server.run(sockets=sockets)` - target(sockets=sockets) + target(sockets=[s.get() for s in sockets]) except KeyboardInterrupt: # pragma: no cover # supress the exception to avoid a traceback from subprocess.Popen # the parent already expects us to end, so no vital information is lost diff --git a/uvicorn/config.py b/uvicorn/config.py index 9aff8c968..54a2f9b3f 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -223,6 +223,7 @@ def __init__( headers: list[tuple[str, str]] | None = None, factory: bool = False, h11_max_incomplete_event_size: int | None = None, + socket_load_balance: bool = False, ): self.app = app self.host = host @@ -268,6 +269,7 @@ def __init__( self.encoded_headers: list[tuple[bytes, bytes]] = [] self.factory = factory self.h11_max_incomplete_event_size = h11_max_incomplete_event_size + self.socket_load_balance = socket_load_balance self.loaded = False self.configure_logging() diff --git a/uvicorn/main.py b/uvicorn/main.py index 43956622d..755fb4a06 100644 --- a/uvicorn/main.py +++ b/uvicorn/main.py @@ -360,6 +360,13 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No help="Treat APP as an application factory, i.e. a () -> callable.", show_default=True, ) +@click.option( + "--socket-load-balance", + is_flag=True, + default=False, + help="Use kernel support for socket load balancing", + show_default=True, +) def main( app: str, host: str, @@ -408,6 +415,7 @@ def main( app_dir: str, h11_max_incomplete_event_size: int | None, factory: bool, + socket_load_balance: bool = False, ) -> None: run( app, @@ -457,6 +465,7 @@ def main( factory=factory, app_dir=app_dir, h11_max_incomplete_event_size=h11_max_incomplete_event_size, + socket_load_balance=socket_load_balance, ) @@ -509,6 +518,7 @@ def run( app_dir: str | None = None, factory: bool = False, h11_max_incomplete_event_size: int | None = None, + socket_load_balance: bool = False, ) -> None: if app_dir is not None: sys.path.insert(0, app_dir) @@ -560,6 +570,7 @@ def run( use_colors=use_colors, factory=factory, h11_max_incomplete_event_size=h11_max_incomplete_event_size, + socket_load_balance=socket_load_balance, ) server = Server(config=config) @@ -570,11 +581,11 @@ def run( try: if config.should_reload: - sock = config.bind_socket() - ChangeReload(config, target=server.run, sockets=[sock]).run() + with config.bind_socket() as sock: + ChangeReload(config, target=server.run, sockets=[sock]).run() elif config.workers > 1: - sock = config.bind_socket() - Multiprocess(config, target=server.run, sockets=[sock]).run() + with config.bind_socket() as sock: + Multiprocess(config, target=server.run, sockets=[sock]).run() else: server.run() except KeyboardInterrupt: From 1550e7a10d3d974f55ff93dde22bb0316e120ab3 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 1 Oct 2024 10:12:10 +0100 Subject: [PATCH 02/13] fix it so it actually runs --- uvicorn/_subprocess.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/uvicorn/_subprocess.py b/uvicorn/_subprocess.py index ea99681b6..8390f83ce 100644 --- a/uvicorn/_subprocess.py +++ b/uvicorn/_subprocess.py @@ -28,15 +28,17 @@ def get(self) -> socket.socket: class SocketShareRebind: def __init__(self, sock: socket.socket): - if (sys.platform == "linux" and hasattr(socket, "SO_REUSEPORT")) or hasattr(socket, "SO_REUSEPORT_LB"): + if not (sys.platform == "linux" and hasattr(socket, "SO_REUSEPORT")) or hasattr(socket, "SO_REUSEPORT_LB"): raise RuntimeError("socket_load_balance not supported") sock.setsockopt(socket.SOL_SOCKET, getattr(socket, "SO_REUSEPORT_LB", socket.SO_REUSEPORT), 1) self._family = sock.family + self._type = sock.type + self._proto = sock.proto self._sockname = sock.getsockname() def get(self) -> socket.socket: try: - sock = socket.socket(family=self._family) + sock = socket.socket(family=self._family, type=self._type, proto=self._proto) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, getattr(socket, "SO_REUSEPORT_LB", socket.SO_REUSEPORT), 1) From 4a0785af6541dff4a1985794e18549ee8a5cfead Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 1 Oct 2024 10:12:27 +0100 Subject: [PATCH 03/13] add a test --- tests/supervisors/test_multiprocess.py | 61 +++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/tests/supervisors/test_multiprocess.py b/tests/supervisors/test_multiprocess.py index e1f594efe..fdfab920a 100644 --- a/tests/supervisors/test_multiprocess.py +++ b/tests/supervisors/test_multiprocess.py @@ -1,17 +1,21 @@ from __future__ import annotations import functools +import multiprocessing import os import signal import socket +import sys import threading import time -from typing import Any, Callable +from typing import Any, Callable, TypeVar +import httpx import pytest from uvicorn import Config from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope +from uvicorn.server import Server from uvicorn.supervisors import Multiprocess from uvicorn.supervisors.multiprocess import Process @@ -169,3 +173,58 @@ def test_multiprocess_sigttou() -> None: assert len(supervisor.processes) == 1 supervisor.signal_queue.append(signal.SIGINT) supervisor.join_all() + + +T = TypeVar("T") + + +class Box: + def __init__(self, v: T): + self.v = v + + +async def lb_app( + d: multiprocessing.managers.DictProxy, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable +) -> None: + if scope["type"] == "lifespan": + await receive() + print("lifespan started") + scope["state"]["count"] = box = Box(0) + await send({"type": "lifespan.startup.complete"}) + await receive() + d[os.getpid()] = box.v + await send({"type": "lifespan.shutdown.complete"}) + return + + scope["state"]["count"].v += 1 + headers = [(b"content-type", b"text/plain")] + await send({"type": "http.response.start", "status": 200, "headers": headers}) + await send({"type": "http.response.body", "body": b"hello"}) + + +@pytest.mark.skipif( + not ((sys.platform == "linux" and hasattr(socket, "SO_REUSEPORT")) or hasattr(socket, "SO_REUSEPORT_LB")), + reason="unsupported", + strict=True, + raises=RuntimeError, +) +def test_multiprocess_socket_balance() -> None: + with multiprocessing.Manager() as m: + d = m.dict() + app = functools.partial(lb_app, d) + config = Config(app=app, workers=2, socket_load_balance=True, port=0, interface="asgi3") + server = Server(config=config) + with config.bind_socket() as sock: + port = sock.getsockname()[1] + try: + supervisor = Multiprocess(config, target=server.run, sockets=[sock]) + threading.Thread(target=supervisor.run, daemon=True).start() + time.sleep(1) + with httpx.Client(): + for i in range(100): + httpx.get(f"http://localhost:{port}/").raise_for_status() + finally: + supervisor.signal_queue.append(signal.SIGINT) + supervisor.join_all() + min_conn, max_conn = sorted(d.values()) + assert max_conn - min_conn < 20 From 2a9b20a7fa00203b05651563fa3c4d11af0da39b Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 1 Oct 2024 10:16:04 +0100 Subject: [PATCH 04/13] mypy fix --- tests/supervisors/test_multiprocess.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/supervisors/test_multiprocess.py b/tests/supervisors/test_multiprocess.py index fdfab920a..1e1a978b2 100644 --- a/tests/supervisors/test_multiprocess.py +++ b/tests/supervisors/test_multiprocess.py @@ -205,7 +205,6 @@ async def lb_app( @pytest.mark.skipif( not ((sys.platform == "linux" and hasattr(socket, "SO_REUSEPORT")) or hasattr(socket, "SO_REUSEPORT_LB")), reason="unsupported", - strict=True, raises=RuntimeError, ) def test_multiprocess_socket_balance() -> None: From 0af6480dd6830602bbc650fa46e2b297aa755d6c Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 1 Oct 2024 10:19:45 +0100 Subject: [PATCH 05/13] mypy fix 2 --- tests/supervisors/test_multiprocess.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/supervisors/test_multiprocess.py b/tests/supervisors/test_multiprocess.py index 1e1a978b2..f13accac8 100644 --- a/tests/supervisors/test_multiprocess.py +++ b/tests/supervisors/test_multiprocess.py @@ -205,7 +205,6 @@ async def lb_app( @pytest.mark.skipif( not ((sys.platform == "linux" and hasattr(socket, "SO_REUSEPORT")) or hasattr(socket, "SO_REUSEPORT_LB")), reason="unsupported", - raises=RuntimeError, ) def test_multiprocess_socket_balance() -> None: with multiprocessing.Manager() as m: From d9c117a2f3c0c9bae078a1b7db1a149fba8ad324 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 1 Oct 2024 10:26:06 +0100 Subject: [PATCH 06/13] add cli usage --- docs/deployment.md | 1 + docs/index.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/deployment.md b/docs/deployment.md index e1854deff..212d40265 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -135,6 +135,7 @@ Options: buffer of an incomplete event. --factory Treat APP as an application factory, i.e. a () -> callable. + --socket-load-balance Use kernel support for socket load balancing --help Show this message and exit. ``` diff --git a/docs/index.md b/docs/index.md index bb6fc321a..e9bf5b3b5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -205,6 +205,7 @@ Options: buffer of an incomplete event. --factory Treat APP as an application factory, i.e. a () -> callable. + --socket-load-balance Use kernel support for socket load balancing --help Show this message and exit. ``` From 66afb6229a5f232c42278b2371aec76943e173b9 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 1 Oct 2024 10:39:16 +0100 Subject: [PATCH 07/13] remove print and sleep --- tests/supervisors/test_multiprocess.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/supervisors/test_multiprocess.py b/tests/supervisors/test_multiprocess.py index f13accac8..f9a7e7702 100644 --- a/tests/supervisors/test_multiprocess.py +++ b/tests/supervisors/test_multiprocess.py @@ -1,7 +1,7 @@ from __future__ import annotations import functools -import multiprocessing +import multiprocessing.managers import os import signal import socket @@ -184,13 +184,17 @@ def __init__(self, v: T): async def lb_app( - d: multiprocessing.managers.DictProxy, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable + d: multiprocessing.managers.DictProxy, + started: threading.Event, + scope: Scope, + receive: ASGIReceiveCallable, + send: ASGISendCallable, ) -> None: if scope["type"] == "lifespan": await receive() - print("lifespan started") scope["state"]["count"] = box = Box(0) await send({"type": "lifespan.startup.complete"}) + started.set() await receive() d[os.getpid()] = box.v await send({"type": "lifespan.shutdown.complete"}) @@ -208,8 +212,9 @@ async def lb_app( ) def test_multiprocess_socket_balance() -> None: with multiprocessing.Manager() as m: + started = m.Event() d = m.dict() - app = functools.partial(lb_app, d) + app = functools.partial(lb_app, d, started) config = Config(app=app, workers=2, socket_load_balance=True, port=0, interface="asgi3") server = Server(config=config) with config.bind_socket() as sock: @@ -217,7 +222,8 @@ def test_multiprocess_socket_balance() -> None: try: supervisor = Multiprocess(config, target=server.run, sockets=[sock]) threading.Thread(target=supervisor.run, daemon=True).start() - time.sleep(1) + if not started.wait(timeout=5): + raise TimeoutError with httpx.Client(): for i in range(100): httpx.get(f"http://localhost:{port}/").raise_for_status() @@ -225,4 +231,4 @@ def test_multiprocess_socket_balance() -> None: supervisor.signal_queue.append(signal.SIGINT) supervisor.join_all() min_conn, max_conn = sorted(d.values()) - assert max_conn - min_conn < 20 + assert (max_conn - min_conn) < 25 From 4166deed90676468dd564ab51f8c264ceba4a05a Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 1 Oct 2024 10:48:00 +0100 Subject: [PATCH 08/13] add coverage pragmas --- tests/supervisors/test_multiprocess.py | 4 ++-- uvicorn/_subprocess.py | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/supervisors/test_multiprocess.py b/tests/supervisors/test_multiprocess.py index f9a7e7702..1860c9ccb 100644 --- a/tests/supervisors/test_multiprocess.py +++ b/tests/supervisors/test_multiprocess.py @@ -189,7 +189,7 @@ async def lb_app( scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable, -) -> None: +) -> None: # pragma: py-darwin pragma: py-win32 if scope["type"] == "lifespan": await receive() scope["state"]["count"] = box = Box(0) @@ -210,7 +210,7 @@ async def lb_app( not ((sys.platform == "linux" and hasattr(socket, "SO_REUSEPORT")) or hasattr(socket, "SO_REUSEPORT_LB")), reason="unsupported", ) -def test_multiprocess_socket_balance() -> None: +def test_multiprocess_socket_balance() -> None: # pragma: py-darwin pragma: py-win32 with multiprocessing.Manager() as m: started = m.Event() d = m.dict() diff --git a/uvicorn/_subprocess.py b/uvicorn/_subprocess.py index 8390f83ce..1433cb6fa 100644 --- a/uvicorn/_subprocess.py +++ b/uvicorn/_subprocess.py @@ -30,13 +30,14 @@ class SocketShareRebind: def __init__(self, sock: socket.socket): if not (sys.platform == "linux" and hasattr(socket, "SO_REUSEPORT")) or hasattr(socket, "SO_REUSEPORT_LB"): raise RuntimeError("socket_load_balance not supported") - sock.setsockopt(socket.SOL_SOCKET, getattr(socket, "SO_REUSEPORT_LB", socket.SO_REUSEPORT), 1) - self._family = sock.family - self._type = sock.type - self._proto = sock.proto - self._sockname = sock.getsockname() + else: # pragma: py-darwin pragma: py-win32 + sock.setsockopt(socket.SOL_SOCKET, getattr(socket, "SO_REUSEPORT_LB", socket.SO_REUSEPORT), 1) + self._family = sock.family + self._type = sock.type + self._proto = sock.proto + self._sockname = sock.getsockname() - def get(self) -> socket.socket: + def get(self) -> socket.socket: # pragma: py-darwin pragma: py-win32 try: sock = socket.socket(family=self._family, type=self._type, proto=self._proto) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -73,7 +74,7 @@ def get_subprocess( stdin_fileno = None socket_shares: list[SocketShareRebind] | list[SocketSharePickle] - if config.socket_load_balance: + if config.socket_load_balance: # pragma: py-darwin pragma: py-win32 socket_shares = [SocketShareRebind(s) for s in sockets] else: socket_shares = [SocketSharePickle(s) for s in sockets] From 77a4a23da473b0eb8d497535f6ba767140f05872 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 1 Oct 2024 10:54:08 +0100 Subject: [PATCH 09/13] change box to dataclass to avoid coverage requirement --- tests/supervisors/test_multiprocess.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/supervisors/test_multiprocess.py b/tests/supervisors/test_multiprocess.py index 1860c9ccb..cd1b037f3 100644 --- a/tests/supervisors/test_multiprocess.py +++ b/tests/supervisors/test_multiprocess.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dataclasses import functools import multiprocessing.managers import os @@ -8,7 +9,7 @@ import sys import threading import time -from typing import Any, Callable, TypeVar +from typing import Any, Callable, Generic, TypeVar import httpx import pytest @@ -178,9 +179,9 @@ def test_multiprocess_sigttou() -> None: T = TypeVar("T") -class Box: - def __init__(self, v: T): - self.v = v +@dataclasses.dataclass +class Box(Generic[T]): + v: T async def lb_app( From d5a1f59be41c6f904abc2b59371e4579895e9648 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 1 Oct 2024 10:58:59 +0100 Subject: [PATCH 10/13] test unsupported --- tests/supervisors/test_multiprocess.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/supervisors/test_multiprocess.py b/tests/supervisors/test_multiprocess.py index cd1b037f3..5ab02e018 100644 --- a/tests/supervisors/test_multiprocess.py +++ b/tests/supervisors/test_multiprocess.py @@ -233,3 +233,12 @@ def test_multiprocess_socket_balance() -> None: # pragma: py-darwin pragma: py- supervisor.join_all() min_conn, max_conn = sorted(d.values()) assert (max_conn - min_conn) < 25 + + +def test_multiprocess_not_supported(monkeypatch): + monkeypatch.delattr(socket, "SO_REUSEPORT") + config = Config(app=app, workers=2, socket_load_balance=True, port=0, interface="asgi3") + with config.bind_socket() as sock: + supervisor = Multiprocess(config, target=run, sockets=[sock]) + with pytest.raises(RuntimeError, match="socket_load_balance not supported"): + supervisor.run() From bb6dc58eec8a56a1c3220d57fab8dc5a09d99387 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 1 Oct 2024 11:25:33 +0100 Subject: [PATCH 11/13] try to collect coverage --- pyproject.toml | 2 ++ scripts/coverage | 1 + tests/supervisors/test_multiprocess.py | 2 +- tests/supervisors/test_reload.py | 2 +- uvicorn/_subprocess.py | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 840ada3a8..3d9b161e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,6 +103,8 @@ filterwarnings = [ source_pkgs = ["uvicorn", "tests"] plugins = ["coverage_conditional_plugin"] omit = ["uvicorn/workers.py", "uvicorn/__main__.py"] +concurrency = ["multiprocessing", "thread"] +parallel = true [tool.coverage.report] precision = 2 diff --git a/scripts/coverage b/scripts/coverage index c93e45e85..2d6ea60ee 100755 --- a/scripts/coverage +++ b/scripts/coverage @@ -8,4 +8,5 @@ export SOURCE_FILES="uvicorn tests" set -x +${PREFIX}coverage combine ${PREFIX}coverage report diff --git a/tests/supervisors/test_multiprocess.py b/tests/supervisors/test_multiprocess.py index 5ab02e018..540c1748a 100644 --- a/tests/supervisors/test_multiprocess.py +++ b/tests/supervisors/test_multiprocess.py @@ -223,7 +223,7 @@ def test_multiprocess_socket_balance() -> None: # pragma: py-darwin pragma: py- try: supervisor = Multiprocess(config, target=server.run, sockets=[sock]) threading.Thread(target=supervisor.run, daemon=True).start() - if not started.wait(timeout=5): + if not started.wait(timeout=5): # pragma: no cover raise TimeoutError with httpx.Client(): for i in range(100): diff --git a/tests/supervisors/test_reload.py b/tests/supervisors/test_reload.py index 30eea2321..cbe06f6d1 100644 --- a/tests/supervisors/test_reload.py +++ b/tests/supervisors/test_reload.py @@ -242,7 +242,7 @@ def test_override_defaults(self, touch_soon) -> None: reload=True, # We need to add *.txt otherwise no regular files will match reload_includes=[".*", "*.txt"], - reload_excludes=["*.py"], + reload_excludes=["*.py", ".coverage.*"], ) reloader = self._setup_reloader(config) diff --git a/uvicorn/_subprocess.py b/uvicorn/_subprocess.py index 1433cb6fa..65c0c7158 100644 --- a/uvicorn/_subprocess.py +++ b/uvicorn/_subprocess.py @@ -45,7 +45,7 @@ def get(self) -> socket.socket: # pragma: py-darwin pragma: py-win32 sock.bind(self._sockname) return sock - except BaseException: + except BaseException: # pragma: no cover sock.close() raise From 47924f253fd10fef64b7076babe0b795d72856e2 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 1 Oct 2024 12:05:48 +0100 Subject: [PATCH 12/13] enable sigterm hook --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 3d9b161e2..7142ee758 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,6 +105,7 @@ plugins = ["coverage_conditional_plugin"] omit = ["uvicorn/workers.py", "uvicorn/__main__.py"] concurrency = ["multiprocessing", "thread"] parallel = true +sigterm = true [tool.coverage.report] precision = 2 From 577c0570eb4a2a4697215bce10a04a817209aa61 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 1 Oct 2024 12:06:29 +0100 Subject: [PATCH 13/13] ignore coverage files that show up in reloader tests --- tests/supervisors/test_reload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/supervisors/test_reload.py b/tests/supervisors/test_reload.py index cbe06f6d1..e54422898 100644 --- a/tests/supervisors/test_reload.py +++ b/tests/supervisors/test_reload.py @@ -157,7 +157,7 @@ def test_should_not_reload_when_exclude_pattern_match_file_is_changed(self, touc app="tests.test_config:asgi_app", reload=True, reload_includes=["*"], - reload_excludes=["*.js"], + reload_excludes=["*.js", ".coverage.*"], ) reloader = self._setup_reloader(config)