Restore previous behavior of only waiting for new tasks at shutdown (#88740)
* Restore previous behavior of only waiting for new tasks at shutdown * cleanup * do a swap instead * await canceled tasks * await canceled tasks * fix * not needed since we no longer clear * log it * reword * wait for airvisual * tests
This commit is contained in:
parent
1d1c553d9b
commit
b5223e1196
3 changed files with 86 additions and 0 deletions
|
@ -38,6 +38,7 @@ from typing import (
|
||||||
)
|
)
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import async_timeout
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import yarl
|
import yarl
|
||||||
|
@ -711,6 +712,14 @@ class HomeAssistant:
|
||||||
"Stopping Home Assistant before startup has completed may fail"
|
"Stopping Home Assistant before startup has completed may fail"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Keep holding the reference to the tasks but do not allow them
|
||||||
|
# to block shutdown. Only tasks created after this point will
|
||||||
|
# be waited for.
|
||||||
|
running_tasks = self._tasks
|
||||||
|
# Avoid clearing here since we want the remove callbacks to fire
|
||||||
|
# and remove the tasks from the original set which is now running_tasks
|
||||||
|
self._tasks = set()
|
||||||
|
|
||||||
# Cancel all background tasks
|
# Cancel all background tasks
|
||||||
for task in self._background_tasks:
|
for task in self._background_tasks:
|
||||||
self._tasks.add(task)
|
self._tasks.add(task)
|
||||||
|
@ -749,6 +758,35 @@ class HomeAssistant:
|
||||||
self.state = CoreState.not_running
|
self.state = CoreState.not_running
|
||||||
self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE)
|
self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE)
|
||||||
|
|
||||||
|
# Make a copy of running_tasks since a task can finish
|
||||||
|
# while we are awaiting canceled tasks to get their result
|
||||||
|
# which will result in the set size changing during iteration
|
||||||
|
for task in list(running_tasks):
|
||||||
|
if task.done():
|
||||||
|
# Since we made a copy we need to check
|
||||||
|
# to see if the task finished while we
|
||||||
|
# were awaiting another task
|
||||||
|
continue
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Task %s was still running after stage 2 shutdown; "
|
||||||
|
"Integrations should cancel non-critical tasks when receiving "
|
||||||
|
"the stop event to prevent delaying shutdown",
|
||||||
|
task,
|
||||||
|
)
|
||||||
|
task.cancel()
|
||||||
|
try:
|
||||||
|
async with async_timeout.timeout(0.1):
|
||||||
|
await task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
# Task may be shielded from cancellation.
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Task %s could not be canceled during stage 3 shutdown", task
|
||||||
|
)
|
||||||
|
except Exception as ex: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Task %s error during stage 3 shutdown: %s", task, ex)
|
||||||
|
|
||||||
# Prevent run_callback_threadsafe from scheduling any additional
|
# Prevent run_callback_threadsafe from scheduling any additional
|
||||||
# callbacks in the event loop as callbacks created on the futures
|
# callbacks in the event loop as callbacks created on the futures
|
||||||
# it returns will never run after the final `self.async_block_till_done`
|
# it returns will never run after the final `self.async_block_till_done`
|
||||||
|
|
|
@ -166,3 +166,4 @@ async def test_step_reauth(
|
||||||
|
|
||||||
assert len(hass.config_entries.async_entries()) == 1
|
assert len(hass.config_entries.async_entries()) == 1
|
||||||
assert hass.config_entries.async_entries()[0].data[CONF_API_KEY] == new_api_key
|
assert hass.config_entries.async_entries()[0].data[CONF_API_KEY] == new_api_key
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
|
@ -9,6 +9,7 @@ import gc
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
||||||
|
|
||||||
|
@ -2003,3 +2004,49 @@ async def test_background_task(hass: HomeAssistant) -> None:
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
await hass.async_stop()
|
await hass.async_stop()
|
||||||
assert result.result() == ha.CoreState.stopping
|
assert result.result() == ha.CoreState.stopping
|
||||||
|
|
||||||
|
|
||||||
|
async def test_shutdown_does_not_block_on_normal_tasks(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Ensure shutdown does not block on normal tasks."""
|
||||||
|
result = asyncio.Future()
|
||||||
|
unshielded_task = asyncio.sleep(10)
|
||||||
|
|
||||||
|
async def test_task():
|
||||||
|
try:
|
||||||
|
await unshielded_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
result.set_result(hass.state)
|
||||||
|
|
||||||
|
start = time.monotonic()
|
||||||
|
task = hass.async_create_task(test_task())
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
await hass.async_stop()
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
assert result.done()
|
||||||
|
assert task.done()
|
||||||
|
assert time.monotonic() - start < 0.5
|
||||||
|
|
||||||
|
|
||||||
|
async def test_shutdown_does_not_block_on_shielded_tasks(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
) -> None:
|
||||||
|
"""Ensure shutdown does not block on shielded tasks."""
|
||||||
|
result = asyncio.Future()
|
||||||
|
shielded_task = asyncio.shield(asyncio.sleep(10))
|
||||||
|
|
||||||
|
async def test_task():
|
||||||
|
try:
|
||||||
|
await shielded_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
result.set_result(hass.state)
|
||||||
|
|
||||||
|
start = time.monotonic()
|
||||||
|
task = hass.async_create_task(test_task())
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
await hass.async_stop()
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
assert result.done()
|
||||||
|
assert task.done()
|
||||||
|
assert time.monotonic() - start < 0.5
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue