Optional uvloop use (#2264)
Co-authored-by: Adam Hopkins <adam@amhopkins.com> Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
This commit is contained in:
@@ -2,16 +2,20 @@ import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
from collections import Counter
|
||||
from inspect import isawaitable
|
||||
from os import environ
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
import sanic
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.compat import OS_IS_WINDOWS, UVLOOP_INSTALLED
|
||||
from sanic.config import Config
|
||||
from sanic.exceptions import SanicException
|
||||
from sanic.response import text
|
||||
from sanic.helpers import _default
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -19,15 +23,6 @@ def clear_app_registry():
|
||||
Sanic._app_registry = {}
|
||||
|
||||
|
||||
def uvloop_installed():
|
||||
try:
|
||||
import uvloop # noqa
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def test_app_loop_running(app):
|
||||
@app.get("/test")
|
||||
async def handler(request):
|
||||
@@ -472,6 +467,102 @@ def test_custom_context():
|
||||
assert app.ctx == ctx
|
||||
|
||||
|
||||
def test_uvloop_config(app, monkeypatch):
|
||||
@app.get("/test")
|
||||
def handler(request):
|
||||
return text("ok")
|
||||
|
||||
try_use_uvloop = Mock()
|
||||
monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop)
|
||||
|
||||
# Default config
|
||||
app.test_client.get("/test")
|
||||
if OS_IS_WINDOWS:
|
||||
try_use_uvloop.assert_not_called()
|
||||
else:
|
||||
try_use_uvloop.assert_called_once()
|
||||
|
||||
try_use_uvloop.reset_mock()
|
||||
app.config["USE_UVLOOP"] = False
|
||||
app.test_client.get("/test")
|
||||
try_use_uvloop.assert_not_called()
|
||||
|
||||
try_use_uvloop.reset_mock()
|
||||
app.config["USE_UVLOOP"] = True
|
||||
app.test_client.get("/test")
|
||||
try_use_uvloop.assert_called_once()
|
||||
|
||||
|
||||
def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch):
|
||||
apps = (
|
||||
Sanic("default-uvloop"),
|
||||
Sanic("no-uvloop"),
|
||||
Sanic("yes-uvloop")
|
||||
)
|
||||
|
||||
apps[1].config.USE_UVLOOP = False
|
||||
apps[2].config.USE_UVLOOP = True
|
||||
|
||||
try_use_uvloop = Mock()
|
||||
monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
for app in apps:
|
||||
srv_coro = app.create_server(
|
||||
return_asyncio_server=True,
|
||||
asyncio_server_kwargs=dict(start_serving=False)
|
||||
)
|
||||
loop.run_until_complete(srv_coro)
|
||||
|
||||
try_use_uvloop.assert_not_called() # Check it didn't try to change policy
|
||||
|
||||
message = (
|
||||
"You are trying to change the uvloop configuration, but "
|
||||
"this is only effective when using the run(...) method. "
|
||||
"When using the create_server(...) method Sanic will use "
|
||||
"the already existing loop."
|
||||
)
|
||||
|
||||
counter = Counter([(r[1], r[2]) for r in caplog.record_tuples])
|
||||
modified = sum(1 for app in apps if app.config.USE_UVLOOP is not _default)
|
||||
|
||||
assert counter[(logging.WARNING, message)] == modified
|
||||
|
||||
|
||||
def test_multiple_uvloop_configs_display_warning(caplog):
|
||||
Sanic._uvloop_setting = None # Reset the setting (changed in prev tests)
|
||||
|
||||
default_uvloop = Sanic("default-uvloop")
|
||||
no_uvloop = Sanic("no-uvloop")
|
||||
yes_uvloop = Sanic("yes-uvloop")
|
||||
|
||||
no_uvloop.config.USE_UVLOOP = False
|
||||
yes_uvloop.config.USE_UVLOOP = True
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
for app in (default_uvloop, no_uvloop, yes_uvloop):
|
||||
srv_coro = app.create_server(
|
||||
return_asyncio_server=True,
|
||||
asyncio_server_kwargs=dict(start_serving=False)
|
||||
)
|
||||
srv = loop.run_until_complete(srv_coro)
|
||||
loop.run_until_complete(srv.startup())
|
||||
|
||||
message = (
|
||||
"It looks like you're running several apps with different "
|
||||
"uvloop settings. This is not supported and may lead to "
|
||||
"unintended behaviour."
|
||||
)
|
||||
|
||||
counter = Counter([(r[1], r[2]) for r in caplog.record_tuples])
|
||||
|
||||
assert counter[(logging.WARNING, message)] == 2
|
||||
|
||||
|
||||
def test_cannot_run_fast_and_workers(app):
|
||||
message = "You cannot use both fast=True and workers=X"
|
||||
with pytest.raises(RuntimeError, match=message):
|
||||
|
||||
@@ -145,6 +145,37 @@ def test_listeners_triggered_async(app):
|
||||
assert after_server_stop
|
||||
|
||||
|
||||
def test_non_default_uvloop_config_raises_warning(app):
|
||||
app.config.USE_UVLOOP = True
|
||||
|
||||
class CustomServer(uvicorn.Server):
|
||||
def install_signal_handlers(self):
|
||||
pass
|
||||
|
||||
config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0)
|
||||
server = CustomServer(config=config)
|
||||
|
||||
with pytest.warns(UserWarning) as records:
|
||||
server.run()
|
||||
|
||||
all_tasks = asyncio.all_tasks(asyncio.get_event_loop())
|
||||
for task in all_tasks:
|
||||
task.cancel()
|
||||
|
||||
msg = ""
|
||||
for record in records:
|
||||
_msg = str(record.message)
|
||||
if _msg.startswith("You have set the USE_UVLOOP configuration"):
|
||||
msg = _msg
|
||||
break
|
||||
|
||||
assert msg == (
|
||||
"You have set the USE_UVLOOP configuration option, but Sanic "
|
||||
"cannot control the event loop when running in ASGI mode."
|
||||
"This option will be ignored."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mockprotocol_events(protocol):
|
||||
assert protocol._not_paused.is_set()
|
||||
|
||||
@@ -271,9 +271,13 @@ def test_exception_in_ws_logged(caplog):
|
||||
with caplog.at_level(logging.INFO):
|
||||
app.test_client.websocket("/feed")
|
||||
|
||||
error_logs = [r for r in caplog.record_tuples if r[0] == "sanic.error"]
|
||||
assert error_logs[1][1] == logging.ERROR
|
||||
assert "Exception occurred while handling uri:" in error_logs[1][2]
|
||||
for record in caplog.record_tuples:
|
||||
if record[2].startswith("Exception occurred"):
|
||||
break
|
||||
|
||||
assert record[0] == "sanic.error"
|
||||
assert record[1] == logging.ERROR
|
||||
assert "Exception occurred while handling uri:" in record[2]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("debug", (True, False))
|
||||
|
||||
@@ -2,7 +2,6 @@ import asyncio
|
||||
import logging
|
||||
import time
|
||||
|
||||
from collections import Counter
|
||||
from multiprocessing import Process
|
||||
|
||||
import httpx
|
||||
@@ -36,11 +35,14 @@ def test_no_exceptions_when_cancel_pending_request(app, caplog):
|
||||
|
||||
p.kill()
|
||||
|
||||
counter = Counter([r[1] for r in caplog.record_tuples])
|
||||
|
||||
assert counter[logging.INFO] == 11
|
||||
assert logging.ERROR not in counter
|
||||
assert (
|
||||
caplog.record_tuples[9][2]
|
||||
== "Request: GET http://127.0.0.1:8000/ stopped. Transport is closed."
|
||||
)
|
||||
info = 0
|
||||
for record in caplog.record_tuples:
|
||||
assert record[1] != logging.ERROR
|
||||
if record[1] == logging.INFO:
|
||||
info += 1
|
||||
if record[2].startswith("Request:"):
|
||||
assert record[2] == (
|
||||
"Request: GET http://127.0.0.1:8000/ stopped. "
|
||||
"Transport is closed."
|
||||
)
|
||||
assert info == 11
|
||||
|
||||
116
tests/test_server_loop.py
Normal file
116
tests/test_server_loop.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import logging
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.server import loop
|
||||
from sanic.compat import OS_IS_WINDOWS, UVLOOP_INSTALLED
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not OS_IS_WINDOWS,
|
||||
reason="Not testable with current client",
|
||||
)
|
||||
def test_raises_warning_if_os_is_windows(caplog):
|
||||
with caplog.at_level(logging.WARNING):
|
||||
loop.try_use_uvloop()
|
||||
|
||||
for record in caplog.records:
|
||||
if record.message.startswith("You are trying to use"):
|
||||
break
|
||||
|
||||
assert record.message == (
|
||||
"You are trying to use uvloop, but uvloop is not compatible "
|
||||
"with your system. You can disable uvloop completely by setting "
|
||||
"the 'USE_UVLOOP' configuration value to false, or simply not "
|
||||
"defining it and letting Sanic handle it for you. Sanic will now "
|
||||
"continue to run using the default event loop."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
OS_IS_WINDOWS or UVLOOP_INSTALLED,
|
||||
reason="Not testable with current client",
|
||||
)
|
||||
def test_raises_warning_if_uvloop_not_installed(caplog):
|
||||
with caplog.at_level(logging.WARNING):
|
||||
loop.try_use_uvloop()
|
||||
|
||||
for record in caplog.records:
|
||||
if record.message.startswith("You are trying to use"):
|
||||
break
|
||||
|
||||
assert record.message == (
|
||||
"You are trying to use uvloop, but uvloop is not "
|
||||
"installed in your system. In order to use uvloop "
|
||||
"you must first install it. Otherwise, you can disable "
|
||||
"uvloop completely by setting the 'USE_UVLOOP' "
|
||||
"configuration value to false. Sanic will now continue "
|
||||
"to run with the default event loop."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
OS_IS_WINDOWS or not UVLOOP_INSTALLED,
|
||||
reason="Not testable with current client",
|
||||
)
|
||||
def test_logs_when_install_and_runtime_config_mismatch(caplog, monkeypatch):
|
||||
getenv = Mock(return_value="no")
|
||||
monkeypatch.setattr(loop, "getenv", getenv)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
loop.try_use_uvloop()
|
||||
|
||||
getenv.assert_called_once_with("SANIC_NO_UVLOOP", "no")
|
||||
assert caplog.record_tuples == []
|
||||
|
||||
getenv = Mock(return_value="yes")
|
||||
monkeypatch.setattr(loop, "getenv", getenv)
|
||||
with caplog.at_level(logging.INFO):
|
||||
loop.try_use_uvloop()
|
||||
|
||||
getenv.assert_called_once_with("SANIC_NO_UVLOOP", "no")
|
||||
for record in caplog.records:
|
||||
if record.message.startswith("You are requesting to run"):
|
||||
break
|
||||
|
||||
assert record.message == (
|
||||
"You are requesting to run Sanic using uvloop, but the "
|
||||
"install-time 'SANIC_NO_UVLOOP' environment variable (used to "
|
||||
"opt-out of installing uvloop with Sanic) is set to true. If "
|
||||
"you want to prevent Sanic from overriding the event loop policy "
|
||||
"during runtime, set the 'USE_UVLOOP' configuration value to "
|
||||
"false."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
OS_IS_WINDOWS or not UVLOOP_INSTALLED,
|
||||
reason="Not testable with current client",
|
||||
)
|
||||
def test_sets_loop_policy_only_when_not_already_set(monkeypatch):
|
||||
import uvloop # type: ignore
|
||||
|
||||
# Existing policy is not uvloop.EventLoopPolicy
|
||||
get_event_loop_policy = Mock(return_value=None)
|
||||
monkeypatch.setattr(
|
||||
loop.asyncio, "get_event_loop_policy", get_event_loop_policy
|
||||
)
|
||||
|
||||
with patch("asyncio.set_event_loop_policy") as set_event_loop_policy:
|
||||
loop.try_use_uvloop()
|
||||
set_event_loop_policy.assert_called_once()
|
||||
args, _ = set_event_loop_policy.call_args
|
||||
policy = args[0]
|
||||
assert isinstance(policy, uvloop.EventLoopPolicy)
|
||||
|
||||
# Existing policy is uvloop.EventLoopPolicy
|
||||
get_event_loop_policy = Mock(return_value=policy)
|
||||
monkeypatch.setattr(
|
||||
loop.asyncio, "get_event_loop_policy", get_event_loop_policy
|
||||
)
|
||||
|
||||
with patch("asyncio.set_event_loop_policy") as set_event_loop_policy:
|
||||
loop.try_use_uvloop()
|
||||
set_event_loop_policy.assert_not_called()
|
||||
Reference in New Issue
Block a user