Fix deadlock in async_get_integration_with_requirements after failed dep pip install (#49540)
This commit is contained in:
parent
d76993034e
commit
d4329e01ef
2 changed files with 116 additions and 19 deletions
|
@ -65,6 +65,7 @@ async def async_get_integration_with_requirements(
|
||||||
|
|
||||||
if isinstance(int_or_evt, asyncio.Event):
|
if isinstance(int_or_evt, asyncio.Event):
|
||||||
await int_or_evt.wait()
|
await int_or_evt.wait()
|
||||||
|
|
||||||
int_or_evt = cache.get(domain, UNDEFINED)
|
int_or_evt = cache.get(domain, UNDEFINED)
|
||||||
|
|
||||||
# When we have waited and it's UNDEFINED, it doesn't exist
|
# 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()
|
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:
|
if integration.requirements:
|
||||||
await async_process_requirements(
|
await async_process_requirements(
|
||||||
hass, integration.domain, integration.requirements
|
hass, integration.domain, integration.requirements
|
||||||
|
@ -97,26 +114,24 @@ async def async_get_integration_with_requirements(
|
||||||
):
|
):
|
||||||
deps_to_check.append(check_domain)
|
deps_to_check.append(check_domain)
|
||||||
|
|
||||||
if deps_to_check:
|
if not deps_to_check:
|
||||||
results = await asyncio.gather(
|
return
|
||||||
*[
|
|
||||||
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
|
|
||||||
|
|
||||||
cache[domain] = integration
|
results = await asyncio.gather(
|
||||||
event.set()
|
*[
|
||||||
return integration
|
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(
|
async def async_process_requirements(
|
||||||
|
|
|
@ -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):
|
async def test_get_integration_with_missing_dependencies(hass):
|
||||||
"""Check getting an integration with missing dependencies."""
|
"""Check getting an integration with missing dependencies."""
|
||||||
hass.config.skip_pip = False
|
hass.config.skip_pip = False
|
||||||
|
|
Loading…
Add table
Reference in a new issue