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:
Néstor Pérez
2021-12-23 10:57:33 +01:00
committed by GitHub
parent 4659069350
commit 98ce4bdeb2
12 changed files with 376 additions and 47 deletions

View File

@@ -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):

View File

@@ -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()

View File

@@ -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))

View File

@@ -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
View 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()