1
mirror of https://github.com/home-assistant/core synced 2024-10-01 05:30:36 +02:00

Fix deadlock in async_get_integration_with_requirements after failed dep pip install (#49540)

This commit is contained in:
J. Nick Koston 2021-04-22 10:32:38 -10:00 committed by GitHub
parent d76993034e
commit d4329e01ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 116 additions and 19 deletions

View File

@ -65,6 +65,7 @@ async def async_get_integration_with_requirements(
if isinstance(int_or_evt, asyncio.Event):
await int_or_evt.wait()
int_or_evt = cache.get(domain, UNDEFINED)
# When we have waited and it's UNDEFINED, it doesn't exist
@ -78,6 +79,22 @@ async def async_get_integration_with_requirements(
event = cache[domain] = asyncio.Event()
try:
await _async_process_integration(hass, integration, done)
except Exception: # pylint: disable=broad-except
del cache[domain]
event.set()
raise
cache[domain] = integration
event.set()
return integration
async def _async_process_integration(
hass: HomeAssistant, integration: Integration, done: set[str]
) -> None:
"""Process an integration and requirements."""
if integration.requirements:
await async_process_requirements(
hass, integration.domain, integration.requirements
@ -97,26 +114,24 @@ async def async_get_integration_with_requirements(
):
deps_to_check.append(check_domain)
if deps_to_check:
results = await asyncio.gather(
*[
async_get_integration_with_requirements(hass, dep, done)
for dep in deps_to_check
],
return_exceptions=True,
)
for result in results:
if not isinstance(result, BaseException):
continue
if not isinstance(result, IntegrationNotFound) or not (
not integration.is_built_in
and result.domain in integration.after_dependencies
):
raise result
if not deps_to_check:
return
cache[domain] = integration
event.set()
return integration
results = await asyncio.gather(
*[
async_get_integration_with_requirements(hass, dep, done)
for dep in deps_to_check
],
return_exceptions=True,
)
for result in results:
if not isinstance(result, BaseException):
continue
if not isinstance(result, IntegrationNotFound) or not (
not integration.is_built_in
and result.domain in integration.after_dependencies
):
raise result
async def async_process_requirements(

View File

@ -139,6 +139,88 @@ async def test_get_integration_with_requirements(hass):
]
async def test_get_integration_with_requirements_pip_install_fails_two_passes(hass):
"""Check getting an integration with loaded requirements and the pip install fails two passes."""
hass.config.skip_pip = False
mock_integration(
hass, MockModule("test_component_dep", requirements=["test-comp-dep==1.0.0"])
)
mock_integration(
hass,
MockModule(
"test_component_after_dep", requirements=["test-comp-after-dep==1.0.0"]
),
)
mock_integration(
hass,
MockModule(
"test_component",
requirements=["test-comp==1.0.0"],
dependencies=["test_component_dep"],
partial_manifest={"after_dependencies": ["test_component_after_dep"]},
),
)
def _mock_install_package(package, **kwargs):
if package == "test-comp==1.0.0":
return True
return False
# 1st pass
with pytest.raises(RequirementsNotFound), patch(
"homeassistant.util.package.is_installed", return_value=False
) as mock_is_installed, patch(
"homeassistant.util.package.install_package", side_effect=_mock_install_package
) as mock_inst:
integration = await async_get_integration_with_requirements(
hass, "test_component"
)
assert integration
assert integration.domain == "test_component"
assert len(mock_is_installed.mock_calls) == 3
assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [
"test-comp-after-dep==1.0.0",
"test-comp-dep==1.0.0",
"test-comp==1.0.0",
]
assert len(mock_inst.mock_calls) == 3
assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [
"test-comp-after-dep==1.0.0",
"test-comp-dep==1.0.0",
"test-comp==1.0.0",
]
# 2nd pass
with pytest.raises(RequirementsNotFound), patch(
"homeassistant.util.package.is_installed", return_value=False
) as mock_is_installed, patch(
"homeassistant.util.package.install_package", side_effect=_mock_install_package
) as mock_inst:
integration = await async_get_integration_with_requirements(
hass, "test_component"
)
assert integration
assert integration.domain == "test_component"
assert len(mock_is_installed.mock_calls) == 3
assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [
"test-comp-after-dep==1.0.0",
"test-comp-dep==1.0.0",
"test-comp==1.0.0",
]
assert len(mock_inst.mock_calls) == 3
assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [
"test-comp-after-dep==1.0.0",
"test-comp-dep==1.0.0",
"test-comp==1.0.0",
]
async def test_get_integration_with_missing_dependencies(hass):
"""Check getting an integration with missing dependencies."""
hass.config.skip_pip = False