Add background tasks to config entries (#88335)

* Use a set for config entries task tracking

* Allow adding background tasks to config entries

* Add tests for config entry add tasks

* Update docstrings on core create task

* Migrate roon and august

* Use in more places

* Guard for None
pull/88292/head
Paulus Schoutsen 2023-02-17 13:50:05 -05:00 committed by GitHub
parent 2b8abf84bd
commit 3a32d2bdcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 82 additions and 21 deletions

View File

@ -191,8 +191,8 @@ class AugustData(AugustSubscriberMixin):
# Do not prevent setup as the sync can timeout # Do not prevent setup as the sync can timeout
# but it is not a fatal error as the lock # but it is not a fatal error as the lock
# will recover automatically when it comes back online. # will recover automatically when it comes back online.
self._config_entry.async_on_unload( self._config_entry.async_create_background_task(
asyncio.create_task(self._async_initial_sync()).cancel self._hass, self._async_initial_sync(), "august-initial-sync"
) )
async def _async_initial_sync(self): async def _async_initial_sync(self):

View File

@ -245,6 +245,8 @@ async def async_setup_entry(
class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]): class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]):
"""Coordinator for calendar RPC calls that use an efficient sync.""" """Coordinator for calendar RPC calls that use an efficient sync."""
config_entry: ConfigEntry
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
@ -299,6 +301,8 @@ class CalendarQueryUpdateCoordinator(DataUpdateCoordinator[list[Event]]):
for limitations in the calendar API for supporting search. for limitations in the calendar API for supporting search.
""" """
config_entry: ConfigEntry
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
@ -434,7 +438,9 @@ class GoogleCalendarEntity(
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
self._apply_coordinator_update() self._apply_coordinator_update()
self.hass.async_create_background_task(refresh(), "google.calendar-refresh") self.coordinator.config_entry.async_create_background_task(
self.hass, refresh(), "google.calendar-refresh"
)
async def async_get_events( async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime self, hass: HomeAssistant, start_date: datetime, end_date: datetime

View File

@ -145,7 +145,7 @@ class QswFirmwareEntity(CoordinatorEntity[QswFirmwareCoordinator]):
def get_device_value(self, key: str, subkey: str) -> Any: def get_device_value(self, key: str, subkey: str) -> Any:
"""Return device value by key.""" """Return device value by key."""
value = None value = None
if key in self.coordinator.data: if self.coordinator.data is not None and key in self.coordinator.data:
data = self.coordinator.data[key] data = self.coordinator.data[key]
if subkey in data: if subkey in data:
value = data[subkey] value = data[subkey]

View File

@ -66,8 +66,8 @@ class RoonServer:
) )
# Initialize Roon background polling # Initialize Roon background polling
self.config_entry.async_on_unload( self.config_entry.async_create_background_task(
asyncio.create_task(self.async_do_loop()).cancel self.hass, self.async_do_loop(), "roon.server-do-loop"
) )
return True return True

View File

@ -483,7 +483,8 @@ class SonosDiscoveryManager:
if uid not in self.data.discovery_known: if uid not in self.data.discovery_known:
_LOGGER.debug("New %s discovery uid=%s: %s", source, uid, info) _LOGGER.debug("New %s discovery uid=%s: %s", source, uid, info)
self.data.discovery_known.add(uid) self.data.discovery_known.add(uid)
self.hass.async_create_background_task( self.entry.async_create_background_task(
self.hass,
self._async_handle_discovery_message( self._async_handle_discovery_message(
uid, uid,
discovered_ip, discovered_ip,

View File

@ -254,8 +254,8 @@ class ZHAGateway:
) )
# background the fetching of state for mains powered devices # background the fetching of state for mains powered devices
self._hass.async_create_background_task( self.config_entry.async_create_background_task(
fetch_updated_state(), "zha.gateway-fetch_updated_state" self._hass, fetch_updated_state(), "zha.gateway-fetch_updated_state"
) )
def device_joined(self, device: zigpy.device.Device) -> None: def device_joined(self, device: zigpy.device.Device) -> None:

View File

@ -612,7 +612,9 @@ class BaseLight(LogMixin, light.LightEntity):
) )
if self._debounced_member_refresh is not None: if self._debounced_member_refresh is not None:
self.debug("transition complete - refreshing group member states") self.debug("transition complete - refreshing group member states")
self.hass.async_create_background_task( assert self.platform and self.platform.config_entry
self.platform.config_entry.async_create_background_task(
self.hass,
self._debounced_member_refresh.async_call(), self._debounced_member_refresh.async_call(),
"zha.light-refresh-debounced-member", "zha.light-refresh-debounced-member",
) )

View File

@ -220,7 +220,8 @@ class ConfigEntry:
"_async_cancel_retry_setup", "_async_cancel_retry_setup",
"_on_unload", "_on_unload",
"reload_lock", "reload_lock",
"_pending_tasks", "_tasks",
"_background_tasks",
) )
def __init__( def __init__(
@ -315,7 +316,8 @@ class ConfigEntry:
# Reload lock to prevent conflicting reloads # Reload lock to prevent conflicting reloads
self.reload_lock = asyncio.Lock() self.reload_lock = asyncio.Lock()
self._pending_tasks: list[asyncio.Future[Any]] = [] self._tasks: set[asyncio.Future[Any]] = set()
self._background_tasks: set[asyncio.Future[Any]] = set()
async def async_setup( async def async_setup(
self, self,
@ -681,11 +683,23 @@ class ConfigEntry:
while self._on_unload: while self._on_unload:
self._on_unload.pop()() self._on_unload.pop()()
while self._pending_tasks: if not self._tasks and not self._background_tasks:
pending = [task for task in self._pending_tasks if not task.done()] return
self._pending_tasks.clear()
if pending: for task in self._background_tasks:
await asyncio.gather(*pending) task.cancel()
_, pending = await asyncio.wait(
[*self._tasks, *self._background_tasks], timeout=10
)
for task in pending:
_LOGGER.warning(
"Unloading %s (%s) config entry. Task %s did not complete in time",
self.title,
self.domain,
task,
)
@callback @callback
def async_start_reauth( def async_start_reauth(
@ -736,9 +750,24 @@ class ConfigEntry:
target: target to call. target: target to call.
""" """
task = hass.async_create_task(target) task = hass.async_create_task(target)
self._tasks.add(task)
task.add_done_callback(self._tasks.remove)
self._pending_tasks.append(task) return task
@callback
def async_create_background_task(
self, hass: HomeAssistant, target: Coroutine[Any, Any, _R], name: str
) -> asyncio.Task[_R]:
"""Create a background task tied to the config entry lifecycle.
Background tasks are automatically canceled when config entry is unloaded.
target: target to call.
"""
task = hass.async_create_background_task(target, name)
self._background_tasks.add(task)
task.add_done_callback(self._background_tasks.remove)
return task return task

View File

@ -514,7 +514,8 @@ class HomeAssistant:
def async_create_task(self, target: Coroutine[Any, Any, _R]) -> asyncio.Task[_R]: def async_create_task(self, target: Coroutine[Any, Any, _R]) -> asyncio.Task[_R]:
"""Create a task from within the eventloop. """Create a task from within the eventloop.
This method must be run in the event loop. This method must be run in the event loop. If you are using this in your
integration, use the create task methods on the config entry instead.
target: target to call. target: target to call.
""" """
@ -533,8 +534,7 @@ class HomeAssistant:
This is a background task which will not block startup and will be This is a background task which will not block startup and will be
automatically cancelled on shutdown. If you are using this in your automatically cancelled on shutdown. If you are using this in your
integration, make sure you also cancel the task when the config entry integration, use the create task methods on the config entry instead.
your task belongs to is unloaded.
This method must be run in the event loop. This method must be run in the event loop.
""" """

View File

@ -3573,3 +3573,26 @@ async def test_initializing_flows_canceled_on_shutdown(hass: HomeAssistant, mana
with pytest.raises(asyncio.exceptions.CancelledError): with pytest.raises(asyncio.exceptions.CancelledError):
await task await task
async def test_task_tracking(hass):
"""Test task tracking for a config entry."""
entry = MockConfigEntry(title="test_title", domain="test")
event = asyncio.Event()
results = []
async def test_task():
try:
await event.wait()
results.append("normal")
except asyncio.CancelledError:
results.append("background")
raise
entry.async_create_task(hass, test_task())
entry.async_create_background_task(hass, test_task(), "background-task-name")
await asyncio.sleep(0)
hass.loop.call_soon(event.set)
await entry._async_process_on_unload()
assert results == ["background", "normal"]