mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 11:47:06 +00:00
Fix memory leak when importing a platform fails (#114602)
* Fix memory leak when importing a platform fails re-raising ImportError would trigger a memory leak * fixes, coverage * Apply suggestions from code review
This commit is contained in:
parent
0963f5e642
commit
b12c69accb
@ -750,9 +750,7 @@ class Integration:
|
|||||||
self._import_futures: dict[str, asyncio.Future[ModuleType]] = {}
|
self._import_futures: dict[str, asyncio.Future[ModuleType]] = {}
|
||||||
cache: dict[str, ModuleType | ComponentProtocol] = hass.data[DATA_COMPONENTS]
|
cache: dict[str, ModuleType | ComponentProtocol] = hass.data[DATA_COMPONENTS]
|
||||||
self._cache = cache
|
self._cache = cache
|
||||||
missing_platforms_cache: dict[str, ImportError] = hass.data[
|
missing_platforms_cache: dict[str, bool] = hass.data[DATA_MISSING_PLATFORMS]
|
||||||
DATA_MISSING_PLATFORMS
|
|
||||||
]
|
|
||||||
self._missing_platforms_cache = missing_platforms_cache
|
self._missing_platforms_cache = missing_platforms_cache
|
||||||
self._top_level_files = top_level_files or set()
|
self._top_level_files = top_level_files or set()
|
||||||
_LOGGER.info("Loaded %s from %s", self.domain, pkg_path)
|
_LOGGER.info("Loaded %s from %s", self.domain, pkg_path)
|
||||||
@ -1085,8 +1083,7 @@ class Integration:
|
|||||||
import_futures: list[tuple[str, asyncio.Future[ModuleType]]] = []
|
import_futures: list[tuple[str, asyncio.Future[ModuleType]]] = []
|
||||||
|
|
||||||
for platform_name in platform_names:
|
for platform_name in platform_names:
|
||||||
full_name = f"{domain}.{platform_name}"
|
if platform := self._get_platform_cached_or_raise(platform_name):
|
||||||
if platform := self._get_platform_cached_or_raise(full_name):
|
|
||||||
platforms[platform_name] = platform
|
platforms[platform_name] = platform
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -1095,6 +1092,7 @@ class Integration:
|
|||||||
in_progress_imports[platform_name] = future
|
in_progress_imports[platform_name] = future
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
full_name = f"{domain}.{platform_name}"
|
||||||
if (
|
if (
|
||||||
self.import_executor
|
self.import_executor
|
||||||
and full_name not in self.hass.config.components
|
and full_name not in self.hass.config.components
|
||||||
@ -1166,14 +1164,18 @@ class Integration:
|
|||||||
|
|
||||||
return platforms
|
return platforms
|
||||||
|
|
||||||
def _get_platform_cached_or_raise(self, full_name: str) -> ModuleType | None:
|
def _get_platform_cached_or_raise(self, platform_name: str) -> ModuleType | None:
|
||||||
"""Return a platform for an integration from cache."""
|
"""Return a platform for an integration from cache."""
|
||||||
|
full_name = f"{self.domain}.{platform_name}"
|
||||||
if full_name in self._cache:
|
if full_name in self._cache:
|
||||||
# the cache is either a ModuleType or a ComponentProtocol
|
# the cache is either a ModuleType or a ComponentProtocol
|
||||||
# but we only care about the ModuleType here
|
# but we only care about the ModuleType here
|
||||||
return self._cache[full_name] # type: ignore[return-value]
|
return self._cache[full_name] # type: ignore[return-value]
|
||||||
if full_name in self._missing_platforms_cache:
|
if full_name in self._missing_platforms_cache:
|
||||||
raise self._missing_platforms_cache[full_name]
|
raise ModuleNotFoundError(
|
||||||
|
f"Platform {full_name} not found",
|
||||||
|
name=f"{self.pkg_path}.{platform_name}",
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def platforms_are_loaded(self, platform_names: Iterable[str]) -> bool:
|
def platforms_are_loaded(self, platform_names: Iterable[str]) -> bool:
|
||||||
@ -1189,9 +1191,7 @@ class Integration:
|
|||||||
|
|
||||||
def get_platform(self, platform_name: str) -> ModuleType:
|
def get_platform(self, platform_name: str) -> ModuleType:
|
||||||
"""Return a platform for an integration."""
|
"""Return a platform for an integration."""
|
||||||
if platform := self._get_platform_cached_or_raise(
|
if platform := self._get_platform_cached_or_raise(platform_name):
|
||||||
f"{self.domain}.{platform_name}"
|
|
||||||
):
|
|
||||||
return platform
|
return platform
|
||||||
return self._load_platform(platform_name)
|
return self._load_platform(platform_name)
|
||||||
|
|
||||||
@ -1212,10 +1212,7 @@ class Integration:
|
|||||||
):
|
):
|
||||||
existing_platforms.append(platform_name)
|
existing_platforms.append(platform_name)
|
||||||
continue
|
continue
|
||||||
missing_platforms[full_name] = ModuleNotFoundError(
|
missing_platforms[full_name] = True
|
||||||
f"Platform {full_name} not found",
|
|
||||||
name=f"{self.pkg_path}.{platform_name}",
|
|
||||||
)
|
|
||||||
|
|
||||||
return existing_platforms
|
return existing_platforms
|
||||||
|
|
||||||
@ -1233,11 +1230,13 @@ class Integration:
|
|||||||
cache: dict[str, ModuleType] = self.hass.data[DATA_COMPONENTS]
|
cache: dict[str, ModuleType] = self.hass.data[DATA_COMPONENTS]
|
||||||
try:
|
try:
|
||||||
cache[full_name] = self._import_platform(platform_name)
|
cache[full_name] = self._import_platform(platform_name)
|
||||||
except ImportError as ex:
|
except ModuleNotFoundError:
|
||||||
if self.domain in cache:
|
if self.domain in cache:
|
||||||
# If the domain is loaded, cache that the platform
|
# If the domain is loaded, cache that the platform
|
||||||
# does not exist so we do not try to load it again
|
# does not exist so we do not try to load it again
|
||||||
self._missing_platforms_cache[full_name] = ex
|
self._missing_platforms_cache[full_name] = True
|
||||||
|
raise
|
||||||
|
except ImportError:
|
||||||
raise
|
raise
|
||||||
except RuntimeError as err:
|
except RuntimeError as err:
|
||||||
# _DeadlockError inherits from RuntimeError
|
# _DeadlockError inherits from RuntimeError
|
||||||
|
@ -274,7 +274,61 @@ async def test_get_integration_exceptions(hass: HomeAssistant) -> None:
|
|||||||
async def test_get_platform_caches_failures_when_component_loaded(
|
async def test_get_platform_caches_failures_when_component_loaded(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test get_platform cache failures only when the component is loaded."""
|
"""Test get_platform caches failures only when the component is loaded.
|
||||||
|
|
||||||
|
Only ModuleNotFoundError is cached, ImportError is not cached.
|
||||||
|
"""
|
||||||
|
integration = await loader.async_get_integration(hass, "hue")
|
||||||
|
|
||||||
|
with (
|
||||||
|
pytest.raises(ModuleNotFoundError),
|
||||||
|
patch(
|
||||||
|
"homeassistant.loader.importlib.import_module",
|
||||||
|
side_effect=ModuleNotFoundError("Boom"),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
assert integration.get_component() == hue
|
||||||
|
|
||||||
|
with (
|
||||||
|
pytest.raises(ModuleNotFoundError),
|
||||||
|
patch(
|
||||||
|
"homeassistant.loader.importlib.import_module",
|
||||||
|
side_effect=ModuleNotFoundError("Boom"),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
assert integration.get_platform("light") == hue_light
|
||||||
|
|
||||||
|
# Hue is not loaded so we should still hit the import_module path
|
||||||
|
with (
|
||||||
|
pytest.raises(ModuleNotFoundError),
|
||||||
|
patch(
|
||||||
|
"homeassistant.loader.importlib.import_module",
|
||||||
|
side_effect=ModuleNotFoundError("Boom"),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
assert integration.get_platform("light") == hue_light
|
||||||
|
|
||||||
|
assert integration.get_component() == hue
|
||||||
|
|
||||||
|
# Hue is loaded so we should cache the import_module failure now
|
||||||
|
with (
|
||||||
|
pytest.raises(ModuleNotFoundError),
|
||||||
|
patch(
|
||||||
|
"homeassistant.loader.importlib.import_module",
|
||||||
|
side_effect=ModuleNotFoundError("Boom"),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
assert integration.get_platform("light") == hue_light
|
||||||
|
|
||||||
|
# Hue is loaded and the last call should have cached the import_module failure
|
||||||
|
with pytest.raises(ModuleNotFoundError):
|
||||||
|
assert integration.get_platform("light") == hue_light
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_platform_only_cached_module_not_found_when_component_loaded(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Test get_platform cache only cache module not found when the component is loaded."""
|
||||||
integration = await loader.async_get_integration(hass, "hue")
|
integration = await loader.async_get_integration(hass, "hue")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
@ -317,41 +371,43 @@ async def test_get_platform_caches_failures_when_component_loaded(
|
|||||||
):
|
):
|
||||||
assert integration.get_platform("light") == hue_light
|
assert integration.get_platform("light") == hue_light
|
||||||
|
|
||||||
# Hue is loaded and the last call should have cached the import_module failure
|
# ImportError is not cached because we only cache ModuleNotFoundError
|
||||||
with pytest.raises(ImportError):
|
assert integration.get_platform("light") == hue_light
|
||||||
assert integration.get_platform("light") == hue_light
|
|
||||||
|
|
||||||
|
|
||||||
async def test_async_get_platform_caches_failures_when_component_loaded(
|
async def test_async_get_platform_caches_failures_when_component_loaded(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test async_get_platform cache failures only when the component is loaded."""
|
"""Test async_get_platform caches failures only when the component is loaded.
|
||||||
|
|
||||||
|
Only ModuleNotFoundError is cached, ImportError is not cached.
|
||||||
|
"""
|
||||||
integration = await loader.async_get_integration(hass, "hue")
|
integration = await loader.async_get_integration(hass, "hue")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
pytest.raises(ImportError),
|
pytest.raises(ModuleNotFoundError),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.loader.importlib.import_module",
|
"homeassistant.loader.importlib.import_module",
|
||||||
side_effect=ImportError("Boom"),
|
side_effect=ModuleNotFoundError("Boom"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
assert integration.get_component() == hue
|
assert integration.get_component() == hue
|
||||||
|
|
||||||
with (
|
with (
|
||||||
pytest.raises(ImportError),
|
pytest.raises(ModuleNotFoundError),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.loader.importlib.import_module",
|
"homeassistant.loader.importlib.import_module",
|
||||||
side_effect=ImportError("Boom"),
|
side_effect=ModuleNotFoundError("Boom"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
assert await integration.async_get_platform("light") == hue_light
|
assert await integration.async_get_platform("light") == hue_light
|
||||||
|
|
||||||
# Hue is not loaded so we should still hit the import_module path
|
# Hue is not loaded so we should still hit the import_module path
|
||||||
with (
|
with (
|
||||||
pytest.raises(ImportError),
|
pytest.raises(ModuleNotFoundError),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.loader.importlib.import_module",
|
"homeassistant.loader.importlib.import_module",
|
||||||
side_effect=ImportError("Boom"),
|
side_effect=ModuleNotFoundError("Boom"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
assert await integration.async_get_platform("light") == hue_light
|
assert await integration.async_get_platform("light") == hue_light
|
||||||
@ -360,16 +416,16 @@ async def test_async_get_platform_caches_failures_when_component_loaded(
|
|||||||
|
|
||||||
# Hue is loaded so we should cache the import_module failure now
|
# Hue is loaded so we should cache the import_module failure now
|
||||||
with (
|
with (
|
||||||
pytest.raises(ImportError),
|
pytest.raises(ModuleNotFoundError),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.loader.importlib.import_module",
|
"homeassistant.loader.importlib.import_module",
|
||||||
side_effect=ImportError("Boom"),
|
side_effect=ModuleNotFoundError("Boom"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
assert await integration.async_get_platform("light") == hue_light
|
assert await integration.async_get_platform("light") == hue_light
|
||||||
|
|
||||||
# Hue is loaded and the last call should have cached the import_module failure
|
# Hue is loaded and the last call should have cached the import_module failure
|
||||||
with pytest.raises(ImportError):
|
with pytest.raises(ModuleNotFoundError):
|
||||||
assert await integration.async_get_platform("light") == hue_light
|
assert await integration.async_get_platform("light") == hue_light
|
||||||
|
|
||||||
# The cache should never be filled because the import error is remembered
|
# The cache should never be filled because the import error is remembered
|
||||||
@ -379,33 +435,36 @@ async def test_async_get_platform_caches_failures_when_component_loaded(
|
|||||||
async def test_async_get_platforms_caches_failures_when_component_loaded(
|
async def test_async_get_platforms_caches_failures_when_component_loaded(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test async_get_platforms cache failures only when the component is loaded."""
|
"""Test async_get_platforms cache failures only when the component is loaded.
|
||||||
|
|
||||||
|
Only ModuleNotFoundError is cached, ImportError is not cached.
|
||||||
|
"""
|
||||||
integration = await loader.async_get_integration(hass, "hue")
|
integration = await loader.async_get_integration(hass, "hue")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
pytest.raises(ImportError),
|
pytest.raises(ModuleNotFoundError),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.loader.importlib.import_module",
|
"homeassistant.loader.importlib.import_module",
|
||||||
side_effect=ImportError("Boom"),
|
side_effect=ModuleNotFoundError("Boom"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
assert integration.get_component() == hue
|
assert integration.get_component() == hue
|
||||||
|
|
||||||
with (
|
with (
|
||||||
pytest.raises(ImportError),
|
pytest.raises(ModuleNotFoundError),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.loader.importlib.import_module",
|
"homeassistant.loader.importlib.import_module",
|
||||||
side_effect=ImportError("Boom"),
|
side_effect=ModuleNotFoundError("Boom"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
assert await integration.async_get_platforms(["light"]) == {"light": hue_light}
|
assert await integration.async_get_platforms(["light"]) == {"light": hue_light}
|
||||||
|
|
||||||
# Hue is not loaded so we should still hit the import_module path
|
# Hue is not loaded so we should still hit the import_module path
|
||||||
with (
|
with (
|
||||||
pytest.raises(ImportError),
|
pytest.raises(ModuleNotFoundError),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.loader.importlib.import_module",
|
"homeassistant.loader.importlib.import_module",
|
||||||
side_effect=ImportError("Boom"),
|
side_effect=ModuleNotFoundError("Boom"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
assert await integration.async_get_platforms(["light"]) == {"light": hue_light}
|
assert await integration.async_get_platforms(["light"]) == {"light": hue_light}
|
||||||
@ -414,16 +473,16 @@ async def test_async_get_platforms_caches_failures_when_component_loaded(
|
|||||||
|
|
||||||
# Hue is loaded so we should cache the import_module failure now
|
# Hue is loaded so we should cache the import_module failure now
|
||||||
with (
|
with (
|
||||||
pytest.raises(ImportError),
|
pytest.raises(ModuleNotFoundError),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.loader.importlib.import_module",
|
"homeassistant.loader.importlib.import_module",
|
||||||
side_effect=ImportError("Boom"),
|
side_effect=ModuleNotFoundError("Boom"),
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
assert await integration.async_get_platforms(["light"]) == {"light": hue_light}
|
assert await integration.async_get_platforms(["light"]) == {"light": hue_light}
|
||||||
|
|
||||||
# Hue is loaded and the last call should have cached the import_module failure
|
# Hue is loaded and the last call should have cached the import_module failure
|
||||||
with pytest.raises(ImportError):
|
with pytest.raises(ModuleNotFoundError):
|
||||||
assert await integration.async_get_platforms(["light"]) == {"light": hue_light}
|
assert await integration.async_get_platforms(["light"]) == {"light": hue_light}
|
||||||
|
|
||||||
# The cache should never be filled because the import error is remembered
|
# The cache should never be filled because the import error is remembered
|
||||||
|
Loading…
x
Reference in New Issue
Block a user