Add runtime checking to create_server to verify that startup has been run (#2328)

This commit is contained in:
Adam Hopkins 2021-12-13 09:36:41 +02:00 committed by GitHub
parent 3d383d7b97
commit 264453459e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 104 additions and 62 deletions

View File

@ -1,7 +1,5 @@
import asyncio import asyncio
from signal import SIGINT, signal
import uvloop import uvloop
from sanic import Sanic, response from sanic import Sanic, response
@ -15,17 +13,18 @@ async def test(request):
return response.json({"answer": "42"}) return response.json({"answer": "42"})
asyncio.set_event_loop(uvloop.new_event_loop()) async def main():
server = app.create_server( server = await app.create_server(
host="0.0.0.0", port=8000, return_asyncio_server=True port=8000, host="0.0.0.0", return_asyncio_server=True
) )
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
server = loop.run_until_complete(task)
loop.run_until_complete(server.startup())
signal(SIGINT, lambda s, f: loop.stop())
try: if server is None:
loop.run_forever() return
finally:
loop.stop() await server.startup()
await server.serve_forever()
if __name__ == "__main__":
asyncio.set_event_loop(uvloop.new_event_loop())
asyncio.run(main())

View File

@ -1703,6 +1703,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
self.error_handler, fallback=self.config.FALLBACK_ERROR_FORMAT self.error_handler, fallback=self.config.FALLBACK_ERROR_FORMAT
) )
TouchUp.run(self) TouchUp.run(self)
self.state.is_started = True
async def _server_event( async def _server_event(
self, self,

View File

@ -42,6 +42,7 @@ class ApplicationState:
reload_dirs: Set[Path] = field(default_factory=set) reload_dirs: Set[Path] = field(default_factory=set)
server: Server = field(default=Server.SANIC) server: Server = field(default=Server.SANIC)
is_running: bool = field(default=False) is_running: bool = field(default=False)
is_started: bool = field(default=False)
is_stopping: bool = field(default=False) is_stopping: bool = field(default=False)
verbosity: int = field(default=0) verbosity: int = field(default=0)
workers: int = field(default=0) workers: int = field(default=0)

View File

@ -2,20 +2,27 @@ from __future__ import annotations
import asyncio import asyncio
from typing import TYPE_CHECKING
from warnings import warn
from sanic.exceptions import SanicException from sanic.exceptions import SanicException
if TYPE_CHECKING:
from sanic import Sanic
class AsyncioServer: class AsyncioServer:
""" """
Wraps an asyncio server with functionality that might be useful to Wraps an asyncio server with functionality that might be useful to
a user who needs to manage the server lifecycle manually. a user who needs to manage the server lifecycle manually.
""" """
__slots__ = ("app", "connections", "loop", "serve_coro", "server", "init") __slots__ = ("app", "connections", "loop", "serve_coro", "server")
def __init__( def __init__(
self, self,
app, app: Sanic,
loop, loop,
serve_coro, serve_coro,
connections, connections,
@ -27,13 +34,20 @@ class AsyncioServer:
self.loop = loop self.loop = loop
self.serve_coro = serve_coro self.serve_coro = serve_coro
self.server = None self.server = None
self.init = False
@property
def init(self):
warn(
"AsyncioServer.init has been deprecated and will be removed "
"in v22.6. Use Sanic.state.is_started instead.",
DeprecationWarning,
)
return self.app.state.is_started
def startup(self): def startup(self):
""" """
Trigger "before_server_start" events Trigger "before_server_start" events
""" """
self.init = True
return self.app._startup() return self.app._startup()
def before_start(self): def before_start(self):
@ -77,30 +91,33 @@ class AsyncioServer:
return task return task
def start_serving(self): def start_serving(self):
if self.server: return self._serve(self.server.start_serving)
try:
return self.server.start_serving()
except AttributeError:
raise NotImplementedError(
"server.start_serving not available in this version "
"of asyncio or uvloop."
)
def serve_forever(self): def serve_forever(self):
return self._serve(self.server.serve_forever)
def _serve(self, serve_func):
if self.server: if self.server:
if not self.app.state.is_started:
raise SanicException(
"Cannot run Sanic server without first running "
"await server.startup()"
)
try: try:
return self.server.serve_forever() return serve_func()
except AttributeError: except AttributeError:
name = serve_func.__name__
raise NotImplementedError( raise NotImplementedError(
"server.serve_forever not available in this version " f"server.{name} not available in this version "
"of asyncio or uvloop." "of asyncio or uvloop."
) )
def _server_event(self, concern: str, action: str): def _server_event(self, concern: str, action: str):
if not self.init: if not self.app.state.is_started:
raise SanicException( raise SanicException(
"Cannot dispatch server event without " "Cannot dispatch server event without "
"first running server.startup()" "first running await server.startup()"
) )
return self.app._server_event(concern, action, loop=self.loop) return self.app._server_event(concern, action, loop=self.loop)

View File

@ -2,12 +2,10 @@ import asyncio
import logging import logging
import re import re
from email import message
from inspect import isawaitable from inspect import isawaitable
from os import environ from os import environ
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import py
import pytest import pytest
from sanic import Sanic from sanic import Sanic
@ -41,41 +39,39 @@ def test_app_loop_running(app):
def test_create_asyncio_server(app): def test_create_asyncio_server(app):
if not uvloop_installed(): loop = asyncio.get_event_loop()
loop = asyncio.get_event_loop() asyncio_srv_coro = app.create_server(return_asyncio_server=True)
asyncio_srv_coro = app.create_server(return_asyncio_server=True) assert isawaitable(asyncio_srv_coro)
assert isawaitable(asyncio_srv_coro) srv = loop.run_until_complete(asyncio_srv_coro)
srv = loop.run_until_complete(asyncio_srv_coro) assert srv.is_serving() is True
assert srv.is_serving() is True
def test_asyncio_server_no_start_serving(app): def test_asyncio_server_no_start_serving(app):
if not uvloop_installed(): loop = asyncio.get_event_loop()
loop = asyncio.get_event_loop() asyncio_srv_coro = app.create_server(
asyncio_srv_coro = app.create_server( port=43123,
port=43123, return_asyncio_server=True,
return_asyncio_server=True, asyncio_server_kwargs=dict(start_serving=False),
asyncio_server_kwargs=dict(start_serving=False), )
) srv = loop.run_until_complete(asyncio_srv_coro)
srv = loop.run_until_complete(asyncio_srv_coro) assert srv.is_serving() is False
assert srv.is_serving() is False
def test_asyncio_server_start_serving(app): def test_asyncio_server_start_serving(app):
if not uvloop_installed(): loop = asyncio.get_event_loop()
loop = asyncio.get_event_loop() asyncio_srv_coro = app.create_server(
asyncio_srv_coro = app.create_server( port=43124,
port=43124, return_asyncio_server=True,
return_asyncio_server=True, asyncio_server_kwargs=dict(start_serving=False),
asyncio_server_kwargs=dict(start_serving=False), )
) srv = loop.run_until_complete(asyncio_srv_coro)
srv = loop.run_until_complete(asyncio_srv_coro) assert srv.is_serving() is False
assert srv.is_serving() is False loop.run_until_complete(srv.startup())
loop.run_until_complete(srv.start_serving()) loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True assert srv.is_serving() is True
wait_close = srv.close() wait_close = srv.close()
loop.run_until_complete(wait_close) loop.run_until_complete(wait_close)
# Looks like we can't easily test `serve_forever()` # Looks like we can't easily test `serve_forever()`
def test_create_server_main(app, caplog): def test_create_server_main(app, caplog):
@ -92,6 +88,21 @@ def test_create_server_main(app, caplog):
) in caplog.record_tuples ) in caplog.record_tuples
def test_create_server_no_startup(app):
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43124,
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
message = (
"Cannot run Sanic server without first running await server.startup()"
)
with pytest.raises(SanicException, match=message):
loop.run_until_complete(srv.start_serving())
def test_create_server_main_convenience(app, caplog): def test_create_server_main_convenience(app, caplog):
app.main_process_start(lambda *_: ...) app.main_process_start(lambda *_: ...)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@ -106,6 +117,19 @@ def test_create_server_main_convenience(app, caplog):
) in caplog.record_tuples ) in caplog.record_tuples
def test_create_server_init(app, caplog):
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
server = loop.run_until_complete(asyncio_srv_coro)
message = (
"AsyncioServer.init has been deprecated and will be removed in v22.6. "
"Use Sanic.state.is_started instead."
)
with pytest.warns(DeprecationWarning, match=message):
server.init
def test_app_loop_not_running(app): def test_app_loop_not_running(app):
with pytest.raises(SanicException) as excinfo: with pytest.raises(SanicException) as excinfo:
app.loop app.loop