Proceed with startup if an integration setup blocks for more than 30m (#36082)

* Proceed with startup if an integration setup blocks for more than 30m

* Fix test location

* Fix log call

* naming

* revert

* do not shield from cancelation

* Adjust test since we now cancel when we hit the timeout
This commit is contained in:
J. Nick Koston 2020-05-27 13:43:05 -05:00 committed by GitHub
parent 6fbc3b54bd
commit f626129e2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 45 additions and 7 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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):