-
-
Notifications
You must be signed in to change notification settings - Fork 803
Support custom IOLOOPs #2435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Support custom IOLOOPs #2435
Changes from all commits
208a37a
8021788
db8f9a1
6247222
a8df5b9
849169f
2dafe54
100d1ad
fcb1cbe
66c61cd
994955d
30b8093
d9db4e4
16c120b
dc2c956
dc77349
4d1dec7
606dbd9
165bda1
bd58e8a
5d629fa
58419af
2c0f7c1
765b1e4
b41ae15
91a6a4e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from __future__ import annotations | ||
|
||
import asyncio | ||
|
||
|
||
class CustomLoop(asyncio.SelectorEventLoop): | ||
pass |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from __future__ import annotations | ||
|
||
import asyncio | ||
from asyncio import AbstractEventLoop | ||
|
||
import pytest | ||
|
||
from tests.custom_loop_utils import CustomLoop | ||
from tests.utils import get_asyncio_default_loop_per_os | ||
from uvicorn._compat import asyncio_run | ||
|
||
|
||
async def assert_event_loop(expected_loop_class: type[AbstractEventLoop]): | ||
assert isinstance(asyncio.get_running_loop(), expected_loop_class) | ||
|
||
|
||
def test_asyncio_run__default_loop_factory() -> None: | ||
asyncio_run(assert_event_loop(get_asyncio_default_loop_per_os()), loop_factory=None) | ||
|
||
|
||
def test_asyncio_run__custom_loop_factory() -> None: | ||
asyncio_run(assert_event_loop(CustomLoop), loop_factory=CustomLoop) | ||
|
||
|
||
def test_asyncio_run__passing_a_non_awaitable_callback_should_throw_error() -> None: | ||
with pytest.raises(ValueError): | ||
asyncio_run( | ||
lambda: None, # type: ignore | ||
loop_factory=CustomLoop, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
from __future__ import annotations | ||
|
||
import asyncio | ||
import sys | ||
from collections.abc import Callable, Coroutine | ||
from typing import Any, TypeVar | ||
|
||
_T = TypeVar("_T") | ||
|
||
if sys.version_info >= (3, 12): | ||
asyncio_run = asyncio.run | ||
elif sys.version_info >= (3, 11): | ||
|
||
def asyncio_run( | ||
main: Coroutine[Any, Any, _T], | ||
*, | ||
debug: bool = False, | ||
loop_factory: Callable[[], asyncio.AbstractEventLoop] | None = None, | ||
) -> _T: | ||
# asyncio.run from Python 3.12 | ||
# https://docs.python.org/3/license.html#psf-license | ||
with asyncio.Runner(debug=debug, loop_factory=loop_factory) as runner: | ||
return runner.run(main) | ||
|
||
else: | ||
# modified version of asyncio.run from Python 3.10 to add loop_factory kwarg | ||
# https://docs.python.org/3/license.html#psf-license | ||
def asyncio_run( | ||
main: Coroutine[Any, Any, _T], | ||
*, | ||
debug: bool = False, | ||
loop_factory: Callable[[], asyncio.AbstractEventLoop] | None = None, | ||
) -> _T: | ||
try: | ||
asyncio.get_running_loop() | ||
except RuntimeError: | ||
pass | ||
else: | ||
raise RuntimeError("asyncio.run() cannot be called from a running event loop") | ||
|
||
if not asyncio.iscoroutine(main): | ||
raise ValueError(f"a coroutine was expected, got {main!r}") | ||
|
||
if loop_factory is None: | ||
loop = asyncio.new_event_loop() | ||
else: | ||
loop = loop_factory() | ||
try: | ||
if loop_factory is None: | ||
asyncio.set_event_loop(loop) | ||
if debug is not None: | ||
loop.set_debug(debug) | ||
return loop.run_until_complete(main) | ||
finally: | ||
try: | ||
_cancel_all_tasks(loop) | ||
loop.run_until_complete(loop.shutdown_asyncgens()) | ||
loop.run_until_complete(loop.shutdown_default_executor()) | ||
finally: | ||
if loop_factory is None: | ||
asyncio.set_event_loop(None) | ||
loop.close() | ||
|
||
def _cancel_all_tasks(loop: asyncio.AbstractEventLoop) -> None: | ||
to_cancel = asyncio.all_tasks(loop) | ||
if not to_cancel: | ||
return | ||
|
||
for task in to_cancel: | ||
task.cancel() | ||
|
||
loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) | ||
|
||
for task in to_cancel: | ||
if task.cancelled(): | ||
continue | ||
if task.exception() is not None: | ||
loop.call_exception_handler( | ||
{ | ||
"message": "unhandled exception during asyncio.run() shutdown", | ||
"exception": task.exception(), | ||
"task": task, | ||
} | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ | |
HTTPProtocolType = Literal["auto", "h11", "httptools"] | ||
WSProtocolType = Literal["auto", "none", "websockets", "wsproto"] | ||
LifespanType = Literal["auto", "on", "off"] | ||
LoopSetupType = Literal["none", "auto", "asyncio", "uvloop"] | ||
LoopFactoryType = Literal["none", "auto", "asyncio", "uvloop"] | ||
InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"] | ||
|
||
LOG_LEVELS: dict[str, int] = { | ||
|
@@ -54,11 +54,11 @@ | |
"on": "uvicorn.lifespan.on:LifespanOn", | ||
"off": "uvicorn.lifespan.off:LifespanOff", | ||
} | ||
LOOP_SETUPS: dict[LoopSetupType, str | None] = { | ||
LOOP_FACTORIES: dict[str, str | None] = { | ||
"none": None, | ||
"auto": "uvicorn.loops.auto:auto_loop_setup", | ||
"asyncio": "uvicorn.loops.asyncio:asyncio_setup", | ||
"uvloop": "uvicorn.loops.uvloop:uvloop_setup", | ||
"auto": "uvicorn.loops.auto:auto_loop_factory", | ||
"asyncio": "uvicorn.loops.asyncio:asyncio_loop_factory", | ||
"uvloop": "uvicorn.loops.uvloop:uvloop_loop_factory", | ||
} | ||
INTERFACES: list[InterfaceType] = ["auto", "asgi3", "asgi2", "wsgi"] | ||
|
||
|
@@ -181,7 +181,7 @@ def __init__( | |
port: int = 8000, | ||
uds: str | None = None, | ||
fd: int | None = None, | ||
loop: LoopSetupType = "auto", | ||
loop: str = "auto", | ||
http: type[asyncio.Protocol] | HTTPProtocolType = "auto", | ||
ws: type[asyncio.Protocol] | WSProtocolType = "auto", | ||
ws_max_size: int = 16 * 1024 * 1024, | ||
|
@@ -472,10 +472,18 @@ def load(self) -> None: | |
|
||
self.loaded = True | ||
|
||
def setup_event_loop(self) -> None: | ||
loop_setup: Callable | None = import_from_string(LOOP_SETUPS[self.loop]) | ||
if loop_setup is not None: | ||
loop_setup(use_subprocess=self.use_subprocess) | ||
def get_loop_factory(self) -> Callable[[], asyncio.AbstractEventLoop] | None: | ||
if self.loop in LOOP_FACTORIES: | ||
loop_factory: Callable | None = import_from_string(LOOP_FACTORIES[self.loop]) | ||
else: | ||
try: | ||
return import_from_string(self.loop) | ||
except ImportFromStringError as exc: | ||
logger.error("Error loading custom loop setup function. %s" % exc) | ||
sys.exit(1) | ||
if loop_factory is None: | ||
return None | ||
return loop_factory(use_subprocess=self.use_subprocess) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. an importable loop factory should not take self.use_subprocess this is only needed to choose a selector event loop on windows |
||
|
||
def bind_socket(self) -> socket.socket: | ||
logger_args: list[str | int] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,11 @@ | ||
from __future__ import annotations | ||
|
||
import asyncio | ||
import logging | ||
import sys | ||
|
||
logger = logging.getLogger("uvicorn.error") | ||
from collections.abc import Callable | ||
|
||
|
||
def asyncio_setup(use_subprocess: bool = False) -> None: | ||
if sys.platform == "win32" and use_subprocess: | ||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # pragma: full coverage | ||
def asyncio_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]: | ||
if sys.platform == "win32" and not use_subprocess: | ||
return asyncio.ProactorEventLoop | ||
Comment on lines
+9
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reference for me later: python/cpython#122240 |
||
return asyncio.SelectorEventLoop |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,17 @@ | ||
def auto_loop_setup(use_subprocess: bool = False) -> None: | ||
from __future__ import annotations | ||
|
||
import asyncio | ||
from collections.abc import Callable | ||
|
||
|
||
def auto_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]: | ||
try: | ||
import uvloop # noqa | ||
except ImportError: # pragma: no cover | ||
from uvicorn.loops.asyncio import asyncio_setup as loop_setup | ||
from uvicorn.loops.asyncio import asyncio_loop_factory as loop_factory | ||
|
||
loop_setup(use_subprocess=use_subprocess) | ||
return loop_factory(use_subprocess=use_subprocess) | ||
else: # pragma: no cover | ||
from uvicorn.loops.uvloop import uvloop_setup | ||
from uvicorn.loops.uvloop import uvloop_loop_factory | ||
|
||
uvloop_setup(use_subprocess=use_subprocess) | ||
return uvloop_loop_factory(use_subprocess=use_subprocess) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,10 @@ | ||
from __future__ import annotations | ||
|
||
import asyncio | ||
from collections.abc import Callable | ||
|
||
import uvloop | ||
|
||
|
||
def uvloop_setup(use_subprocess: bool = False) -> None: | ||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) | ||
def uvloop_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]: | ||
return uvloop.new_event_loop |
Uh oh!
There was an error while loading. Please reload this page.