Fix race in config entry setup again (#111800)

Because the setup again was scheduled as a task, it would
not unset self._async_cancel_retry_setup in time and we would
try to unsub self._async_cancel_retry_setup after it had already
fired. Change it to call a callback that runs right away so it
unsets self._async_cancel_retry_setup as soon as its called
so there is no race

fixes #111796
This commit is contained in:
J. Nick Koston 2024-02-29 04:47:36 -10:00 committed by GitHub
parent bc6b4d01c8
commit 63c3d6e113
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 22 additions and 18 deletions

View File

@ -33,6 +33,7 @@ from .core import (
CoreState, CoreState,
Event, Event,
HassJob, HassJob,
HassJobType,
HomeAssistant, HomeAssistant,
callback, callback,
) )
@ -363,7 +364,6 @@ class ConfigEntry:
self._integration_for_domain: loader.Integration | None = None self._integration_for_domain: loader.Integration | None = None
self._tries = 0 self._tries = 0
self._setup_again_job: HassJob | None = None
def __repr__(self) -> str: def __repr__(self) -> str:
"""Representation of ConfigEntry.""" """Representation of ConfigEntry."""
@ -555,12 +555,18 @@ class ConfigEntry:
if hass.state is CoreState.running: if hass.state is CoreState.running:
self._async_cancel_retry_setup = async_call_later( self._async_cancel_retry_setup = async_call_later(
hass, wait_time, self._async_get_setup_again_job(hass) hass,
wait_time,
HassJob(
functools.partial(self._async_setup_again, hass),
job_type=HassJobType.Callback,
),
) )
else: else:
self._async_cancel_retry_setup = hass.bus.async_listen_once( self._async_cancel_retry_setup = hass.bus.async_listen(
EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STARTED,
functools.partial(self._async_setup_again, hass), functools.partial(self._async_setup_again, hass),
run_immediately=True,
) )
await self._async_process_on_unload(hass) await self._async_process_on_unload(hass)
@ -585,28 +591,25 @@ class ConfigEntry:
if not domain_is_integration: if not domain_is_integration:
return return
self.async_cancel_retry_setup()
if result: if result:
self._async_set_state(hass, ConfigEntryState.LOADED, None) self._async_set_state(hass, ConfigEntryState.LOADED, None)
else: else:
self._async_set_state(hass, ConfigEntryState.SETUP_ERROR, error_reason) self._async_set_state(hass, ConfigEntryState.SETUP_ERROR, error_reason)
async def _async_setup_again(self, hass: HomeAssistant, *_: Any) -> None: @callback
"""Run setup again.""" def _async_setup_again(self, hass: HomeAssistant, *_: Any) -> None:
"""Schedule setup again.
This method is a callback to ensure that _async_cancel_retry_setup
is unset as soon as its callback is called.
"""
self._async_cancel_retry_setup = None
# Check again when we fire in case shutdown # Check again when we fire in case shutdown
# 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:
self._async_cancel_retry_setup = None hass.async_create_task(self.async_setup(hass), eager_start=True)
await self.async_setup(hass)
@callback
def _async_get_setup_again_job(self, hass: HomeAssistant) -> HassJob:
"""Get a job that will call setup again."""
if not self._setup_again_job:
self._setup_again_job = HassJob(
functools.partial(self._async_setup_again, hass),
cancel_on_shutdown=True,
)
return self._setup_again_job
@callback @callback
def async_shutdown(self) -> None: def async_shutdown(self) -> None:

View File

@ -1174,7 +1174,8 @@ async def test_setup_raise_not_ready(
mock_setup_entry.side_effect = None mock_setup_entry.side_effect = None
mock_setup_entry.return_value = True mock_setup_entry.return_value = True
await hass.async_run_hass_job(p_setup, None) hass.async_run_hass_job(p_setup, None)
await hass.async_block_till_done()
assert entry.state is config_entries.ConfigEntryState.LOADED assert entry.state is config_entries.ConfigEntryState.LOADED
assert entry.reason is None assert entry.reason is None