Fix stop listener memory leak in DataUpdateCoordinator on retry (#49186)
* Fix stop listener leak in DataUpdateCoordinator When an integration retries setup it will add a new stop listener * Skip scheduled refreshes when hass is stopping * Update homeassistant/helpers/update_coordinator.py * ensure manual refresh after stop
This commit is contained in:
parent
63fa9f7dd8
commit
8bee25c938
2 changed files with 19 additions and 8 deletions
|
@ -12,7 +12,6 @@ import aiohttp
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
|
||||||
from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers import entity, event
|
from homeassistant.helpers import entity, event
|
||||||
|
@ -74,10 +73,6 @@ class DataUpdateCoordinator(Generic[T]):
|
||||||
|
|
||||||
self._debounced_refresh = request_refresh_debouncer
|
self._debounced_refresh = request_refresh_debouncer
|
||||||
|
|
||||||
self.hass.bus.async_listen_once(
|
|
||||||
EVENT_HOMEASSISTANT_STOP, self._async_stop_refresh
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_add_listener(self, update_callback: CALLBACK_TYPE) -> Callable[[], None]:
|
def async_add_listener(self, update_callback: CALLBACK_TYPE) -> Callable[[], None]:
|
||||||
"""Listen for data updates."""
|
"""Listen for data updates."""
|
||||||
|
@ -128,7 +123,7 @@ class DataUpdateCoordinator(Generic[T]):
|
||||||
async def _handle_refresh_interval(self, _now: datetime) -> None:
|
async def _handle_refresh_interval(self, _now: datetime) -> None:
|
||||||
"""Handle a refresh interval occurrence."""
|
"""Handle a refresh interval occurrence."""
|
||||||
self._unsub_refresh = None
|
self._unsub_refresh = None
|
||||||
await self.async_refresh()
|
await self._async_refresh(log_failures=True, scheduled=True)
|
||||||
|
|
||||||
async def async_request_refresh(self) -> None:
|
async def async_request_refresh(self) -> None:
|
||||||
"""Request a refresh.
|
"""Request a refresh.
|
||||||
|
@ -162,7 +157,10 @@ class DataUpdateCoordinator(Generic[T]):
|
||||||
await self._async_refresh(log_failures=True)
|
await self._async_refresh(log_failures=True)
|
||||||
|
|
||||||
async def _async_refresh(
|
async def _async_refresh(
|
||||||
self, log_failures: bool = True, raise_on_auth_failed: bool = False
|
self,
|
||||||
|
log_failures: bool = True,
|
||||||
|
raise_on_auth_failed: bool = False,
|
||||||
|
scheduled: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Refresh data."""
|
"""Refresh data."""
|
||||||
if self._unsub_refresh:
|
if self._unsub_refresh:
|
||||||
|
@ -170,6 +168,10 @@ class DataUpdateCoordinator(Generic[T]):
|
||||||
self._unsub_refresh = None
|
self._unsub_refresh = None
|
||||||
|
|
||||||
self._debounced_refresh.async_cancel()
|
self._debounced_refresh.async_cancel()
|
||||||
|
|
||||||
|
if scheduled and self.hass.is_stopping:
|
||||||
|
return
|
||||||
|
|
||||||
start = monotonic()
|
start = monotonic()
|
||||||
auth_failed = False
|
auth_failed = False
|
||||||
|
|
||||||
|
@ -249,7 +251,7 @@ class DataUpdateCoordinator(Generic[T]):
|
||||||
self.name,
|
self.name,
|
||||||
monotonic() - start,
|
monotonic() - start,
|
||||||
)
|
)
|
||||||
if not auth_failed and self._listeners:
|
if not auth_failed and self._listeners and not self.hass.is_stopping:
|
||||||
self._schedule_refresh()
|
self._schedule_refresh()
|
||||||
|
|
||||||
for update_callback in self._listeners:
|
for update_callback in self._listeners:
|
||||||
|
|
|
@ -335,6 +335,15 @@ async def test_stop_refresh_on_ha_stop(hass, crd):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert crd.data == 1
|
assert crd.data == 1
|
||||||
|
|
||||||
|
# Ensure we can still manually refresh after stop
|
||||||
|
await crd.async_refresh()
|
||||||
|
assert crd.data == 2
|
||||||
|
|
||||||
|
# ...and that the manual refresh doesn't setup another scheduled refresh
|
||||||
|
async_fire_time_changed(hass, utcnow() + update_interval)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert crd.data == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"err_msg",
|
"err_msg",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue