diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index ffcd1b7f3cf..32a7384c350 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -422,4 +422,5 @@ async def _async_set_up_integrations( await async_setup_multi_components(stage_2_domains) # Wrap up startup + _LOGGER.debug("Waiting for startup to wrap up") await hass.async_block_till_done() diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 70321d364b8..9c20216d4ec 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -19,6 +19,9 @@ DATA_SETUP = "setup_tasks" DATA_DEPS_REQS = "deps_reqs_processed" SLOW_SETUP_WARNING = 10 +# Since a pip install can run, we wait +# 30 minutes to timeout +SLOW_SETUP_MAX_WAIT = 1800 def setup_component(hass: core.HomeAssistant, domain: str, config: ConfigType) -> bool: @@ -167,16 +170,28 @@ async def _async_setup_component( try: if hasattr(component, "async_setup"): - result = await component.async_setup( # type: ignore + task = component.async_setup( # type: ignore hass, processed_config ) elif hasattr(component, "setup"): - result = await hass.async_add_executor_job( - component.setup, hass, processed_config # type: ignore + # This should not be replaced with hass.async_add_executor_job because + # we don't want to track this task in case it blocks startup. + task = hass.loop.run_in_executor( + None, component.setup, hass, processed_config # type: ignore ) else: log_error("No setup function defined.") return False + + result = await asyncio.wait_for(task, SLOW_SETUP_MAX_WAIT) + except asyncio.TimeoutError: + _LOGGER.error( + "Setup of %s is taking longer than %s seconds." + " Startup will proceed without waiting any longer.", + domain, + SLOW_SETUP_MAX_WAIT, + ) + return False except Exception: # pylint: disable=broad-except _LOGGER.exception("Error during setup of component %s", domain) async_notify_setup_error(hass, domain, integration.documentation) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index eb24ea971a7..039f83d7031 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -187,8 +187,8 @@ async def test_platform_warn_slow_setup(hass): assert mock_call.called # mock_calls[0] is the warning message for component setup - # mock_calls[3] is the warning message for platform setup - timeout, logger_method = mock_call.mock_calls[3][1][:2] + # mock_calls[5] is the warning message for platform setup + timeout, logger_method = mock_call.mock_calls[5][1][:2] assert timeout == entity_platform.SLOW_SETUP_WARNING assert logger_method == _LOGGER.warning diff --git a/tests/test_setup.py b/tests/test_setup.py index 4197fe7370a..820b0c4db23 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -489,13 +489,16 @@ async def test_component_warn_slow_setup(hass): result = await setup.async_setup_component(hass, "test_component1", {}) assert result assert mock_call.called - assert len(mock_call.mock_calls) == 3 + assert len(mock_call.mock_calls) == 5 timeout, logger_method = mock_call.mock_calls[0][1][:2] assert timeout == setup.SLOW_SETUP_WARNING assert logger_method == setup._LOGGER.warning + timeout, function = mock_call.mock_calls[1][1][:2] + assert timeout == setup.SLOW_SETUP_MAX_WAIT + assert mock_call().cancel.called @@ -507,7 +510,26 @@ async def test_platform_no_warn_slow(hass): with patch.object(hass.loop, "call_later") as mock_call: result = await setup.async_setup_component(hass, "test_component1", {}) assert result - assert not mock_call.called + timeout, function = mock_call.mock_calls[0][1][:2] + assert timeout == setup.SLOW_SETUP_MAX_WAIT + + +async def test_platform_error_slow_setup(hass, caplog): + """Don't block startup more than SLOW_SETUP_MAX_WAIT.""" + + with patch.object(setup, "SLOW_SETUP_MAX_WAIT", 1): + called = [] + + async def async_setup(*args): + """Tracking Setup.""" + called.append(1) + await asyncio.sleep(2) + + mock_integration(hass, MockModule("test_component1", async_setup=async_setup)) + result = await setup.async_setup_component(hass, "test_component1", {}) + assert len(called) == 1 + assert not result + assert "test_component1 is taking longer than 1 seconds" in caplog.text async def test_when_setup_already_loaded(hass):