Hold the reload lock while attempting config entry setup retry (#115538)

This commit is contained in:
J. Nick Koston 2024-04-13 10:26:41 -10:00 committed by GitHub
parent b9d4d0e15d
commit 82d0f478a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 68 additions and 1 deletions

View File

@ -699,11 +699,20 @@ class ConfigEntry:
# has started so we do not block shutdown # has started so we do not block shutdown
if not hass.is_stopping: if not hass.is_stopping:
hass.async_create_task( hass.async_create_task(
self.async_setup(hass), self._async_setup_retry(hass),
f"config entry retry {self.domain} {self.title}", f"config entry retry {self.domain} {self.title}",
eager_start=True, eager_start=True,
) )
async def _async_setup_retry(self, hass: HomeAssistant) -> None:
"""Retry setup.
We hold the reload lock during setup retry to ensure
that nothing can reload the entry while we are retrying.
"""
async with self.reload_lock:
await self.async_setup(hass)
@callback @callback
def async_shutdown(self) -> None: def async_shutdown(self) -> None:
"""Call when Home Assistant is stopping.""" """Call when Home Assistant is stopping."""
@ -1762,11 +1771,22 @@ class ConfigEntries:
async def async_reload(self, entry_id: str) -> bool: async def async_reload(self, entry_id: str) -> bool:
"""Reload an entry. """Reload an entry.
When reloading from an integration is is preferable to
call async_schedule_reload instead of this method since
it will cancel setup retry before starting this method
in a task which eliminates a race condition where the
setup retry can fire during the reload.
If an entry was not loaded, will just load. If an entry was not loaded, will just load.
""" """
if (entry := self.async_get_entry(entry_id)) is None: if (entry := self.async_get_entry(entry_id)) is None:
raise UnknownEntry raise UnknownEntry
# Cancel the setup retry task before waiting for the
# reload lock to reduce the chance of concurrent reload
# attempts.
entry.async_cancel_retry_setup()
async with entry.reload_lock: async with entry.reload_lock:
unload_result = await self.async_unload(entry_id) unload_result = await self.async_unload(entry_id)

View File

@ -1284,6 +1284,53 @@ async def test_setup_does_not_retry_during_shutdown(hass: HomeAssistant) -> None
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_reload_during_setup_retrying_waits(hass: HomeAssistant) -> None:
"""Test reloading during setup retry waits."""
entry = MockConfigEntry(domain="test")
entry.add_to_hass(hass)
load_attempts = []
sleep_duration = 0
async def _mock_setup_entry(hass, entry):
"""Mock setup entry."""
nonlocal sleep_duration
await asyncio.sleep(sleep_duration)
load_attempts.append(entry.entry_id)
raise ConfigEntryNotReady
mock_integration(hass, MockModule("test", async_setup_entry=_mock_setup_entry))
mock_platform(hass, "test.config_flow", None)
await hass.async_create_task(
hass.config_entries.async_setup(entry.entry_id), eager_start=True
)
assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY
# Now make the setup take a while so that the setup retry
# will still be in progress when the reload request comes in
sleep_duration = 0.1
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))
await asyncio.sleep(0)
# Should not raise homeassistant.config_entries.OperationNotAllowed
await hass.config_entries.async_reload(entry.entry_id)
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10))
await asyncio.sleep(0)
# Should not raise homeassistant.config_entries.OperationNotAllowed
hass.config_entries.async_schedule_reload(entry.entry_id)
await hass.async_block_till_done()
assert load_attempts == [
entry.entry_id,
entry.entry_id,
entry.entry_id,
entry.entry_id,
entry.entry_id,
]
async def test_create_entry_options( async def test_create_entry_options(
hass: HomeAssistant, manager: config_entries.ConfigEntries hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None: ) -> None: