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
from signal import SIGINT, signal
import uvloop
from sanic import Sanic, response
@ -15,17 +13,18 @@ async def test(request):
return response.json({"answer": "42"})
asyncio.set_event_loop(uvloop.new_event_loop())
server = app.create_server(
host="0.0.0.0", port=8000, return_asyncio_server=True
async def main():
server = await app.create_server(
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:
loop.run_forever()
finally:
loop.stop()
if server is None:
return
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
)
TouchUp.run(self)
self.state.is_started = True
async def _server_event(
self,

View File

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

View File

@ -2,20 +2,27 @@ from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING
from warnings import warn
from sanic.exceptions import SanicException
if TYPE_CHECKING:
from sanic import Sanic
class AsyncioServer:
"""
Wraps an asyncio server with functionality that might be useful to
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__(
self,
app,
app: Sanic,
loop,
serve_coro,
connections,
@ -27,13 +34,20 @@ class AsyncioServer:
self.loop = loop
self.serve_coro = serve_coro
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):
"""
Trigger "before_server_start" events
"""
self.init = True
return self.app._startup()
def before_start(self):
@ -77,30 +91,33 @@ class AsyncioServer:
return task
def start_serving(self):
if self.server:
try:
return self.server.start_serving()
except AttributeError:
raise NotImplementedError(
"server.start_serving not available in this version "
"of asyncio or uvloop."
)
return self._serve(self.server.start_serving)
def serve_forever(self):
return self._serve(self.server.serve_forever)
def _serve(self, serve_func):
if self.server:
if not self.app.state.is_started:
raise SanicException(
"Cannot run Sanic server without first running "
"await server.startup()"
)
try:
return self.server.serve_forever()
return serve_func()
except AttributeError:
name = serve_func.__name__
raise NotImplementedError(
"server.serve_forever not available in this version "
f"server.{name} not available in this version "
"of asyncio or uvloop."
)
def _server_event(self, concern: str, action: str):
if not self.init:
if not self.app.state.is_started:
raise SanicException(
"Cannot dispatch server event without "
"first running server.startup()"
"first running await server.startup()"
)
return self.app._server_event(concern, action, loop=self.loop)

View File

@ -2,12 +2,10 @@ import asyncio
import logging
import re
from email import message
from inspect import isawaitable
from os import environ
from unittest.mock import Mock, patch
import py
import pytest
from sanic import Sanic
@ -41,7 +39,6 @@ def test_app_loop_running(app):
def test_create_asyncio_server(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(return_asyncio_server=True)
assert isawaitable(asyncio_srv_coro)
@ -50,7 +47,6 @@ def test_create_asyncio_server(app):
def test_asyncio_server_no_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43123,
@ -62,7 +58,6 @@ def test_asyncio_server_no_start_serving(app):
def test_asyncio_server_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
port=43124,
@ -71,6 +66,7 @@ def test_asyncio_server_start_serving(app):
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
loop.run_until_complete(srv.startup())
loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True
wait_close = srv.close()
@ -92,6 +88,21 @@ def test_create_server_main(app, caplog):
) 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):
app.main_process_start(lambda *_: ...)
loop = asyncio.get_event_loop()
@ -106,6 +117,19 @@ def test_create_server_main_convenience(app, caplog):
) 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):
with pytest.raises(SanicException) as excinfo:
app.loop