Ensure startup can proceed if async_get_integration raises (#50799)

* Ensure startup can proceed if async_get_integration raises

There were cases where the event would never get set and
startup would deadlock because the second attempt to load
the integration would block forever

* pylint

* reorder
This commit is contained in:
J. Nick Koston 2021-05-17 18:32:05 -04:00 committed by GitHub
parent 8129db1cfe
commit a43561e3e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 48 additions and 15 deletions

View file

@ -506,31 +506,35 @@ async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration
event = cache[domain] = asyncio.Event() event = cache[domain] = asyncio.Event()
try:
integration = await _async_get_integration(hass, domain)
except Exception: # pylint: disable=broad-except
# Remove event from cache.
cache.pop(domain)
event.set()
raise
cache[domain] = integration
event.set()
return integration
async def _async_get_integration(hass: HomeAssistant, domain: str) -> Integration:
# Instead of using resolve_from_root we use the cache of custom # Instead of using resolve_from_root we use the cache of custom
# components to find the integration. # components to find the integration.
integration = (await async_get_custom_components(hass)).get(domain) if integration := (await async_get_custom_components(hass)).get(domain):
if integration is not None:
validate_custom_integration_version(integration) validate_custom_integration_version(integration)
_LOGGER.warning(CUSTOM_WARNING, integration.domain) _LOGGER.warning(CUSTOM_WARNING, integration.domain)
cache[domain] = integration
event.set()
return integration return integration
from homeassistant import components # pylint: disable=import-outside-toplevel from homeassistant import components # pylint: disable=import-outside-toplevel
integration = await hass.async_add_executor_job( if integration := await hass.async_add_executor_job(
Integration.resolve_from_root, hass, components, domain Integration.resolve_from_root, hass, components, domain
) ):
event.set() return integration
if not integration: raise IntegrationNotFound(domain)
# Remove event from cache.
cache.pop(domain)
raise IntegrationNotFound(domain)
cache[domain] = integration
return integration
class LoaderError(Exception): class LoaderError(Exception):

View file

@ -467,3 +467,32 @@ async def test_get_custom_components_safe_mode(hass):
"""Test that we get empty custom components in safe mode.""" """Test that we get empty custom components in safe mode."""
hass.config.safe_mode = True hass.config.safe_mode = True
assert await loader.async_get_custom_components(hass) == {} assert await loader.async_get_custom_components(hass) == {}
async def test_custom_integration_missing_version(hass, caplog):
"""Test trying to load a custom integration without a version twice does not deadlock."""
test_integration_1 = loader.Integration(
hass, "custom_components.test1", None, {"domain": "test1"}
)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {
"test1": test_integration_1,
}
with pytest.raises(loader.IntegrationNotFound):
await loader.async_get_integration(hass, "test1")
with pytest.raises(loader.IntegrationNotFound):
await loader.async_get_integration(hass, "test1")
async def test_custom_integration_missing(hass, caplog):
"""Test trying to load a custom integration that is missing twice not deadlock."""
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {}
with pytest.raises(loader.IntegrationNotFound):
await loader.async_get_integration(hass, "test1")
with pytest.raises(loader.IntegrationNotFound):
await loader.async_get_integration(hass, "test1")