mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Fix lingering timer in cloud (#90822)
* Fix lingering timer in cloud * Rename variable * Improve * Improve again * Adjust * Adjust * Add property to HassJob instead * Adjust * Rename * Adjust * Adjust * Make it read-only * Add specific test
This commit is contained in:
parent
175f38b68a
commit
9705607db4
@ -18,7 +18,7 @@ from homeassistant.const import (
|
|||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_validation as cv, entityfilter
|
from homeassistant.helpers import config_validation as cv, entityfilter
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
@ -311,7 +311,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
async_call_later(
|
async_call_later(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
delay=timedelta(hours=STARTUP_REPAIR_DELAY),
|
delay=timedelta(hours=STARTUP_REPAIR_DELAY),
|
||||||
action=async_startup_repairs,
|
action=HassJob(
|
||||||
|
async_startup_repairs, "cloud startup repairs", cancel_on_shutdown=True
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -217,13 +217,25 @@ class HassJob(Generic[_P, _R_co]):
|
|||||||
we run the job.
|
we run the job.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("job_type", "target", "name")
|
__slots__ = ("job_type", "target", "name", "_cancel_on_shutdown")
|
||||||
|
|
||||||
def __init__(self, target: Callable[_P, _R_co], name: str | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
target: Callable[_P, _R_co],
|
||||||
|
name: str | None = None,
|
||||||
|
*,
|
||||||
|
cancel_on_shutdown: bool | None = None,
|
||||||
|
) -> None:
|
||||||
"""Create a job object."""
|
"""Create a job object."""
|
||||||
self.target = target
|
self.target = target
|
||||||
self.name = name
|
self.name = name
|
||||||
self.job_type = _get_hassjob_callable_job_type(target)
|
self.job_type = _get_hassjob_callable_job_type(target)
|
||||||
|
self._cancel_on_shutdown = cancel_on_shutdown
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cancel_on_shutdown(self) -> bool | None:
|
||||||
|
"""Return if the job should be cancelled on shutdown."""
|
||||||
|
return self._cancel_on_shutdown
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Return the job."""
|
"""Return the job."""
|
||||||
@ -730,6 +742,7 @@ class HomeAssistant:
|
|||||||
self._tasks.add(task)
|
self._tasks.add(task)
|
||||||
task.add_done_callback(self._tasks.remove)
|
task.add_done_callback(self._tasks.remove)
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
self._cancel_cancellable_timers()
|
||||||
|
|
||||||
self.exit_code = exit_code
|
self.exit_code = exit_code
|
||||||
|
|
||||||
@ -814,6 +827,20 @@ class HomeAssistant:
|
|||||||
if self._stopped is not None:
|
if self._stopped is not None:
|
||||||
self._stopped.set()
|
self._stopped.set()
|
||||||
|
|
||||||
|
def _cancel_cancellable_timers(self) -> None:
|
||||||
|
"""Cancel timer handles marked as cancellable."""
|
||||||
|
# pylint: disable-next=protected-access
|
||||||
|
handles: Iterable[asyncio.TimerHandle] = self.loop._scheduled # type: ignore[attr-defined]
|
||||||
|
for handle in handles:
|
||||||
|
if (
|
||||||
|
not handle.cancelled()
|
||||||
|
and (args := handle._args) # pylint: disable=protected-access
|
||||||
|
# pylint: disable-next=unidiomatic-typecheck
|
||||||
|
and type(job := args[0]) is HassJob
|
||||||
|
and job.cancel_on_shutdown
|
||||||
|
):
|
||||||
|
handle.cancel()
|
||||||
|
|
||||||
def _async_log_running_tasks(self, stage: int) -> None:
|
def _async_log_running_tasks(self, stage: int) -> None:
|
||||||
"""Log all running tasks."""
|
"""Log all running tasks."""
|
||||||
for task in self._tasks:
|
for task in self._tasks:
|
||||||
|
@ -355,7 +355,6 @@ async def test_webhook_config_flow_registers_webhook(
|
|||||||
assert result["data"]["webhook_id"] is not None
|
assert result["data"]["webhook_id"] is not None
|
||||||
|
|
||||||
|
|
||||||
@patch("homeassistant.components.cloud.STARTUP_REPAIR_DELAY", 0)
|
|
||||||
async def test_webhook_create_cloudhook(
|
async def test_webhook_create_cloudhook(
|
||||||
hass: HomeAssistant, webhook_flow_conf: None
|
hass: HomeAssistant, webhook_flow_conf: None
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -411,7 +410,6 @@ async def test_webhook_create_cloudhook(
|
|||||||
assert result["require_restart"] is False
|
assert result["require_restart"] is False
|
||||||
|
|
||||||
|
|
||||||
@patch("homeassistant.components.cloud.STARTUP_REPAIR_DELAY", 0)
|
|
||||||
async def test_webhook_create_cloudhook_aborts_not_connected(
|
async def test_webhook_create_cloudhook_aborts_not_connected(
|
||||||
hass: HomeAssistant, webhook_flow_conf: None
|
hass: HomeAssistant, webhook_flow_conf: None
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -551,7 +551,6 @@ async def test_setup_hass_takes_longer_than_log_slow_startup(
|
|||||||
assert "Waiting on integrations to complete setup" in caplog.text
|
assert "Waiting on integrations to complete setup" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@patch("homeassistant.components.cloud.STARTUP_REPAIR_DELAY", 0)
|
|
||||||
async def test_setup_hass_invalid_yaml(
|
async def test_setup_hass_invalid_yaml(
|
||||||
mock_enable_logging: Mock,
|
mock_enable_logging: Mock,
|
||||||
mock_is_virtual_env: Mock,
|
mock_is_virtual_env: Mock,
|
||||||
@ -607,7 +606,6 @@ async def test_setup_hass_config_dir_nonexistent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@patch("homeassistant.components.cloud.STARTUP_REPAIR_DELAY", 0)
|
|
||||||
async def test_setup_hass_safe_mode(
|
async def test_setup_hass_safe_mode(
|
||||||
mock_enable_logging: Mock,
|
mock_enable_logging: Mock,
|
||||||
mock_is_virtual_env: Mock,
|
mock_is_virtual_env: Mock,
|
||||||
@ -642,7 +640,6 @@ async def test_setup_hass_safe_mode(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("hass_config", [{"homeassistant": {"non-existing": 1}}])
|
@pytest.mark.parametrize("hass_config", [{"homeassistant": {"non-existing": 1}}])
|
||||||
@patch("homeassistant.components.cloud.STARTUP_REPAIR_DELAY", 0)
|
|
||||||
async def test_setup_hass_invalid_core_config(
|
async def test_setup_hass_invalid_core_config(
|
||||||
mock_hass_config: None,
|
mock_hass_config: None,
|
||||||
mock_enable_logging: Mock,
|
mock_enable_logging: Mock,
|
||||||
@ -681,7 +678,6 @@ async def test_setup_hass_invalid_core_config(
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@patch("homeassistant.components.cloud.STARTUP_REPAIR_DELAY", 0)
|
|
||||||
async def test_setup_safe_mode_if_no_frontend(
|
async def test_setup_safe_mode_if_no_frontend(
|
||||||
mock_hass_config: None,
|
mock_hass_config: None,
|
||||||
mock_enable_logging: Mock,
|
mock_enable_logging: Mock,
|
||||||
|
@ -33,7 +33,7 @@ from homeassistant.const import (
|
|||||||
__version__,
|
__version__,
|
||||||
)
|
)
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HassJob, HomeAssistant, State
|
||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import (
|
||||||
InvalidEntityFormatError,
|
InvalidEntityFormatError,
|
||||||
InvalidStateError,
|
InvalidStateError,
|
||||||
@ -2081,3 +2081,26 @@ async def test_shutdown_does_not_block_on_shielded_tasks(
|
|||||||
|
|
||||||
# Cleanup lingering task after test is done
|
# Cleanup lingering task after test is done
|
||||||
sleep_task.cancel()
|
sleep_task.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cancellable_hassjob(hass: HomeAssistant) -> None:
|
||||||
|
"""Simulate a shutdown, ensure cancellable jobs are cancelled."""
|
||||||
|
job = MagicMock()
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
|
def run_job(job: HassJob) -> None:
|
||||||
|
"""Call the action."""
|
||||||
|
hass.async_run_hass_job(job)
|
||||||
|
|
||||||
|
timer1 = hass.loop.call_later(
|
||||||
|
60, run_job, HassJob(ha.callback(job), cancel_on_shutdown=True)
|
||||||
|
)
|
||||||
|
timer2 = hass.loop.call_later(60, run_job, HassJob(ha.callback(job)))
|
||||||
|
|
||||||
|
await hass.async_stop()
|
||||||
|
|
||||||
|
assert timer1.cancelled()
|
||||||
|
assert not timer2.cancelled()
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
timer2.cancel()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user