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() app._on_startup.freeze()
await app.startup() await app.startup()
DescriptionXmlView(config).register(app, app.router) DescriptionXmlView(config).register(hass, app, app.router)
HueUsernameView().register(app, app.router) HueUsernameView().register(hass, app, app.router)
HueConfigView(config).register(app, app.router) HueConfigView(config).register(hass, app, app.router)
HueUnauthorizedUser().register(app, app.router) HueUnauthorizedUser().register(hass, app, app.router)
HueAllLightsStateView(config).register(app, app.router) HueAllLightsStateView(config).register(hass, app, app.router)
HueOneLightStateView(config).register(app, app.router) HueOneLightStateView(config).register(hass, app, app.router)
HueOneLightChangeView(config).register(app, app.router) HueOneLightChangeView(config).register(hass, app, app.router)
HueAllGroupsStateView(config).register(app, app.router) HueAllGroupsStateView(config).register(hass, app, app.router)
HueGroupView(config).register(app, app.router) HueGroupView(config).register(hass, app, app.router)
HueFullStateView(config).register(app, app.router) HueFullStateView(config).register(hass, app, app.router)
async def _start(event: Event) -> None: async def _start(event: Event) -> None:
"""Start the bridge.""" """Start the bridge."""

View file

@ -365,7 +365,7 @@ class HomeAssistantHTTP:
class_name = view.__class__.__name__ class_name = view.__class__.__name__
raise AttributeError(f'{class_name} missing required attribute "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( def register_redirect(
self, self,

View file

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

View file

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

View file

@ -333,13 +333,15 @@ async def test_failed_login_attempts_counter(
return None, 200 return None, 200
app.router.add_get( 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( 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( 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) setup_bans(hass, app, 5)

View file

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

View file

@ -20,13 +20,13 @@ from homeassistant.exceptions import ServiceNotFound, Unauthorized
@pytest.fixture @pytest.fixture
def mock_request(): def mock_request() -> Mock:
"""Mock a request.""" """Mock a request."""
return Mock(app={"hass": Mock(is_stopping=False)}, match_info={}) return Mock(app={"hass": Mock(is_stopping=False)}, match_info={})
@pytest.fixture @pytest.fixture
def mock_request_with_stopping(): def mock_request_with_stopping() -> Mock:
"""Mock a request.""" """Mock a request."""
return Mock(app={"hass": Mock(is_stopping=True)}, match_info={}) 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 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.""" """Test handling unauth exceptions."""
with pytest.raises(HTTPUnauthorized): with pytest.raises(HTTPUnauthorized):
await request_handler_factory( 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) )(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.""" """Test handling unauth exceptions."""
with pytest.raises(HTTPBadRequest): with pytest.raises(HTTPBadRequest):
await request_handler_factory( 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) )(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.""" """Test handling unauth exceptions."""
with pytest.raises(HTTPInternalServerError): with pytest.raises(HTTPInternalServerError):
await request_handler_factory( await request_handler_factory(
mock_request.app["hass"],
Mock(requires_auth=False), Mock(requires_auth=False),
AsyncMock(side_effect=ServiceNotFound("test", "test")), AsyncMock(side_effect=ServiceNotFound("test", "test")),
)(mock_request) )(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.""" """Test we get a 503 when not running."""
response = await request_handler_factory( 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) )(mock_request_with_stopping)
assert response.status == HTTPStatus.SERVICE_UNAVAILABLE 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)