Improve http handler performance (#93324)

This commit is contained in:
J. Nick Koston 2023-05-21 13:36:03 -05:00 committed by GitHub
parent ab0d35df92
commit e27554f7a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 81 additions and 59 deletions

View File

@ -138,16 +138,16 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
app._on_startup.freeze()
await app.startup()
DescriptionXmlView(config).register(app, app.router)
HueUsernameView().register(app, app.router)
HueConfigView(config).register(app, app.router)
HueUnauthorizedUser().register(app, app.router)
HueAllLightsStateView(config).register(app, app.router)
HueOneLightStateView(config).register(app, app.router)
HueOneLightChangeView(config).register(app, app.router)
HueAllGroupsStateView(config).register(app, app.router)
HueGroupView(config).register(app, app.router)
HueFullStateView(config).register(app, app.router)
DescriptionXmlView(config).register(hass, app, app.router)
HueUsernameView().register(hass, app, app.router)
HueConfigView(config).register(hass, app, app.router)
HueUnauthorizedUser().register(hass, app, app.router)
HueAllLightsStateView(config).register(hass, app, app.router)
HueOneLightStateView(config).register(hass, app, app.router)
HueOneLightChangeView(config).register(hass, app, app.router)
HueAllGroupsStateView(config).register(hass, app, app.router)
HueGroupView(config).register(hass, app, app.router)
HueFullStateView(config).register(hass, app, app.router)
async def _start(event: Event) -> None:
"""Start the bridge."""

View File

@ -365,7 +365,7 @@ class HomeAssistantHTTP:
class_name = view.__class__.__name__
raise AttributeError(f'{class_name} missing required attribute "name"')
view.register(self.app, self.app.router)
view.register(self.hass, self.app, self.app.router)
def register_redirect(
self,

View File

@ -19,7 +19,7 @@ import voluptuous as vol
from homeassistant import exceptions
from homeassistant.const import CONTENT_TYPE_JSON
from homeassistant.core import Context, is_callback
from homeassistant.core import Context, HomeAssistant, is_callback
from homeassistant.helpers.json import (
find_paths_unserializable_data,
json_bytes,
@ -27,7 +27,7 @@ from homeassistant.helpers.json import (
)
from homeassistant.util.json import JSON_ENCODE_EXCEPTIONS, format_unserializable_data
from .const import KEY_AUTHENTICATED, KEY_HASS
from .const import KEY_AUTHENTICATED
_LOGGER = logging.getLogger(__name__)
@ -88,7 +88,9 @@ class HomeAssistantView:
data["code"] = message_code
return self.json(data, status_code, headers=headers)
def register(self, app: web.Application, router: web.UrlDispatcher) -> None:
def register(
self, hass: HomeAssistant, app: web.Application, router: web.UrlDispatcher
) -> None:
"""Register the view with a router."""
assert self.url is not None, "No url set for view"
urls = [self.url] + self.extra_urls
@ -98,7 +100,7 @@ class HomeAssistantView:
if not (handler := getattr(self, method, None)):
continue
handler = request_handler_factory(self, handler)
handler = request_handler_factory(hass, self, handler)
for url in urls:
routes.append(router.add_route(method, url, handler))
@ -115,16 +117,17 @@ class HomeAssistantView:
def request_handler_factory(
view: HomeAssistantView, handler: Callable
hass: HomeAssistant, view: HomeAssistantView, handler: Callable
) -> Callable[[web.Request], Awaitable[web.StreamResponse]]:
"""Wrap the handler classes."""
assert asyncio.iscoroutinefunction(handler) or is_callback(
is_coroutinefunction = asyncio.iscoroutinefunction(handler)
assert is_coroutinefunction or is_callback(
handler
), "Handler should be a coroutine or a callback."
async def handle(request: web.Request) -> web.StreamResponse:
"""Handle incoming request."""
if request.app[KEY_HASS].is_stopping:
if hass.is_stopping:
return web.Response(status=HTTPStatus.SERVICE_UNAVAILABLE)
authenticated = request.get(KEY_AUTHENTICATED, False)
@ -132,18 +135,19 @@ def request_handler_factory(
if view.requires_auth and not authenticated:
raise HTTPUnauthorized()
_LOGGER.debug(
"Serving %s to %s (auth: %s)",
request.path,
request.remote,
authenticated,
)
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug(
"Serving %s to %s (auth: %s)",
request.path,
request.remote,
authenticated,
)
try:
result = handler(request, **request.match_info)
if asyncio.iscoroutine(result):
result = await result
if is_coroutinefunction:
result = await handler(request, **request.match_info)
else:
result = handler(request, **request.match_info)
except vol.Invalid as err:
raise HTTPBadRequest() from err
except exceptions.ServiceNotFound as err:
@ -156,21 +160,20 @@ def request_handler_factory(
return result
status_code = HTTPStatus.OK
if isinstance(result, tuple):
result, status_code = result
if isinstance(result, bytes):
bresult = result
elif isinstance(result, str):
bresult = result.encode("utf-8")
elif result is None:
bresult = b""
else:
raise TypeError(
f"Result should be None, string, bytes or StreamResponse. Got: {result}"
)
return web.Response(body=result, status=status_code)
return web.Response(body=bresult, status=status_code)
if isinstance(result, str):
return web.Response(text=result, status=status_code)
if result is None:
return web.Response(body=b"", status=status_code)
raise TypeError(
f"Result should be None, string, bytes or StreamResponse. Got: {result}"
)
return handle

View File

@ -215,13 +215,13 @@ def _mock_hue_endpoints(
web_app = hass.http.app
config = Config(hass, conf, "127.0.0.1")
config.numbers = entity_numbers
HueUsernameView().register(web_app, web_app.router)
HueAllLightsStateView(config).register(web_app, web_app.router)
HueOneLightStateView(config).register(web_app, web_app.router)
HueOneLightChangeView(config).register(web_app, web_app.router)
HueAllGroupsStateView(config).register(web_app, web_app.router)
HueFullStateView(config).register(web_app, web_app.router)
HueConfigView(config).register(web_app, web_app.router)
HueUsernameView().register(hass, web_app, web_app.router)
HueAllLightsStateView(config).register(hass, web_app, web_app.router)
HueOneLightStateView(config).register(hass, web_app, web_app.router)
HueOneLightChangeView(config).register(hass, web_app, web_app.router)
HueAllGroupsStateView(config).register(hass, web_app, web_app.router)
HueFullStateView(config).register(hass, web_app, web_app.router)
HueConfigView(config).register(hass, web_app, web_app.router)
@pytest.fixture

View File

@ -333,13 +333,15 @@ async def test_failed_login_attempts_counter(
return None, 200
app.router.add_get(
"/auth_true", request_handler_factory(Mock(requires_auth=True), auth_handler)
"/auth_true",
request_handler_factory(hass, Mock(requires_auth=True), auth_handler),
)
app.router.add_get(
"/auth_false", request_handler_factory(Mock(requires_auth=True), auth_handler)
"/auth_false",
request_handler_factory(hass, Mock(requires_auth=True), auth_handler),
)
app.router.add_get(
"/", request_handler_factory(Mock(requires_auth=False), auth_handler)
"/", request_handler_factory(hass, Mock(requires_auth=False), auth_handler)
)
setup_bans(hass, app, 5)

View File

@ -27,7 +27,7 @@ async def get_client(aiohttp_client, validator):
"""Test method."""
return b""
TestView().register(app, app.router)
TestView().register(app["hass"], app, app.router)
client = await aiohttp_client(app)
return client

View File

@ -20,13 +20,13 @@ from homeassistant.exceptions import ServiceNotFound, Unauthorized
@pytest.fixture
def mock_request():
def mock_request() -> Mock:
"""Mock a request."""
return Mock(app={"hass": Mock(is_stopping=False)}, match_info={})
@pytest.fixture
def mock_request_with_stopping():
def mock_request_with_stopping() -> Mock:
"""Mock a request."""
return Mock(app={"hass": Mock(is_stopping=True)}, match_info={})
@ -48,34 +48,51 @@ async def test_nan_serialized_to_null() -> None:
assert json.loads(response.body.decode("utf-8")) is None
async def test_handling_unauthorized(mock_request) -> None:
async def test_handling_unauthorized(mock_request: Mock) -> None:
"""Test handling unauth exceptions."""
with pytest.raises(HTTPUnauthorized):
await request_handler_factory(
Mock(requires_auth=False), AsyncMock(side_effect=Unauthorized)
mock_request.app["hass"],
Mock(requires_auth=False),
AsyncMock(side_effect=Unauthorized),
)(mock_request)
async def test_handling_invalid_data(mock_request) -> None:
async def test_handling_invalid_data(mock_request: Mock) -> None:
"""Test handling unauth exceptions."""
with pytest.raises(HTTPBadRequest):
await request_handler_factory(
Mock(requires_auth=False), AsyncMock(side_effect=vol.Invalid("yo"))
mock_request.app["hass"],
Mock(requires_auth=False),
AsyncMock(side_effect=vol.Invalid("yo")),
)(mock_request)
async def test_handling_service_not_found(mock_request) -> None:
async def test_handling_service_not_found(mock_request: Mock) -> None:
"""Test handling unauth exceptions."""
with pytest.raises(HTTPInternalServerError):
await request_handler_factory(
mock_request.app["hass"],
Mock(requires_auth=False),
AsyncMock(side_effect=ServiceNotFound("test", "test")),
)(mock_request)
async def test_not_running(mock_request_with_stopping) -> None:
async def test_not_running(mock_request_with_stopping: Mock) -> None:
"""Test we get a 503 when not running."""
response = await request_handler_factory(
Mock(requires_auth=False), AsyncMock(side_effect=Unauthorized)
mock_request_with_stopping.app["hass"],
Mock(requires_auth=False),
AsyncMock(side_effect=Unauthorized),
)(mock_request_with_stopping)
assert response.status == HTTPStatus.SERVICE_UNAVAILABLE
async def test_invalid_handler(mock_request: Mock) -> None:
"""Test an invalid handler."""
with pytest.raises(TypeError):
await request_handler_factory(
mock_request.app["hass"],
Mock(requires_auth=False),
AsyncMock(return_value=["not valid"]),
)(mock_request)