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
if not hass.is_stopping:
hass.async_create_task(
self.async_setup(hass),
self._async_setup_retry(hass),
f"config entry retry {self.domain} {self.title}",
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
def async_shutdown(self) -> None:
"""Call when Home Assistant is stopping."""
@ -1762,11 +1771,22 @@ class ConfigEntries:
async def async_reload(self, entry_id: str) -> bool:
"""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 (entry := self.async_get_entry(entry_id)) is None:
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:
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
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(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None: