Fix race in config entry setup (#117756)

This commit is contained in:
J. Nick Koston 2024-05-19 21:47:47 -10:00 committed by GitHub
parent d11003ef12
commit 13ba8e62a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 94 additions and 0 deletions

View File

@ -728,6 +728,17 @@ class ConfigEntry(Generic[_DataT]):
) -> None: ) -> None:
"""Set up while holding the setup lock.""" """Set up while holding the setup lock."""
async with self.setup_lock: async with self.setup_lock:
if self.state is ConfigEntryState.LOADED:
# If something loaded the config entry while
# we were waiting for the lock, we should not
# set it up again.
_LOGGER.debug(
"Not setting up %s (%s %s) again, already loaded",
self.title,
self.domain,
self.entry_id,
)
return
await self.async_setup(hass, integration=integration) await self.async_setup(hass, integration=integration)
@callback @callback

View File

@ -90,6 +90,89 @@ async def manager(hass: HomeAssistant) -> config_entries.ConfigEntries:
return manager return manager
async def test_setup_race_only_setup_once(hass: HomeAssistant) -> None:
"""Test ensure that config entries are only setup once."""
attempts = 0
slow_config_entry_setup_future = hass.loop.create_future()
fast_config_entry_setup_future = hass.loop.create_future()
slow_setup_future = hass.loop.create_future()
async def async_setup(hass, config):
"""Mock setup."""
await slow_setup_future
return True
async def async_setup_entry(hass, entry):
"""Mock setup entry."""
slow = entry.data["slow"]
if slow:
await slow_config_entry_setup_future
return True
nonlocal attempts
attempts += 1
if attempts == 1:
raise ConfigEntryNotReady
await fast_config_entry_setup_future
return True
async def async_unload_entry(hass, entry):
"""Mock unload entry."""
return True
mock_integration(
hass,
MockModule(
"comp",
async_setup=async_setup,
async_setup_entry=async_setup_entry,
async_unload_entry=async_unload_entry,
),
)
mock_platform(hass, "comp.config_flow", None)
entry = MockConfigEntry(domain="comp", data={"slow": False})
entry.add_to_hass(hass)
entry2 = MockConfigEntry(domain="comp", data={"slow": True})
entry2.add_to_hass(hass)
await entry2.setup_lock.acquire()
async def _async_reload_entry(entry: MockConfigEntry):
async with entry.setup_lock:
await entry.async_unload(hass)
await entry.async_setup(hass)
hass.async_create_task(_async_reload_entry(entry2))
setup_task = hass.async_create_task(async_setup_component(hass, "comp", {}))
entry2.setup_lock.release()
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
assert entry2.state is config_entries.ConfigEntryState.NOT_LOADED
assert "comp" not in hass.config.components
slow_setup_future.set_result(None)
await asyncio.sleep(0)
assert "comp" in hass.config.components
assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY
assert entry2.state is config_entries.ConfigEntryState.SETUP_IN_PROGRESS
fast_config_entry_setup_future.set_result(None)
# Make sure setup retry is started
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5))
slow_config_entry_setup_future.set_result(None)
await hass.async_block_till_done()
assert entry.state is config_entries.ConfigEntryState.LOADED
await hass.async_block_till_done()
assert attempts == 2
await hass.async_block_till_done()
assert setup_task.done()
assert entry2.state is config_entries.ConfigEntryState.LOADED
async def test_call_setup_entry(hass: HomeAssistant) -> None: async def test_call_setup_entry(hass: HomeAssistant) -> None:
"""Test we call <component>.setup_entry.""" """Test we call <component>.setup_entry."""
entry = MockConfigEntry(domain="comp") entry = MockConfigEntry(domain="comp")