Skip to content

Commit edd75a9

Browse files
authored
Fix race in TimestampDataUpdateCoordinator (#115542)
* Fix race in TimestampDataUpdateCoordinator The last_update_success_time value was being set after the listeners were fired which could lead to a loop because the listener may re-trigger an update because it thinks the data is stale * coverage * docstring
1 parent 08e2b65 commit edd75a9

File tree

2 files changed

+48
-16
lines changed

2 files changed

+48
-16
lines changed

homeassistant/helpers/update_coordinator.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,8 @@ async def _async_refresh( # noqa: C901
401401
if not auth_failed and self._listeners and not self.hass.is_stopping:
402402
self._schedule_refresh()
403403

404+
self._async_refresh_finished()
405+
404406
if not self.last_update_success and not previous_update_success:
405407
return
406408

@@ -411,6 +413,15 @@ async def _async_refresh( # noqa: C901
411413
):
412414
self.async_update_listeners()
413415

416+
@callback
417+
def _async_refresh_finished(self) -> None:
418+
"""Handle when a refresh has finished.
419+
420+
Called when refresh is finished before listeners are updated.
421+
422+
To be overridden by subclasses.
423+
"""
424+
414425
@callback
415426
def async_set_update_error(self, err: Exception) -> None:
416427
"""Manually set an error, log the message and notify listeners."""
@@ -444,20 +455,9 @@ class TimestampDataUpdateCoordinator(DataUpdateCoordinator[_DataT]):
444455

445456
last_update_success_time: datetime | None = None
446457

447-
async def _async_refresh(
448-
self,
449-
log_failures: bool = True,
450-
raise_on_auth_failed: bool = False,
451-
scheduled: bool = False,
452-
raise_on_entry_error: bool = False,
453-
) -> None:
454-
"""Refresh data."""
455-
await super()._async_refresh(
456-
log_failures,
457-
raise_on_auth_failed,
458-
scheduled,
459-
raise_on_entry_error,
460-
)
458+
@callback
459+
def _async_refresh_finished(self) -> None:
460+
"""Handle when a refresh has finished."""
461461
if self.last_update_success:
462462
self.last_update_success_time = utcnow()
463463

tests/helpers/test_update_coordinator.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Tests for the update coordinator."""
22

3-
from datetime import timedelta
3+
from datetime import datetime, timedelta
44
import logging
55
from unittest.mock import AsyncMock, Mock, patch
66
import urllib.error
@@ -12,7 +12,7 @@
1212

1313
from homeassistant import config_entries
1414
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
15-
from homeassistant.core import CoreState, HomeAssistant
15+
from homeassistant.core import CoreState, HomeAssistant, callback
1616
from homeassistant.exceptions import ConfigEntryNotReady
1717
from homeassistant.helpers import update_coordinator
1818
from homeassistant.util.dt import utcnow
@@ -715,3 +715,35 @@ async def _update_method() -> int:
715715
update_callback.reset_mock()
716716

717717
remove_callbacks()
718+
719+
720+
async def test_timestamp_date_update_coordinator(hass: HomeAssistant) -> None:
721+
"""Test last_update_success_time is set before calling listeners."""
722+
last_update_success_times: list[datetime | None] = []
723+
724+
async def refresh() -> int:
725+
return 1
726+
727+
crd = update_coordinator.TimestampDataUpdateCoordinator[int](
728+
hass,
729+
_LOGGER,
730+
name="test",
731+
update_method=refresh,
732+
update_interval=timedelta(seconds=10),
733+
)
734+
735+
@callback
736+
def listener():
737+
last_update_success_times.append(crd.last_update_success_time)
738+
739+
unsub = crd.async_add_listener(listener)
740+
741+
await crd.async_refresh()
742+
743+
assert len(last_update_success_times) == 1
744+
# Ensure the time is set before the listener is called
745+
assert last_update_success_times != [None]
746+
747+
unsub()
748+
await crd.async_refresh()
749+
assert len(last_update_success_times) == 1

0 commit comments

Comments
 (0)