Skip to content

Commit

Permalink
Optional uvloop use (#2264)
Browse files Browse the repository at this point in the history
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
  • Loading branch information
3 people committed Dec 23, 2021
1 parent 4659069 commit 98ce4bd
Show file tree
Hide file tree
Showing 12 changed files with 376 additions and 47 deletions.
44 changes: 37 additions & 7 deletions sanic/app.py
Expand Up @@ -69,6 +69,7 @@
URLBuildError,
)
from sanic.handlers import ErrorHandler
from sanic.helpers import _default
from sanic.http import Stage
from sanic.log import LOGGING_CONFIG_DEFAULTS, Colors, error_logger, logger
from sanic.mixins.listeners import ListenerEvent
Expand All @@ -88,7 +89,7 @@
from sanic.router import Router
from sanic.server import AsyncioServer, HttpProtocol
from sanic.server import Signal as ServerSignal
from sanic.server import serve, serve_multiple, serve_single
from sanic.server import serve, serve_multiple, serve_single, try_use_uvloop
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
from sanic.server.websockets.impl import ConnectionClosed
from sanic.signals import Signal, SignalRouter
Expand Down Expand Up @@ -130,6 +131,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"_task_registry",
"_test_client",
"_test_manager",
"_uvloop_setting", # TODO: Remove in v22.6
"asgi",
"auto_reload",
"auto_reload",
Expand Down Expand Up @@ -159,6 +161,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
)

_app_registry: Dict[str, "Sanic"] = {}
_uvloop_setting = None
test_mode = False

def __init__(
Expand Down Expand Up @@ -1142,6 +1145,11 @@ def run(
register_sys_signals=register_sys_signals,
)

if self.config.USE_UVLOOP is True or (
self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS
):
try_use_uvloop()

try:
self.is_running = True
self.is_stopping = False
Expand Down Expand Up @@ -1239,12 +1247,13 @@ async def create_server(
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)

# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
self.config.ACCESS_LOG = access_log

if noisy_exceptions is not None:
self.config.NOISY_EXCEPTIONS = noisy_exceptions
# Set explicitly passed configuration values
for attribute, value in {
"ACCESS_LOG": access_log,
"NOISY_EXCEPTIONS": noisy_exceptions,
}.items():
if value is not None:
setattr(self.config, attribute, value)

server_settings = self._helper(
host=host,
Expand All @@ -1259,6 +1268,14 @@ async def create_server(
run_async=return_asyncio_server,
)

if self.config.USE_UVLOOP is not _default:
error_logger.warning(
"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."
)

main_start = server_settings.pop("main_start", None)
main_stop = server_settings.pop("main_stop", None)
if main_start or main_stop:
Expand Down Expand Up @@ -1833,6 +1850,19 @@ async def _startup(self):
self._future_registry.clear()
self.signalize()
self.finalize()

# TODO: Replace in v22.6 to check against apps in app registry
if (
self.__class__._uvloop_setting is not None
and self.__class__._uvloop_setting != self.config.USE_UVLOOP
):
error_logger.warning(
"It looks like you're running several apps with different "
"uvloop settings. This is not supported and may lead to "
"unintended behaviour."
)
self.__class__._uvloop_setting = self.config.USE_UVLOOP

ErrorHandler.finalize(self.error_handler, config=self.config)
TouchUp.run(self)
self.state.is_started = True
Expand Down
8 changes: 8 additions & 0 deletions sanic/asgi.py
Expand Up @@ -7,6 +7,7 @@

from sanic.compat import Header
from sanic.exceptions import ServerError
from sanic.helpers import _default
from sanic.http import Stage
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
from sanic.request import Request
Expand Down Expand Up @@ -53,6 +54,13 @@ async def startup(self) -> None:
await self.asgi_app.sanic_app._server_event("init", "before")
await self.asgi_app.sanic_app._server_event("init", "after")

if self.asgi_app.sanic_app.config.USE_UVLOOP is not _default:
warnings.warn(
"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."
)

async def shutdown(self) -> None:
"""
Gather the listeners to fire on server stop.
Expand Down
8 changes: 8 additions & 0 deletions sanic/compat.py
Expand Up @@ -8,6 +8,14 @@


OS_IS_WINDOWS = os.name == "nt"
UVLOOP_INSTALLED = False

try:
import uvloop # type: ignore # noqa

UVLOOP_INSTALLED = True
except ImportError:
pass


def enable_windows_color_support():
Expand Down
4 changes: 3 additions & 1 deletion sanic/config.py
Expand Up @@ -7,7 +7,7 @@
from warnings import warn

from sanic.errorpages import DEFAULT_FORMAT, check_error_format
from sanic.helpers import _default
from sanic.helpers import Default, _default
from sanic.http import Http
from sanic.log import error_logger
from sanic.utils import load_module_from_file_location, str_to_bool
Expand Down Expand Up @@ -38,6 +38,7 @@
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds
"USE_UVLOOP": _default,
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
"WEBSOCKET_PING_INTERVAL": 20,
"WEBSOCKET_PING_TIMEOUT": 20,
Expand Down Expand Up @@ -79,6 +80,7 @@ class Config(dict, metaclass=DescriptorMeta):
REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int
SERVER_NAME: str
USE_UVLOOP: Union[Default, bool]
WEBSOCKET_MAX_SIZE: int
WEBSOCKET_PING_INTERVAL: int
WEBSOCKET_PING_TIMEOUT: int
Expand Down
13 changes: 2 additions & 11 deletions sanic/server/__init__.py
@@ -1,20 +1,10 @@
import asyncio

from sanic.models.server_types import ConnInfo, Signal
from sanic.server.async_server import AsyncioServer
from sanic.server.loop import try_use_uvloop
from sanic.server.protocols.http_protocol import HttpProtocol
from sanic.server.runners import serve, serve_multiple, serve_single


try:
import uvloop # type: ignore

if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass


__all__ = (
"AsyncioServer",
"ConnInfo",
Expand All @@ -23,4 +13,5 @@
"serve",
"serve_multiple",
"serve_single",
"try_use_uvloop",
)
49 changes: 49 additions & 0 deletions sanic/server/loop.py
@@ -0,0 +1,49 @@
import asyncio

from distutils.util import strtobool
from os import getenv

from sanic.compat import OS_IS_WINDOWS
from sanic.log import error_logger


def try_use_uvloop() -> None:
"""
Use uvloop instead of the default asyncio loop.
"""
if OS_IS_WINDOWS:
error_logger.warning(
"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."
)
return

try:
import uvloop # type: ignore
except ImportError:
error_logger.warning(
"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."
)
return

uvloop_install_removed = strtobool(getenv("SANIC_NO_UVLOOP", "no"))
if uvloop_install_removed:
error_logger.info(
"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."
)

if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
11 changes: 4 additions & 7 deletions sanic/worker.py
Expand Up @@ -7,8 +7,9 @@

from gunicorn.workers import base # type: ignore

from sanic.compat import UVLOOP_INSTALLED
from sanic.log import logger
from sanic.server import HttpProtocol, Signal, serve
from sanic.server import HttpProtocol, Signal, serve, try_use_uvloop
from sanic.server.protocols.websocket_protocol import WebSocketProtocol


Expand All @@ -17,12 +18,8 @@
except ImportError:
ssl = None # type: ignore

try:
import uvloop # type: ignore

asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
if UVLOOP_INSTALLED:
try_use_uvloop()


class GunicornWorker(base.Worker):
Expand Down

0 comments on commit 98ce4bd

Please sign in to comment.