Improve http handler performance (#93324)
This commit is contained in:
parent
ab0d35df92
commit
e27554f7a6
7 changed files with 81 additions and 59 deletions
|
@ -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."""
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue